diff --git a/src/hooks/atlas/subagent-session-id.test.ts b/src/hooks/atlas/subagent-session-id.test.ts index 45f716f0f..b73a0c75c 100644 --- a/src/hooks/atlas/subagent-session-id.test.ts +++ b/src/hooks/atlas/subagent-session-id.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" -import { extractSessionIdFromOutput } from "./subagent-session-id" +import { extractSessionIdFromMetadata, extractSessionIdFromOutput } from "./subagent-session-id" describe("extractSessionIdFromOutput", () => { test("extracts Session ID blocks from background output", () => { @@ -77,3 +77,44 @@ debug log: session_id: ses_wrong_body_789` expect(result).toBe("ses_real_metadata_456") }) }) + +describe("extractSessionIdFromMetadata", () => { + test("extracts sessionId from tool metadata object", () => { + // given + const metadata = { sessionId: "ses_plugin_abc123" } + + // when + const result = extractSessionIdFromMetadata(metadata) + + // then + expect(result).toBe("ses_plugin_abc123") + }) + + test("returns undefined for metadata without sessionId", () => { + // given + const metadata = { title: "some task" } + + // when + const result = extractSessionIdFromMetadata(metadata) + + // then + expect(result).toBeUndefined() + }) + + test("returns undefined for non-object metadata", () => { + expect(extractSessionIdFromMetadata(null)).toBeUndefined() + expect(extractSessionIdFromMetadata(undefined)).toBeUndefined() + expect(extractSessionIdFromMetadata("string")).toBeUndefined() + }) + + test("rejects sessionId values that don't start with ses_", () => { + // given + const metadata = { sessionId: "not-a-session-id" } + + // when + const result = extractSessionIdFromMetadata(metadata) + + // then + expect(result).toBeUndefined() + }) +}) diff --git a/src/hooks/atlas/subagent-session-id.ts b/src/hooks/atlas/subagent-session-id.ts index b316e5f68..95aef2aee 100644 --- a/src/hooks/atlas/subagent-session-id.ts +++ b/src/hooks/atlas/subagent-session-id.ts @@ -3,6 +3,16 @@ import { log } from "../../shared/logger" import { isSessionInBoulderLineage } from "./boulder-session-lineage" import { HOOK_NAME } from "./hook-name" +export function extractSessionIdFromMetadata(metadata: unknown): string | undefined { + if (metadata && typeof metadata === "object" && "sessionId" in metadata) { + const value = (metadata as Record).sessionId + if (typeof value === "string" && value.startsWith("ses_")) { + return value + } + } + return undefined +} + export function extractSessionIdFromOutput(output: string): string | undefined { const taskMetadataBlocks = [...output.matchAll(/([\s\S]*?)<\/task_metadata>/gi)] const lastTaskMetadataBlock = taskMetadataBlocks.at(-1)?.[1] diff --git a/src/hooks/atlas/tool-execute-after.ts b/src/hooks/atlas/tool-execute-after.ts index 61461f520..9a463534c 100644 --- a/src/hooks/atlas/tool-execute-after.ts +++ b/src/hooks/atlas/tool-execute-after.ts @@ -14,7 +14,7 @@ import { shouldPauseForFinalWaveApproval } from "./final-wave-approval-gate" import { HOOK_NAME } from "./hook-name" import { DIRECT_WORK_REMINDER } from "./system-reminder-templates" import { isSisyphusPath } from "./sisyphus-path" -import { extractSessionIdFromOutput, validateSubagentSessionId } from "./subagent-session-id" +import { extractSessionIdFromMetadata, extractSessionIdFromOutput, validateSubagentSessionId } from "./subagent-session-id" import { buildCompletionGate, buildFinalWaveApprovalReminder, @@ -105,7 +105,9 @@ export function createToolExecuteAfterHandler(input: { return } - if (toolInput.tool !== "task") { + const metadataSessionId = extractSessionIdFromMetadata(toolOutput.metadata) + const isPluginToolWithSession = toolInput.tool !== "task" && !!metadataSessionId + if (toolInput.tool !== "task" && !isPluginToolWithSession) { return } @@ -115,6 +117,7 @@ export function createToolExecuteAfterHandler(input: { pendingTaskRefs.delete(toolInput.callID) } const isBackgroundLaunch = outputStr.includes("Background task launched") || outputStr.includes("Background task continued") + || outputStr.includes("Background delegate launched") if (isBackgroundLaunch) { return } @@ -125,7 +128,7 @@ export function createToolExecuteAfterHandler(input: { const verificationDirectory = worktreePath ? worktreePath : ctx.directory const gitStats = collectGitDiffStats(verificationDirectory) const fileChanges = formatFileChanges(gitStats) - const extractedSessionId = extractSessionIdFromOutput(toolOutput.output) + const extractedSessionId = metadataSessionId ?? extractSessionIdFromOutput(toolOutput.output) if (boulderState) { const progress = getPlanProgress(boulderState.active_plan)