{"record":{"id":"4b61d9c04f6e71a2","repo":"can1357/oh-my-pi","slug":"vibe-mode-exit-requires-atomic-parent-session-pers","errorCode":null,"errorMessage":"Vibe mode exit requires atomic parent-session persistence.","messagePattern":"Vibe mode exit requires atomic parent-session persistence\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/vibe/runtime.ts","lineNumber":515,"sourceCode":"\t\t\tcurrentScope.ownerId !== scope.ownerId ||\n\t\t\tcurrentScope.parentSessionId !== scope.parentSessionId ||\n\t\t\tcurrentScope.parentSessionFile !== scope.parentSessionFile\n\t\t) {\n\t\t\tthrow new ToolError(\"Vibe parent session changed before mode exit could be persisted.\");\n\t\t}\n\t\tconst parentSessionFile = currentScope.parentSessionFile;\n\t\tconst persistedPending = pending.filter(record => record.childSessionFile !== undefined);\n\t\tfor (const record of persistedPending) {\n\t\t\tif (\n\t\t\t\t!parentSessionFile ||\n\t\t\t\tpath.resolve(parentSessionFile.slice(0, -6), `${record.id}.jsonl`) !== record.childSessionFile\n\t\t\t) {\n\t\t\t\tthrow new ToolError(`Vibe session \"${record.id}\" changed parent scope before termination.`);\n\t\t\t}\n\t\t}\n\t\tconst appendEntriesAtomically = sessionManager.appendEntriesAtomically;\n\t\tif (!appendEntriesAtomically) {\n\t\t\tthrow new ToolError(\"Vibe mode exit requires atomic parent-session persistence.\");\n\t\t}\n\t\tawait appendEntriesAtomically.call(sessionManager, () => {\n\t\t\tfor (const record of persistedPending) {\n\t\t\t\tsessionManager.appendCustomEntry(VIBE_LIFECYCLE_CUSTOM_TYPE, {\n\t\t\t\t\t...this.#eventBase(record),\n\t\t\t\t\taction: \"tombstone\",\n\t\t\t\t\treason: \"mode-exit\",\n\t\t\t\t});\n\t\t\t}\n\t\t\tsessionManager.appendModeChange?.(\"none\");\n\t\t});\n\t\tfor (const record of pending) record.terminalPersisted = true;\n\t}\n\n\t#manager(session: ToolSession): AsyncJobManager {\n\t\tconst manager = session.asyncJobManager;\n\t\tif (!manager) {\n\t\t\tthrow new ToolError(\"Vibe sessions require async execution (no background job manager is available).\");","sourceCodeStart":497,"sourceCodeEnd":533,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/vibe/runtime.ts#L497-L533","documentation":"After path checks pass, #persistModeExit requires the parent session manager to expose appendEntriesAtomically so all tombstone entries are written as one atomic batch. If sessionManager.appendEntriesAtomically is undefined, partial tombstone writes could corrupt lifecycle state, so it throws. This guards against minimal/stub session managers lacking the atomic-append capability.","triggerScenarios":"vibe exit (→ #killAllLocked → #persistModeExit) with a sessionManager implementation that has appendCustomEntry but not appendEntriesAtomically — e.g. a custom SessionManager, an older/stub implementation, or a test double missing the method.","commonSituations":"SDK embeddings using a hand-rolled session manager; older session manager versions predating atomic append; test doubles stubbing only part of the interface.","solutions":["Provide a session manager that implements appendEntriesAtomically (upgrade to a current SessionManager).","Extend the custom session manager to batch appends atomically and expose it via appendEntriesAtomically.","In tests, use the real SessionManager instead of a partial stub, or stub appendEntriesAtomically as pass-through."],"exampleFix":"// before\nconst manager = { appendCustomEntry: fn } as SessionManager; // no atomic append\nawait vibe.exit(session); // throws\n// after\nconst manager = new SessionManager(sessionFile); // implements appendEntriesAtomically\nawait vibe.exit(session);","handlingStrategy":"validation","validationCode":"const sm = session.sessionManager;\nif (sm && typeof (sm as { appendEntriesAtomically?: unknown }).appendEntriesAtomically !== \"function\") {\n  throw new Error(\"SessionManager lacks appendEntriesAtomically; upgrade or stub it before vibe exit\");\n}","typeGuard":"function hasAtomicAppend(sm: unknown): sm is { appendEntriesAtomically: (fn: () => void) => Promise<void>; appendCustomEntry: (t: string, e: unknown) => void } {\n  return !!sm && typeof (sm as { appendEntriesAtomically?: unknown }).appendEntriesAtomically === \"function\";\n}","tryCatchPattern":"try {\n  await vibe.exit(session);\n} catch (err) {\n  if (err instanceof ToolError && err.message.includes(\"atomic parent-session persistence\")) {\n    session.sessionManager = upgradeToAtomicSessionManager(session.sessionManager);\n    await vibe.exit(session);\n  } else throw err;\n}","preventionTips":["Use the real SessionManager, not partial stubs, in embeddings and tests","Keep custom session managers implementing the full persistence interface","Pin package versions so session manager and vibe runtime stay compatible"],"tags":["session","persistence","api-compatibility"],"backgroundTag":"missing-api-surface","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}