Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dist/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4115,6 +4115,8 @@ async function setupV2(context) {
if (item.status === "complete" || item.status === "unmet")
continue;
try {
if (!await ownsSession(item.sessionID))
continue;
const transcript = await context.session.context({ sessionID: item.sessionID });
if (disposed)
return;
Expand Down
8 changes: 6 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2868,15 +2868,19 @@ async function setupV2(context: PluginV2.Plugin.Context): Promise<PluginV2.Plugi

// Rebuild task-deferral state for goals that survived a plugin restart. The
// plugin context exposes no live child-session query, so this replays each
// non-closed goal session's persisted transcript through the tracker. Best
// effort: unfetchable transcripts fall back to live-event observation only.
// locally owned, non-closed goal session's persisted transcript through the
// tracker. Every directory has its own plugin instance, so loading foreign
// transcripts here would make each new instance replay every open goal in
// the shared state file. Best effort: unresolvable or unfetchable sessions
// fall back to live-event observation only.
// Continuation decisions await this recovery, so a settled lifecycle event
// cannot slip past a pending transcript load.
async function recoverTrackedTasks() {
for (const item of (await getAllGoals()).goals) {
if (disposed) return
if (item.status === "complete" || item.status === "unmet") continue
try {
if (!(await ownsSession(item.sessionID))) continue
const transcript = await context.session.context({ sessionID: item.sessionID })
if (disposed) return
taskTracker.recoverFromTranscript(item.sessionID, transcript)
Expand Down
24 changes: 24 additions & 0 deletions test/server-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,30 @@ test("V2 rebuilds task deferral from session transcripts after a plugin restart"
})
})

test("V2 only reads recovery transcripts for goals owned by the plugin location", async () => {
await createGoal("ses_local", "Recover the local task state")
await createGoal("ses_foreign", "Leave another project's transcript alone")
const location = { directory: "/work/current", workspaceID: "workspace-current" }
const mock = makeMockContext(
{},
[],
{ ses_local: [], ses_foreign: [] },
location,
{
ses_local: { location },
ses_foreign: { location: { directory: "/work/other", workspaceID: "workspace-other" } },
},
)
const cleanup = await setupPlugin(mock as never)

await waitFor(() => mock.sessionGetCalls.includes("ses_local") && mock.sessionGetCalls.includes("ses_foreign"))
await waitFor(() => mock.contextCalls.includes("ses_local"))
expect(mock.contextCalls).toEqual(["ses_local"])

mock.stream.end()
await cleanup()
})

test("V2 continuation proceeds after restart when transcripts show no blocking tasks", async () => {
await createGoal("ses_v2", "Verify continuation without recovered tasks")
const mock = makeMockContext({}, [], { ses_v2: [] })
Expand Down
Loading