fix(atlas): tighten session reuse metadata parsing

This commit is contained in:
HaD0Yun
2026-03-17 18:14:17 +09:00
parent 5c6194372e
commit 8adf6a2c47
4 changed files with 84 additions and 8 deletions

View File

@@ -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"

View File

@@ -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")
})
})

View File

@@ -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)]

View File

@@ -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) {