mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-17 03:57:18 +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 {
|
||||
readonly sessionId: string;
|
||||
constructor(sessionId: string) {
|
||||
super(`No session with id "${sessionId}"`);
|
||||
constructor(sessionId: string, extra?: string) {
|
||||
super(`No session with id "${sessionId}"` + (extra ? `. ${extra}` : ''));
|
||||
this.name = 'SessionNotFoundError';
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
|
@ -1680,12 +1680,14 @@ export function createHttpAcpBridge(opts: BridgeOptions): HttpAcpBridge {
|
|||
// continuation running (e.g. channel.exited firing during
|
||||
// a crash spawn, or a direct bridge.killSession call from
|
||||
// outside the route handler). In those cases byId.get()
|
||||
// returned undefined; we'd otherwise return
|
||||
// `{ attached: true, sessionId: <zombie> }` and every
|
||||
// subsequent prompt/cancel call would 404. Fail loud
|
||||
// instead so the caller can retry into a fresh spawn.
|
||||
// returned undefined. Fail loud with a descriptive error
|
||||
// so the caller can distinguish "immediate agent death"
|
||||
// from a stale sessionId and retry into a fresh spawn.
|
||||
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) {
|
||||
// Same swallow as above — we picked up an in-flight
|
||||
|
|
@ -1897,7 +1899,16 @@ export function createHttpAcpBridge(opts: BridgeOptions): HttpAcpBridge {
|
|||
|
||||
listWorkspaceSessions(workspaceCwd) {
|
||||
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[] = [];
|
||||
for (const entry of byId.values()) {
|
||||
if (entry.workspaceCwd === key) {
|
||||
|
|
@ -2321,8 +2332,19 @@ export function canonicalizeWorkspace(p: string): string {
|
|||
// entire bridge-side path resolution anyway, but if Stage 2
|
||||
// ever lands without that change, switch to the async version.
|
||||
return realpathSync.native(resolved);
|
||||
} catch {
|
||||
return resolved;
|
||||
} catch (err) {
|
||||
// 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' });
|
||||
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);
|
||||
res.status(200).json({ sessions });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -91,7 +91,9 @@ export function requireWorkspaceCwd(caps: DaemonCapabilities): string {
|
|||
if (typeof caps.workspaceCwd !== 'string' || caps.workspaceCwd.length === 0) {
|
||||
throw new DaemonCapabilityMissingError(
|
||||
'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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue