{"record":{"id":"65243e98079a2acc","repo":"can1357/oh-my-pi","slug":"vibe-parent-session-changed-before-spawn-failure-c","errorCode":null,"errorMessage":"Vibe parent session changed before spawn failure could be persisted.","messagePattern":"Vibe parent session changed before spawn failure could be persisted\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/vibe/runtime.ts","lineNumber":922,"sourceCode":"\t\t\t\t\t\tchildSessionFile: childSessionName,\n\t\t\t\t\t\tcreatedAt,\n\t\t\t\t\t},\n\t\t\t\t\trecord.parentSessionFile,\n\t\t\t\t);\n\t\t\t\tif (!spawnPersisted) throw new ToolError(\"Vibe parent session changed before the worker could start.\");\n\t\t\t}\n\t\t\tconst jobId = this.#registerTurnJob(session, manager, record, args.prompt, { first: true });\n\t\t\treturn { id, jobId };\n\t\t} catch (error) {\n\t\t\trecord.killed = true;\n\t\t\trecord.state = \"dead\";\n\t\t\trecord.lastActivityAt = Date.now();\n\t\t\trecord.lastActivity = \"spawn failed\";\n\t\t\tif (childSessionFile) {\n\t\t\t\t// A rejected terminal write leaves this dead record in the map so mode exit can retry it.\n\t\t\t\trecord.terminalPersisted = await this.#appendTombstone(session, record, \"spawn-failed\");\n\t\t\t\tif (!record.terminalPersisted) {\n\t\t\t\t\tthrow new ToolError(\"Vibe parent session changed before spawn failure could be persisted.\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.#records.delete(key);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/**\n\t * Send a message to a worker. Mid-turn and streaming → steering; mid-turn\n\t * otherwise → queued for the next turn; idle/parked → starts a new\n\t * background turn immediately.\n\t */\n\tasync send(session: ToolSession, args: { session: string; message: string }): Promise<VibeSendOutcome> {\n\t\tconst scope = this.ownerScope(session);\n\t\tconst record = this.#record(scope, args.session);\n\t\tif (record.state === \"dead\") {\n\t\t\tthrow new ToolError(`Vibe session \"${record.id}\" is dead. Spawn a new one with vibe_spawn.`);\n\t\t}","sourceCodeStart":904,"sourceCodeEnd":940,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/vibe/runtime.ts#L904-L940","documentation":"During a failed vibe worker spawn, the registry tries to append a 'spawn-failed' tombstone lifecycle event to the parent session file. This error is thrown when that tombstone write also fails to persist (the append was rejected, e.g. because the parent session file changed underneath the registry). The dead record is intentionally left in the map so that Vibe mode exit can retry persisting the tombstone later.","triggerScenarios":"Calling VibeSessionRegistry.spawn() (vibe_spawn tool) when (1) the initial spawn lifecycle event fails to persist, throwing inside the try block, AND (2) the follow-up #appendTombstone(..., 'spawn-failed') also returns false because the parent session file was modified/replaced concurrently by another writer.","commonSituations":"Concurrent sessions sharing the same parent session file; a crash-recovery or session-rewrite process touching the .jsonl while a worker is spawning; running Vibe mode where the parent session file was rotated, moved, or deleted mid-spawn; race between two parallel vibe_spawn calls in the same owner scope.","solutions":["Exit and re-enter Vibe mode so the registry re-reads the current parent session file and can retry the tombstone persistence on mode exit.","Ensure no other process or session is writing to the same parent session file (check for duplicate omp sessions pointed at the same session directory).","Retry the vibe_spawn call after the concurrent writer has finished; the original spawn already failed, so a fresh spawn is required.","If it recurs, verify disk state and permissions of the parent session .jsonl file for errors that make appends fail."],"exampleFix":"// before: spawning while another process rewrites the session file\nawait registry.spawn(session, { cli: 'codex', prompt: 'do work' });\n// throws: tombstone could not be persisted\n\n// after: serialize spawns / re-enter vibe mode first\nawait exitVibeModeAndRetry();\nconst out = await registry.spawn(session, { cli: 'codex', prompt: 'do work' });","handlingStrategy":"retry","validationCode":"// Ensure exclusive access to the parent session file before spawning\nif (!parentSessionFile) throw new Error('vibe_spawn requires a persisted parent session');\n// Avoid concurrent spawns from the same owner scope\nawait spawnMutex.withLock(() => registry.spawn(session, args));","typeGuard":null,"tryCatchPattern":"try {\n\tawait registry.spawn(session, args);\n} catch (err) {\n\tif (err instanceof ToolError && err.message.includes('spawn failure could be persisted')) {\n\t\t// exit and re-enter vibe mode, then retry spawn\n\t\tawait reenterVibeMode(session);\n\t\tawait registry.spawn(session, args);\n\t}\n\tthrow err;\n}","preventionTips":["Never run two omp sessions against the same parent session file.","Serialize vibe_spawn calls per owner scope.","Re-enter Vibe mode after any error mentioning session-file persistence.","Monitor session .jsonl files for external writers/rotations."],"tags":["vibe","session-persistence","race-condition","spawn"],"backgroundTag":"session-file-write-conflict","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}