fix(atlas): tighten session reuse metadata parsing
This commit is contained in:
@@ -33,6 +33,8 @@ mock.module("../../shared/opencode-storage-detection", () => ({
|
||||
}))
|
||||
|
||||
const { createAtlasHook } = await import("./index")
|
||||
const { createToolExecuteAfterHandler } = await import("./tool-execute-after")
|
||||
const { createToolExecuteBeforeHandler } = await import("./tool-execute-before")
|
||||
const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector")
|
||||
|
||||
describe("atlas hook", () => {
|
||||
@@ -410,6 +412,62 @@ describe("atlas hook", () => {
|
||||
cleanupMessageStorage(sessionID)
|
||||
})
|
||||
|
||||
test("should clean pending task refs when a task returns background launch output", async () => {
|
||||
// given - direct handlers with shared pending maps
|
||||
const sessionID = "session-bg-launch-cleanup-test"
|
||||
setupMessageStorage(sessionID, "atlas")
|
||||
|
||||
const planPath = join(TEST_DIR, "background-cleanup-plan.md")
|
||||
writeFileSync(planPath, `# Plan
|
||||
|
||||
## TODOs
|
||||
- [ ] 1. Implement auth flow
|
||||
`)
|
||||
writeBoulderState(TEST_DIR, {
|
||||
active_plan: planPath,
|
||||
started_at: "2026-01-02T10:00:00Z",
|
||||
session_ids: ["session-1"],
|
||||
plan_name: "background-cleanup-plan",
|
||||
})
|
||||
|
||||
const pendingFilePaths = new Map<string, string>()
|
||||
const pendingTaskRefs = new Map<string, { key: string; label: string; title: string } | null>()
|
||||
const beforeHandler = createToolExecuteBeforeHandler({
|
||||
ctx: createMockPluginInput(),
|
||||
pendingFilePaths,
|
||||
pendingTaskRefs,
|
||||
})
|
||||
const afterHandler = createToolExecuteAfterHandler({
|
||||
ctx: createMockPluginInput(),
|
||||
pendingFilePaths,
|
||||
pendingTaskRefs,
|
||||
autoCommit: true,
|
||||
getState: () => ({ promptFailureCount: 0 }),
|
||||
})
|
||||
|
||||
// when - the task is captured before execution
|
||||
await beforeHandler(
|
||||
{ tool: "task", sessionID, callID: "call-bg-launch" },
|
||||
{ args: { prompt: "Implement auth flow" } }
|
||||
)
|
||||
expect(pendingTaskRefs.size).toBe(1)
|
||||
|
||||
// and the task returns a background launch result
|
||||
await afterHandler(
|
||||
{ tool: "task", sessionID, callID: "call-bg-launch" },
|
||||
{
|
||||
title: "Sisyphus Task",
|
||||
output: "Background task launched.\n\nSession ID: ses_bg_12345",
|
||||
metadata: {},
|
||||
}
|
||||
)
|
||||
|
||||
// then - the pending task ref is still cleaned up
|
||||
expect(pendingTaskRefs.size).toBe(0)
|
||||
|
||||
cleanupMessageStorage(sessionID)
|
||||
})
|
||||
|
||||
test("should persist preferred subagent session for the current top-level task", async () => {
|
||||
// given - boulder state with a current top-level task, Atlas caller
|
||||
const sessionID = "session-task-session-track-test"
|
||||
|
||||
@@ -50,4 +50,19 @@ session_id: ses_real_metadata_456
|
||||
// then
|
||||
expect(result).toBe("ses_real_metadata_456")
|
||||
})
|
||||
|
||||
test("does not let task_metadata parsing bleed into incidental body text after the closing tag", () => {
|
||||
// given
|
||||
const output = `<task_metadata>
|
||||
session_id: ses_real_metadata_456
|
||||
</task_metadata>
|
||||
|
||||
debug log: session_id: ses_wrong_body_789`
|
||||
|
||||
// when
|
||||
const result = extractSessionIdFromOutput(output)
|
||||
|
||||
// then
|
||||
expect(result).toBe("ses_real_metadata_456")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
export function extractSessionIdFromOutput(output: string): string | undefined {
|
||||
const taskMetadataMatches = [...output.matchAll(/<task_metadata>[\s\S]*?session_id:\s*(ses_[a-zA-Z0-9_]+)[\s\S]*?<\/task_metadata>/gi)]
|
||||
const lastTaskMetadataMatch = taskMetadataMatches.at(-1)
|
||||
if (lastTaskMetadataMatch) {
|
||||
return lastTaskMetadataMatch[1]
|
||||
const taskMetadataBlocks = [...output.matchAll(/<task_metadata>([\s\S]*?)<\/task_metadata>/gi)]
|
||||
const lastTaskMetadataBlock = taskMetadataBlocks.at(-1)?.[1]
|
||||
if (lastTaskMetadataBlock) {
|
||||
const taskMetadataSessionMatch = lastTaskMetadataBlock.match(/session_id:\s*(ses_[a-zA-Z0-9_]+)/i)
|
||||
if (taskMetadataSessionMatch) {
|
||||
return taskMetadataSessionMatch[1]
|
||||
}
|
||||
}
|
||||
|
||||
const explicitSessionMatches = [...output.matchAll(/Session ID:\s*(ses_[a-zA-Z0-9_]+)/g)]
|
||||
|
||||
@@ -71,6 +71,10 @@ export function createToolExecuteAfterHandler(input: {
|
||||
}
|
||||
|
||||
const outputStr = toolOutput.output && typeof toolOutput.output === "string" ? toolOutput.output : ""
|
||||
const pendingTaskRef = toolInput.callID ? pendingTaskRefs.get(toolInput.callID) : undefined
|
||||
if (toolInput.callID) {
|
||||
pendingTaskRefs.delete(toolInput.callID)
|
||||
}
|
||||
const isBackgroundLaunch = outputStr.includes("Background task launched") || outputStr.includes("Background task continued")
|
||||
if (isBackgroundLaunch) {
|
||||
return
|
||||
@@ -80,10 +84,6 @@ export function createToolExecuteAfterHandler(input: {
|
||||
const gitStats = collectGitDiffStats(ctx.directory)
|
||||
const fileChanges = formatFileChanges(gitStats)
|
||||
const subagentSessionId = extractSessionIdFromOutput(toolOutput.output)
|
||||
const pendingTaskRef = toolInput.callID ? pendingTaskRefs.get(toolInput.callID) : undefined
|
||||
if (toolInput.callID) {
|
||||
pendingTaskRefs.delete(toolInput.callID)
|
||||
}
|
||||
|
||||
const boulderState = readBoulderState(ctx.directory)
|
||||
if (boulderState) {
|
||||
|
||||
Reference in New Issue
Block a user