feat(hooks/atlas): use getWorkForSession in boulder lookups and session tracking
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
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 { log } from "../../shared/logger"
|
||||||
import { HOOK_NAME } from "./hook-name"
|
import { HOOK_NAME } from "./hook-name"
|
||||||
import { extractSessionIdFromOutput, validateSubagentSessionId } from "./subagent-session-id"
|
import { extractSessionIdFromOutput, validateSubagentSessionId } from "./subagent-session-id"
|
||||||
@@ -19,8 +28,9 @@ export async function syncBackgroundLaunchSessionTracking(input: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trackedWork = getWorkForSession(ctx.directory, toolInput.sessionID)
|
||||||
const extractedSessionId = metadataSessionId ?? extractSessionIdFromOutput(toolOutput.output)
|
const extractedSessionId = metadataSessionId ?? extractSessionIdFromOutput(toolOutput.output)
|
||||||
const lineageSessionIDs = boulderState.session_ids
|
const lineageSessionIDs = trackedWork?.session_ids ?? boulderState.session_ids
|
||||||
const subagentSessionId = await validateSubagentSessionId({
|
const subagentSessionId = await validateSubagentSessionId({
|
||||||
client: ctx.client,
|
client: ctx.client,
|
||||||
sessionID: extractedSessionId,
|
sessionID: extractedSessionId,
|
||||||
@@ -36,22 +46,39 @@ export async function syncBackgroundLaunchSessionTracking(input: {
|
|||||||
return
|
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(
|
const { currentTask, shouldSkipTaskSessionUpdate } = resolveTaskContext(
|
||||||
pendingTaskRef,
|
pendingTaskRef,
|
||||||
resolveBoulderPlanPath(ctx.directory, boulderState),
|
trackedWork
|
||||||
|
? resolveBoulderPlanPathForWork(ctx.directory, trackedWork)
|
||||||
|
: resolveBoulderPlanPath(ctx.directory, boulderState),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (currentTask && !shouldSkipTaskSessionUpdate) {
|
if (currentTask && !shouldSkipTaskSessionUpdate) {
|
||||||
upsertTaskSessionState(ctx.directory, {
|
if (trackedWork) {
|
||||||
taskKey: currentTask.key,
|
upsertTaskSessionStateForWork(ctx.directory, trackedWork.work_id, {
|
||||||
taskLabel: currentTask.label,
|
taskKey: currentTask.key,
|
||||||
taskTitle: currentTask.title,
|
taskLabel: currentTask.label,
|
||||||
sessionId: trackedSessionId,
|
taskTitle: currentTask.title,
|
||||||
agent: typeof toolOutput.metadata?.agent === "string" ? toolOutput.metadata.agent : undefined,
|
sessionId: trackedSessionId,
|
||||||
category: typeof toolOutput.metadata?.category === "string" ? toolOutput.metadata.category : undefined,
|
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`, {
|
log(`[${HOOK_NAME}] Background launch session tracked`, {
|
||||||
|
|||||||
@@ -131,4 +131,77 @@ describe("resolveActiveBoulderSession", () => {
|
|||||||
rmSync(worktreeDirectory, { recursive: true, force: true })
|
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 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"
|
import type { BoulderState, PlanProgress } from "../../features/boulder-state"
|
||||||
|
|
||||||
export async function resolveActiveBoulderSession(input: {
|
export async function resolveActiveBoulderSession(input: {
|
||||||
@@ -16,14 +22,37 @@ export async function resolveActiveBoulderSession(input: {
|
|||||||
return null
|
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
|
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) {
|
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(sessionID)
|
||||||
expect(readBoulderState(testDirectory)?.session_ids).not.toContain(childSessionID)
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user