{"record":{"id":"d7fb8bea1477d8e5","repo":"can1357/oh-my-pi","slug":"vibe-session-record-id-changed-parent-scope-b","errorCode":null,"errorMessage":"Vibe session \"${record.id}\" changed parent scope before termination.","messagePattern":"Vibe session \"(.+?)\" changed parent scope before termination\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/vibe/runtime.ts","lineNumber":510,"sourceCode":"\t\t\tfor (const record of pending) record.terminalPersisted = true;\n\t\t\treturn;\n\t\t}\n\t\tconst currentScope = this.ownerScope(session);\n\t\tif (\n\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}","sourceCodeStart":492,"sourceCodeEnd":528,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/vibe/runtime.ts#L492-L528","documentation":"For each pending worker record with a childSessionFile, #persistModeExit recomputes the expected child session path from the CURRENT parent session file and compares it with the path recorded at spawn time. A mismatch means the parent session file moved/changed after the worker was spawned, so tombstones would land in the wrong place; it throws naming the offending record id.","triggerScenarios":"vibe exit (→ #killAllLocked → #persistModeExit) where path.resolve(parentSessionFile minus .jsonl suffix, `${record.id}.jsonl`) differs from record.childSessionFile — typically because the parent session file changed between spawn and exit, or the record was built under a different session file.","commonSituations":"Session file relocated or renamed (resume-from-copy) while workers were alive; worker records rebuilt from persisted events under a new session file path; mixing records across forked parent sessions.","solutions":["Keep the parent session file path stable while vibe workers are alive; if the file must move, exit vibe mode first.","Purge or re-create worker records so childSessionFile matches the current parent file location before exiting.","Check for double-resolution bugs: record.childSessionFile is expected to already be absolute and derived from the same parent file."],"exampleFix":"// before\nrenameSessionFile(session, newPath); // workers spawned under old path\nawait vibe.exit(session); // throws for record id\n// after\nawait vibe.exit(session); // persist tombstones first\nrenameSessionFile(session, newPath);","handlingStrategy":"try-catch","validationCode":"const parentFile = session.getSessionFile();\nfor (const rec of activeRecords) {\n  if (rec.childSessionFile && path.resolve(path.dirname(parentFile!), `${rec.id}.jsonl`) !== rec.childSessionFile) {\n    throw new Error(`Record ${rec.id} is parented to a different session file; exit vibe mode before moving the session`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await vibe.exit(session);\n} catch (err) {\n  if (err instanceof ToolError && /changed parent scope before termination/.test(err.message)) {\n    const id = err.message.match(/\"([^\"]+)\"/)?.[1];\n    logger.warn(\"vibe worker parented to stale session file\", { id });\n  } else throw err;\n}","preventionTips":["Treat the parent session file as immutable while workers are alive","Exit vibe mode before any operation that moves or renames the session file","Don't mix worker records across forked sessions"],"tags":["session","persistence","paths"],"backgroundTag":"session-identity-mismatch","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}