fix: harden session lifecycle and align cli.mdx example with claude.json

- destroySession: wrap session/cancel RPC in try/catch so local cleanup
  always succeeds even when the agent is unreachable
- createSession/resumeOrCreateSession: clean up the remote session if
  post-creation config calls (setMode/setModel/setThoughtLevel) fail,
  preventing leaked orphan sessions
- cli.mdx: fix example output to match current claude.json (model name,
  model order, and populated modes)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nathan Flurry 2026-03-05 17:53:58 -08:00
parent 5c7a0ac761
commit bdbd1d46a9
2 changed files with 51 additions and 20 deletions

View file

@ -200,13 +200,22 @@ Example output:
"models": {
"currentValue": "default",
"values": [
{ "value": "default", "name": "Default (recommended)" },
{ "value": "opus", "name": "Opus" },
{ "value": "default", "name": "Default" },
{ "value": "sonnet", "name": "Sonnet" },
{ "value": "opus", "name": "Opus" },
{ "value": "haiku", "name": "Haiku" }
]
},
"modes": { "values": [] },
"modes": {
"currentValue": "default",
"values": [
{ "value": "default", "name": "Default" },
{ "value": "acceptEdits", "name": "Accept Edits" },
{ "value": "plan", "name": "Plan" },
{ "value": "dontAsk", "name": "Don't Ask" },
{ "value": "bypassPermissions", "name": "Bypass Permissions" }
]
},
"thoughtLevels": { "values": [] }
}
]

View file

@ -679,14 +679,23 @@ export class SandboxAgent {
live.bindSession(record.id, record.agentSessionId);
let session = this.upsertSessionHandle(record);
if (request.mode) {
session = (await this.setSessionMode(session.id, request.mode)).session;
}
if (request.model) {
session = (await this.setSessionModel(session.id, request.model)).session;
}
if (request.thoughtLevel) {
session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session;
try {
if (request.mode) {
session = (await this.setSessionMode(session.id, request.mode)).session;
}
if (request.model) {
session = (await this.setSessionModel(session.id, request.model)).session;
}
if (request.thoughtLevel) {
session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session;
}
} catch (err) {
try {
await this.destroySession(session.id);
} catch {
// Best-effort cleanup
}
throw err;
}
return session;
@ -728,14 +737,23 @@ export class SandboxAgent {
const existing = await this.persist.getSession(request.id);
if (existing) {
let session = await this.resumeSession(existing.id);
if (request.mode) {
session = (await this.setSessionMode(session.id, request.mode)).session;
}
if (request.model) {
session = (await this.setSessionModel(session.id, request.model)).session;
}
if (request.thoughtLevel) {
session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session;
try {
if (request.mode) {
session = (await this.setSessionMode(session.id, request.mode)).session;
}
if (request.model) {
session = (await this.setSessionModel(session.id, request.model)).session;
}
if (request.thoughtLevel) {
session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session;
}
} catch (err) {
try {
await this.destroySession(session.id);
} catch {
// Best-effort cleanup
}
throw err;
}
return session;
}
@ -743,7 +761,11 @@ export class SandboxAgent {
}
async destroySession(id: string): Promise<Session> {
await this.sendSessionMethodInternal(id, SESSION_CANCEL_METHOD, {}, {}, true);
try {
await this.sendSessionMethodInternal(id, SESSION_CANCEL_METHOD, {}, {}, true);
} catch {
// Best-effort: agent may already be gone
}
const existing = await this.requireSessionRecord(id);
const updated: SessionRecord = {