{"record":{"id":"70de792d5b85cbfc","repo":"can1357/oh-my-pi","slug":"vibe-tombstone-recovery-requires-parent-session-pe","errorCode":null,"errorMessage":"Vibe tombstone recovery requires parent-session persistence.","messagePattern":"Vibe tombstone recovery requires parent-session persistence\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/vibe/runtime.ts","lineNumber":1201,"sourceCode":"\t\trecord: VibeRecord,\n\t\tmanager: AsyncJobManager | undefined,\n\t\tsession: VibeParentSession,\n\t\treason: VibeTombstoneReason,\n\t\tpersistTerminal = true,\n\t\tteardownDeadline?: number,\n\t): Promise<VibeKillOutcome> {\n\t\tconst registered = this.#registeredAgent(record);\n\t\tconst settlingJobs = new Set<AsyncJob>();\n\t\tif (record.turn && manager) {\n\t\t\tconst job = manager.getJob(record.turn.jobId);\n\t\t\tif (job) settlingJobs.add(job);\n\t\t}\n\t\tlet persistenceError: unknown;\n\t\tif (persistTerminal && !record.terminalPersisted) {\n\t\t\ttry {\n\t\t\t\tif (record.killed) {\n\t\t\t\t\tconst recover = session.sessionManager?.recoverPersistenceFromCurrentState;\n\t\t\t\t\tif (!recover) throw new ToolError(\"Vibe tombstone recovery requires parent-session persistence.\");\n\t\t\t\t\tawait recover.call(session.sessionManager);\n\t\t\t\t}\n\t\t\t\tif (!this.#hasInMemoryTombstone(session, record) && record.childSessionFile) {\n\t\t\t\t\tif (!(await this.#appendTombstone(session, record, reason))) {\n\t\t\t\t\t\tthrow new ToolError(`Vibe session \"${record.id}\" changed parent scope before termination.`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trecord.terminalPersisted = true;\n\t\t\t} catch (error) {\n\t\t\t\tpersistenceError = error;\n\t\t\t}\n\t\t}\n\t\trecord.killed = true;\n\t\trecord.queue.length = 0;\n\t\tlet cancelledTurn = false;\n\t\tif (record.turn && manager) {\n\t\t\tconst job = manager.getJob(record.turn.jobId);\n\t\t\tif (job) settlingJobs.add(job);","sourceCodeStart":1183,"sourceCodeEnd":1219,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/vibe/runtime.ts#L1183-L1219","documentation":"During VibeSessionRegistry.#killRecord, when a record is already killed (record.killed) and its terminal tombstone still needs persisting, the registry calls session.sessionManager?.recoverPersistenceFromCurrentState to re-materialize persistence state. If the parent session has no sessionManager, or its SessionManager does not expose recoverPersistenceFromCurrentState (optional capability), the registry throws this ToolError because it cannot safely reconcile the tombstone. The error is captured into persistenceError, retried once via the recovery path, and rethrown if unrecoverable.","triggerScenarios":"Calling kill (or mode-exit teardown) on a Vibe record whose record.killed is already true and record.terminalPersisted is false, while session.sessionManager is undefined (headless/embedded parent without persistence) or is a SessionManager version/facade lacking the optional recoverPersistenceFromCurrentState method.","commonSituations":"Embedding the agent SDK with persistence disabled (no session file) and then killing a vibe worker twice or killing after a prior persistence failure; running an older/compat SessionManager shim that predates recoverPersistenceFromCurrentState; teardown races where the first kill attempt threw before marking terminalPersisted.","solutions":["Ensure the parent ToolSession is created with a real persisted SessionManager (a session file) before spawning vibe workers, so kill paths can persist tombstones.","Upgrade/replace the SessionManager implementation so it provides recoverPersistenceFromCurrentState (it is an optional method the registry feature-detects).","Avoid killing the same record twice without letting the first kill complete; check the kill outcome and retry mode-exit so #persistModeExit can reconcile instead of the killed-record path.","Catch the ToolError from kill and re-run the teardown after re-initializing the parent session manager."],"exampleFix":"// before: parent session created without persistence\nconst session = await createSession({ persist: false });\nawait vibe.kill(id); // ToolError: Vibe tombstone recovery requires parent-session persistence.\n// after: create the session with a session file / real SessionManager\nconst session = await createSession({ sessionFile: \"/path/to/session.jsonl\" });\nawait vibe.kill(id);","handlingStrategy":"try-catch","validationCode":"const sm = session.sessionManager;\nif (!sm || typeof sm.recoverPersistenceFromCurrentState !== \"function\") {\n  throw new Error(\"parent session persistence unavailable; cannot kill vibe workers safely\");\n}","typeGuard":"function hasTombstoneRecovery(sm: unknown): sm is { recoverPersistenceFromCurrentState: () => Promise<void> } {\n  return typeof sm === \"object\" && sm !== null &&\n    \"recoverPersistenceFromCurrentState\" in sm &&\n    typeof (sm as { recoverPersistenceFromCurrentState?: unknown }).recoverPersistenceFromCurrentState === \"function\";\n}","tryCatchPattern":"try {\n  await vibe.kill(workerId);\n} catch (error) {\n  if (error instanceof ToolError && error.message.includes(\"tombstone recovery requires parent-session persistence\")) {\n    logger.warn(\"vibe kill skipped durable tombstone; worker left dead in memory\", { workerId });\n  } else throw error;\n}","preventionTips":["Always create the parent session with a persisted session file when using vibe workers.","Feature-check recoverPersistenceFromCurrentState before enabling vibe kill paths in embedded/SDK usage.","Avoid double-kill; check the kill outcome and let mode-exit reconcile unpersisted tombstones.","Keep SessionManager implementations up to date with the vibe lifecycle API surface."],"tags":["vibe","persistence","session-manager","tombstone"],"backgroundTag":"missing-parent-session-persistence","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}