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 { 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")
|
const { MESSAGE_STORAGE } = await import("../../features/hook-message-injector")
|
||||||
|
|
||||||
describe("atlas hook", () => {
|
describe("atlas hook", () => {
|
||||||
@@ -410,6 +412,62 @@ describe("atlas hook", () => {
|
|||||||
cleanupMessageStorage(sessionID)
|
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 () => {
|
test("should persist preferred subagent session for the current top-level task", async () => {
|
||||||
// given - boulder state with a current top-level task, Atlas caller
|
// given - boulder state with a current top-level task, Atlas caller
|
||||||
const sessionID = "session-task-session-track-test"
|
const sessionID = "session-task-session-track-test"
|
||||||
|
|||||||
@@ -50,4 +50,19 @@ session_id: ses_real_metadata_456
|
|||||||
// then
|
// then
|
||||||
expect(result).toBe("ses_real_metadata_456")
|
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 {
|
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 taskMetadataBlocks = [...output.matchAll(/<task_metadata>([\s\S]*?)<\/task_metadata>/gi)]
|
||||||
const lastTaskMetadataMatch = taskMetadataMatches.at(-1)
|
const lastTaskMetadataBlock = taskMetadataBlocks.at(-1)?.[1]
|
||||||
if (lastTaskMetadataMatch) {
|
if (lastTaskMetadataBlock) {
|
||||||
return lastTaskMetadataMatch[1]
|
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)]
|
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 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")
|
const isBackgroundLaunch = outputStr.includes("Background task launched") || outputStr.includes("Background task continued")
|
||||||
if (isBackgroundLaunch) {
|
if (isBackgroundLaunch) {
|
||||||
return
|
return
|
||||||
@@ -80,10 +84,6 @@ export function createToolExecuteAfterHandler(input: {
|
|||||||
const gitStats = collectGitDiffStats(ctx.directory)
|
const gitStats = collectGitDiffStats(ctx.directory)
|
||||||
const fileChanges = formatFileChanges(gitStats)
|
const fileChanges = formatFileChanges(gitStats)
|
||||||
const subagentSessionId = extractSessionIdFromOutput(toolOutput.output)
|
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)
|
const boulderState = readBoulderState(ctx.directory)
|
||||||
if (boulderState) {
|
if (boulderState) {
|
||||||
|
|||||||
Reference in New Issue
Block a user