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": { "models": {
"currentValue": "default", "currentValue": "default",
"values": [ "values": [
{ "value": "default", "name": "Default (recommended)" }, { "value": "default", "name": "Default" },
{ "value": "opus", "name": "Opus" },
{ "value": "sonnet", "name": "Sonnet" }, { "value": "sonnet", "name": "Sonnet" },
{ "value": "opus", "name": "Opus" },
{ "value": "haiku", "name": "Haiku" } { "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": [] } "thoughtLevels": { "values": [] }
} }
] ]

View file

@ -679,6 +679,7 @@ export class SandboxAgent {
live.bindSession(record.id, record.agentSessionId); live.bindSession(record.id, record.agentSessionId);
let session = this.upsertSessionHandle(record); let session = this.upsertSessionHandle(record);
try {
if (request.mode) { if (request.mode) {
session = (await this.setSessionMode(session.id, request.mode)).session; session = (await this.setSessionMode(session.id, request.mode)).session;
} }
@ -688,6 +689,14 @@ export class SandboxAgent {
if (request.thoughtLevel) { if (request.thoughtLevel) {
session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session; 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; return session;
} }
@ -728,6 +737,7 @@ export class SandboxAgent {
const existing = await this.persist.getSession(request.id); const existing = await this.persist.getSession(request.id);
if (existing) { if (existing) {
let session = await this.resumeSession(existing.id); let session = await this.resumeSession(existing.id);
try {
if (request.mode) { if (request.mode) {
session = (await this.setSessionMode(session.id, request.mode)).session; session = (await this.setSessionMode(session.id, request.mode)).session;
} }
@ -737,13 +747,25 @@ export class SandboxAgent {
if (request.thoughtLevel) { if (request.thoughtLevel) {
session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session; 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; return session;
} }
return this.createSession(request); return this.createSession(request);
} }
async destroySession(id: string): Promise<Session> { async destroySession(id: string): Promise<Session> {
try {
await this.sendSessionMethodInternal(id, SESSION_CANCEL_METHOD, {}, {}, true); await this.sendSessionMethodInternal(id, SESSION_CANCEL_METHOD, {}, {}, true);
} catch {
// Best-effort: agent may already be gone
}
const existing = await this.requireSessionRecord(id); const existing = await this.requireSessionRecord(id);
const updated: SessionRecord = { const updated: SessionRecord = {