feat(hooks/atlas): use getWorkForSession in boulder lookups and session tracking

This commit is contained in:
YeonGyu-Kim
2026-05-11 13:37:19 +09:00
parent 42db7078af
commit 18af3d3617
4 changed files with 234 additions and 17 deletions
@@ -1,5 +1,14 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { appendSessionId, type BoulderState, resolveBoulderPlanPath, upsertTaskSessionState } from "../../features/boulder-state"
import {
appendSessionId,
appendSessionIdForWork,
getWorkForSession,
type BoulderState,
resolveBoulderPlanPath,
resolveBoulderPlanPathForWork,
upsertTaskSessionState,
upsertTaskSessionStateForWork,
} from "../../features/boulder-state"
import { log } from "../../shared/logger"
import { HOOK_NAME } from "./hook-name"
import { extractSessionIdFromOutput, validateSubagentSessionId } from "./subagent-session-id"
@@ -19,8 +28,9 @@ export async function syncBackgroundLaunchSessionTracking(input: {
return
}
const trackedWork = getWorkForSession(ctx.directory, toolInput.sessionID)
const extractedSessionId = metadataSessionId ?? extractSessionIdFromOutput(toolOutput.output)
const lineageSessionIDs = boulderState.session_ids
const lineageSessionIDs = trackedWork?.session_ids ?? boulderState.session_ids
const subagentSessionId = await validateSubagentSessionId({
client: ctx.client,
sessionID: extractedSessionId,
@@ -36,22 +46,39 @@ export async function syncBackgroundLaunchSessionTracking(input: {
return
}
appendSessionId(ctx.directory, trackedSessionId, "appended")
if (trackedWork) {
appendSessionIdForWork(ctx.directory, trackedWork.work_id, trackedSessionId, "appended")
} else {
appendSessionId(ctx.directory, trackedSessionId, "appended")
}
const { currentTask, shouldSkipTaskSessionUpdate } = resolveTaskContext(
pendingTaskRef,
resolveBoulderPlanPath(ctx.directory, boulderState),
trackedWork
? resolveBoulderPlanPathForWork(ctx.directory, trackedWork)
: resolveBoulderPlanPath(ctx.directory, boulderState),
)
if (currentTask && !shouldSkipTaskSessionUpdate) {
upsertTaskSessionState(ctx.directory, {
taskKey: currentTask.key,
taskLabel: currentTask.label,
taskTitle: currentTask.title,
sessionId: trackedSessionId,
agent: typeof toolOutput.metadata?.agent === "string" ? toolOutput.metadata.agent : undefined,
category: typeof toolOutput.metadata?.category === "string" ? toolOutput.metadata.category : undefined,
})
if (trackedWork) {
upsertTaskSessionStateForWork(ctx.directory, trackedWork.work_id, {
taskKey: currentTask.key,
taskLabel: currentTask.label,
taskTitle: currentTask.title,
sessionId: trackedSessionId,
agent: typeof toolOutput.metadata?.agent === "string" ? toolOutput.metadata.agent : undefined,
category: typeof toolOutput.metadata?.category === "string" ? toolOutput.metadata.category : undefined,
})
} else {
upsertTaskSessionState(ctx.directory, {
taskKey: currentTask.key,
taskLabel: currentTask.label,
taskTitle: currentTask.title,
sessionId: trackedSessionId,
agent: typeof toolOutput.metadata?.agent === "string" ? toolOutput.metadata.agent : undefined,
category: typeof toolOutput.metadata?.category === "string" ? toolOutput.metadata.category : undefined,
})
}
}
log(`[${HOOK_NAME}] Background launch session tracked`, {
@@ -131,4 +131,77 @@ describe("resolveActiveBoulderSession", () => {
rmSync(worktreeDirectory, { recursive: true, force: true })
}
})
test("uses work resolved by session id when works map is present", async () => {
// given
const legacyPlanPath = join(testDirectory, "legacy-plan.md")
const workAPlanPath = join(testDirectory, "work-a-plan.md")
const workBPlanPath = join(testDirectory, "work-b-plan.md")
writeFileSync(legacyPlanPath, "# Plan\n- [ ] Legacy\n", "utf-8")
writeFileSync(workAPlanPath, "# Plan\n- [ ] Work A\n", "utf-8")
writeFileSync(workBPlanPath, "# Plan\n- [x] Work B\n", "utf-8")
writeBoulderState(testDirectory, {
schema_version: 2,
active_work_id: "work-a",
active_plan: legacyPlanPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: ["ses_legacy"],
plan_name: "legacy-plan",
works: {
"work-a": {
work_id: "work-a",
active_plan: workAPlanPath,
plan_name: "work-a-plan",
started_at: "2026-01-02T10:00:00Z",
session_ids: ["ses_work_a"],
status: "active",
},
"work-b": {
work_id: "work-b",
active_plan: workBPlanPath,
plan_name: "work-b-plan",
started_at: "2026-01-02T11:00:00Z",
session_ids: ["ses_work_b"],
status: "active",
},
},
})
// when
const result = await resolveActiveBoulderSession({
client: { session: { get: async () => ({ data: {} }) } } as never,
directory: testDirectory,
sessionID: "ses_work_b",
})
// then
expect(result).not.toBeNull()
expect(result?.boulderState.active_plan).toBe(workBPlanPath)
expect(result?.progress.isComplete).toBe(true)
})
test("falls back to top-level mirror when works map is missing", async () => {
// given
const legacyPlanPath = join(testDirectory, "legacy-only-plan.md")
writeFileSync(legacyPlanPath, "# Plan\n- [ ] Task 1\n", "utf-8")
writeBoulderState(testDirectory, {
active_plan: legacyPlanPath,
started_at: "2026-01-02T10:00:00Z",
session_ids: ["ses_legacy_only"],
plan_name: "legacy-only-plan",
})
// when
const result = await resolveActiveBoulderSession({
client: { session: { get: async () => ({ data: {} }) } } as never,
directory: testDirectory,
sessionID: "ses_legacy_only",
})
// then
expect(result).not.toBeNull()
expect(result?.boulderState.active_plan).toBe(legacyPlanPath)
expect(result?.progress.isComplete).toBe(false)
})
})
@@ -1,5 +1,11 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { getPlanProgress, readBoulderState, resolveBoulderPlanPath } from "../../features/boulder-state"
import {
getPlanProgress,
getWorkForSession,
readBoulderState,
resolveBoulderPlanPath,
resolveBoulderPlanPathForWork,
} from "../../features/boulder-state"
import type { BoulderState, PlanProgress } from "../../features/boulder-state"
export async function resolveActiveBoulderSession(input: {
@@ -16,14 +22,37 @@ export async function resolveActiveBoulderSession(input: {
return null
}
if (!boulderState.session_ids.includes(input.sessionID)) {
const sessionWork = getWorkForSession(input.directory, input.sessionID)
if (!sessionWork && !boulderState.session_ids.includes(input.sessionID)) {
return null
}
const progress = getPlanProgress(resolveBoulderPlanPath(input.directory, boulderState))
const nextBoulderState: BoulderState = sessionWork
? {
...boulderState,
active_plan: sessionWork.active_plan,
plan_name: sessionWork.plan_name,
status: sessionWork.status,
started_at: sessionWork.started_at,
ended_at: sessionWork.ended_at,
elapsed_ms: sessionWork.elapsed_ms,
updated_at: sessionWork.updated_at,
session_ids: [...sessionWork.session_ids],
session_origins: sessionWork.session_origins ? { ...sessionWork.session_origins } : {},
agent: sessionWork.agent,
worktree_path: sessionWork.worktree_path,
task_sessions: sessionWork.task_sessions ? { ...sessionWork.task_sessions } : {},
}
: boulderState
const progress = getPlanProgress(
sessionWork
? resolveBoulderPlanPathForWork(input.directory, sessionWork)
: resolveBoulderPlanPath(input.directory, nextBoulderState),
)
if (progress.isComplete) {
return { boulderState, progress, appendedSession: false }
return { boulderState: nextBoulderState, progress, appendedSession: false }
}
return { boulderState, progress, appendedSession: false }
return { boulderState: nextBoulderState, progress, appendedSession: false }
}
@@ -424,6 +424,94 @@ describe("createToolExecuteAfterHandler background launch detection", () => {
expect(readBoulderState(testDirectory)?.session_ids).not.toContain(sessionID)
expect(readBoulderState(testDirectory)?.session_ids).not.toContain(childSessionID)
})
it("#then it should append launched child to the session-resolved work", async () => {
const parentSessionID = "ses_parent_for_work"
const childSessionID = "ses_child_for_work"
const planPathA = join(testDirectory, "background-launch-work-a.md")
const planPathB = join(testDirectory, "background-launch-work-b.md")
const project = createProject()
const client = {
session: {
get: async () => createSessionGetResult(undefined),
},
} as unknown as PluginInput["client"]
spyOn(client.session, "get").mockImplementation((input) => Promise.resolve(
createSessionGetResult(input?.path?.id === childSessionID ? parentSessionID : undefined),
) as never)
writeFileSync(planPathA, "# Plan\n\n## TODOs\n- [ ] 1. Work A\n")
writeFileSync(planPathB, "# Plan\n\n## TODOs\n- [ ] 1. Work B\n")
writeBoulderState(testDirectory, {
schema_version: 2,
active_work_id: "work-a",
active_plan: planPathA,
started_at: "2026-01-02T10:00:00Z",
session_ids: ["ses_unrelated_active"],
plan_name: "background-launch-work-a",
works: {
"work-a": {
work_id: "work-a",
active_plan: planPathA,
plan_name: "background-launch-work-a",
started_at: "2026-01-02T10:00:00Z",
session_ids: ["ses_unrelated_active"],
status: "active",
},
"work-b": {
work_id: "work-b",
active_plan: planPathB,
plan_name: "background-launch-work-b",
started_at: "2026-01-02T10:05:00Z",
session_ids: [parentSessionID],
status: "active",
},
},
})
const pendingFilePaths = new Map<string, string>()
const pendingTaskRefs = new Map()
const ctx = {
client,
project,
directory: testDirectory,
worktree: testDirectory,
serverUrl: new URL("https://example.com"),
$: Bun.$,
} satisfies PluginInput
const beforeHandler = createToolExecuteBeforeHandler({ ctx, pendingFilePaths, pendingTaskRefs })
const afterHandler = createToolExecuteAfterHandler({
ctx,
pendingFilePaths,
pendingTaskRefs,
autoCommit: true,
getState: () => ({ promptFailureCount: 0 }),
})
await beforeHandler(
{ tool: "task", sessionID: parentSessionID, callID: "call-bg-work" },
{ args: { prompt: "Work B" } },
)
await afterHandler(
{ tool: "task", sessionID: parentSessionID, callID: "call-bg-work" },
{
title: "Sisyphus Task",
output: "Background task launched.\n\nBackground Task ID: bg_work\n\n<task_metadata>\nsession_id: ses_child_for_work\n</task_metadata>",
metadata: {
sessionId: childSessionID,
agent: "sisyphus-junior",
category: "deep",
},
},
)
const boulderState = readBoulderState(testDirectory)
expect(boulderState?.works?.["work-b"]?.session_ids).toContain(childSessionID)
expect(boulderState?.works?.["work-a"]?.session_ids).not.toContain(childSessionID)
})
})
})
})