mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-19 07:54:38 +00:00
fix(serve): apply auto-fixes from /review (#4113)
- canonicalizeWorkspace: narrow catch to ENOENT only, propagate other filesystem errors - listWorkspaceSessions: add fast-path string equality to avoid realpathSync on every poll - GET /workspace/:id/sessions: return 400 workspace_mismatch for cross-workspace queries - SessionNotFoundError: accept optional extra message; clarify agent-crash-on-spawn case - requireWorkspaceCwd: distinguish empty-string (post-§02 bug) from absent (pre-§02 daemon)
This commit is contained in:
parent
b34a28bfca
commit
0c6e963cd2
3 changed files with 47 additions and 11 deletions
|
|
@ -223,8 +223,8 @@ export interface HttpAcpBridge {
|
||||||
*/
|
*/
|
||||||
export class SessionNotFoundError extends Error {
|
export class SessionNotFoundError extends Error {
|
||||||
readonly sessionId: string;
|
readonly sessionId: string;
|
||||||
constructor(sessionId: string) {
|
constructor(sessionId: string, extra?: string) {
|
||||||
super(`No session with id "${sessionId}"`);
|
super(`No session with id "${sessionId}"` + (extra ? `. ${extra}` : ''));
|
||||||
this.name = 'SessionNotFoundError';
|
this.name = 'SessionNotFoundError';
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
@ -1680,12 +1680,14 @@ export function createHttpAcpBridge(opts: BridgeOptions): HttpAcpBridge {
|
||||||
// continuation running (e.g. channel.exited firing during
|
// continuation running (e.g. channel.exited firing during
|
||||||
// a crash spawn, or a direct bridge.killSession call from
|
// a crash spawn, or a direct bridge.killSession call from
|
||||||
// outside the route handler). In those cases byId.get()
|
// outside the route handler). In those cases byId.get()
|
||||||
// returned undefined; we'd otherwise return
|
// returned undefined. Fail loud with a descriptive error
|
||||||
// `{ attached: true, sessionId: <zombie> }` and every
|
// so the caller can distinguish "immediate agent death"
|
||||||
// subsequent prompt/cancel call would 404. Fail loud
|
// from a stale sessionId and retry into a fresh spawn.
|
||||||
// instead so the caller can retry into a fresh spawn.
|
|
||||||
if (!attachedEntry) {
|
if (!attachedEntry) {
|
||||||
throw new SessionNotFoundError(session.sessionId);
|
throw new SessionNotFoundError(
|
||||||
|
session.sessionId,
|
||||||
|
'the agent child likely crashed during initialization — retry to spawn a new session',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (req.modelServiceId) {
|
if (req.modelServiceId) {
|
||||||
// Same swallow as above — we picked up an in-flight
|
// Same swallow as above — we picked up an in-flight
|
||||||
|
|
@ -1897,7 +1899,16 @@ export function createHttpAcpBridge(opts: BridgeOptions): HttpAcpBridge {
|
||||||
|
|
||||||
listWorkspaceSessions(workspaceCwd) {
|
listWorkspaceSessions(workspaceCwd) {
|
||||||
if (!path.isAbsolute(workspaceCwd)) return [];
|
if (!path.isAbsolute(workspaceCwd)) return [];
|
||||||
const key = canonicalizeWorkspace(workspaceCwd);
|
// fast-path: under §02 single-workspace, string equality
|
||||||
|
// with boundWorkspace avoids a realpathSync syscall on
|
||||||
|
// every poll. If the literal doesn't match, canonicalize
|
||||||
|
// to handle symlink aliases; if that still doesn't match,
|
||||||
|
// this daemon doesn't own the workspace.
|
||||||
|
const key =
|
||||||
|
workspaceCwd === boundWorkspace
|
||||||
|
? boundWorkspace
|
||||||
|
: canonicalizeWorkspace(workspaceCwd);
|
||||||
|
if (key !== boundWorkspace) return [];
|
||||||
const out: BridgeSessionSummary[] = [];
|
const out: BridgeSessionSummary[] = [];
|
||||||
for (const entry of byId.values()) {
|
for (const entry of byId.values()) {
|
||||||
if (entry.workspaceCwd === key) {
|
if (entry.workspaceCwd === key) {
|
||||||
|
|
@ -2321,8 +2332,19 @@ export function canonicalizeWorkspace(p: string): string {
|
||||||
// entire bridge-side path resolution anyway, but if Stage 2
|
// entire bridge-side path resolution anyway, but if Stage 2
|
||||||
// ever lands without that change, switch to the async version.
|
// ever lands without that change, switch to the async version.
|
||||||
return realpathSync.native(resolved);
|
return realpathSync.native(resolved);
|
||||||
} catch {
|
} catch (err) {
|
||||||
return resolved;
|
// Only fall back to path.resolve for ENOENT (path doesn't exist
|
||||||
|
// yet). Other filesystem errors (EACCES, EIO, ELOOP) should
|
||||||
|
// propagate — swallowing them would hide transient I/O failures
|
||||||
|
// behind misleading workspace_mismatch rejections.
|
||||||
|
if (
|
||||||
|
err &&
|
||||||
|
typeof err === 'object' &&
|
||||||
|
(err as { code?: unknown }).code === 'ENOENT'
|
||||||
|
) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -434,6 +434,18 @@ export function createServeApp(
|
||||||
.json({ error: '`:id` must decode to an absolute workspace path' });
|
.json({ error: '`:id` must decode to an absolute workspace path' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// #3803 §02: reject cross-workspace queries so orchestrators
|
||||||
|
// don't mistake "no sessions here" for "workspace is idle".
|
||||||
|
const key = canonicalizeWorkspace(workspaceCwd);
|
||||||
|
if (key !== boundWorkspace) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: `Workspace mismatch: daemon is bound to "${boundWorkspace}"`,
|
||||||
|
code: 'workspace_mismatch',
|
||||||
|
boundWorkspace,
|
||||||
|
requestedWorkspace: key,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sessions = bridge.listWorkspaceSessions(workspaceCwd);
|
const sessions = bridge.listWorkspaceSessions(workspaceCwd);
|
||||||
res.status(200).json({ sessions });
|
res.status(200).json({ sessions });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,9 @@ export function requireWorkspaceCwd(caps: DaemonCapabilities): string {
|
||||||
if (typeof caps.workspaceCwd !== 'string' || caps.workspaceCwd.length === 0) {
|
if (typeof caps.workspaceCwd !== 'string' || caps.workspaceCwd.length === 0) {
|
||||||
throw new DaemonCapabilityMissingError(
|
throw new DaemonCapabilityMissingError(
|
||||||
'workspaceCwd',
|
'workspaceCwd',
|
||||||
'introduced in #3803 §02 (1 daemon = 1 workspace)',
|
caps.workspaceCwd === ''
|
||||||
|
? 'daemon returned an empty workspaceCwd (post-§02 daemon with a bug)'
|
||||||
|
: 'daemon predates #3803 §02 (1 daemon = 1 workspace); upgrade it',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return caps.workspaceCwd;
|
return caps.workspaceCwd;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue