{"record":{"id":"8492d9a05d47da91","repo":"can1357/oh-my-pi","slug":"vibe-session-record-id-changed-parent-scope-b-8492d9","errorCode":null,"errorMessage":"Vibe session \"${record.id}\" changed parent scope before its turn started.","messagePattern":"Vibe session \"(.+?)\" changed parent scope before its turn started\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/vibe/runtime.ts","lineNumber":1424,"sourceCode":"\t\tconst jobId = manager.register(\n\t\t\t\"task\",\n\t\t\t`vibe ${record.cli} ${record.id}: ${firstLine(message, 60)}`,\n\t\t\tasync ({ jobId: ownJobId, signal }) => {\n\t\t\t\trecord.state = \"running\";\n\t\t\t\trecord.turnCount = turnIndex;\n\t\t\t\trecord.lastActivityAt = Date.now();\n\t\t\t\ttry {\n\t\t\t\t\tconst turnStartedPersisted = await this.#appendLifecycleEvent(\n\t\t\t\t\t\tsession,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t...this.#eventBase(record),\n\t\t\t\t\t\t\taction: \"turn-started\",\n\t\t\t\t\t\t\tturn: turnIndex,\n\t\t\t\t\t\t},\n\t\t\t\t\t\trecord.parentSessionFile,\n\t\t\t\t\t);\n\t\t\t\t\tif (record.childSessionFile && !turnStartedPersisted) {\n\t\t\t\t\t\tthrow new ToolError(`Vibe session \"${record.id}\" changed parent scope before its turn started.`);\n\t\t\t\t\t}\n\t\t\t\t\tconst result = options.first\n\t\t\t\t\t\t? await runSubprocess(await this.#buildSpawnOptions(session, record, message, signal, onProgress))\n\t\t\t\t\t\t: await runSubagentFollowUpTurn({\n\t\t\t\t\t\t\t\tid: record.id,\n\t\t\t\t\t\t\t\tagent: record.agent,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tdescription: `vibe ${record.cli} session`,\n\t\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t\t\tonProgress,\n\t\t\t\t\t\t\t\teventBus: session.eventBus,\n\t\t\t\t\t\t\t\tsubagentEventBus: session.subagentEventBus,\n\t\t\t\t\t\t\t\tartifactsDir: session.getSessionFile()?.slice(0, -6),\n\t\t\t\t\t\t\t});\n\t\t\t\t\treturn await this.#settleTurn(session, manager, record, turn, ownJobId, turnIndex, result);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (error instanceof VibeTurnError) throw error;\n\t\t\t\t\tawait this.#finishTurn(session, manager, record, ownJobId);","sourceCodeStart":1406,"sourceCodeEnd":1442,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/vibe/runtime.ts#L1406-L1442","documentation":"At the start of every vibe worker turn, #registerTurnJob appends a 'turn-started' lifecycle event to the parent session file via #appendLifecycleEvent, which validates the current parent scope against record.parentSessionFile. If the event cannot be persisted because the parent session was switched, branched, compacted, or its owner changed before the turn began, and the record has a childSessionFile, the turn aborts with this ToolError before spawning any work. The error is then wrapped into a VibeTurnError by the catch block.","triggerScenarios":"Sending a message/new turn to a vibe worker after the parent session scope changed: getSessionFile() resolves to a different path than record.parentSessionFile, or session id / owner agent id no longer match, so #appendLifecycleEvent returns false while record.childSessionFile is set.","commonSituations":"Queueing follow-up prompts to a background vibe worker and then switching sessions in the same process; session compaction replaced the session file between turns; the worker's owning agent changed; automation that switches sessions while vibe workers still have queued turns.","solutions":["Drain or kill vibe workers before switching/compacting the parent session; deliver queued turns in the original scope.","Re-register the vibe worker under the current session scope (new record bound to the current parentSessionFile) and send the message there.","Switch back to the original parent session file before sending the follow-up turn.","If the turn is not essential, treat the wrapped VibeTurnError as terminal for that turn and start a fresh worker."],"exampleFix":"// before: follow-up after session switch\nawait session.switchTo(newFile);\nawait vibe.send(workerId, \"continue\"); // turn fails: changed parent scope before its turn started\n// after: send while scope matches, or respawn under the new scope\nawait vibe.send(workerId, \"continue\");\nawait session.switchTo(newFile);","handlingStrategy":"validation","validationCode":"const current = session.getSessionFile();\nconst canRunTurn = !!current && record.parentSessionFile !== null &&\n  path.resolve(current) === path.resolve(record.parentSessionFile) &&\n  session.getSessionId?.() === record.parentSessionId;\nif (!canRunTurn) throw new Error(\"parent scope changed; re-register worker before sending turns\");","typeGuard":"null","tryCatchPattern":"try {\n  await vibe.send(workerId, message);\n} catch (error) {\n  if (error instanceof VibeTurnError && error.message.includes(\"changed parent scope before its turn started\")) {\n    // restore scope or respawn the worker under the current session\n  } else throw error;\n}","preventionTips":["Deliver all queued turns before switching or compacting the parent session.","After an intentional scope change, kill the old record and register a fresh worker bound to the new session file.","Gate automated follow-up sends on a scope check against record.parentSessionFile.","Keep worker lifetimes shorter than the parent session's lifetime."],"tags":["vibe","lifecycle","session-scope","turn"],"backgroundTag":"session-scope-changed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}