diff --git a/sdk/typescript/scripts/check-package.mjs b/sdk/typescript/scripts/check-package.mjs index f8f79468..72603d9f 100644 --- a/sdk/typescript/scripts/check-package.mjs +++ b/sdk/typescript/scripts/check-package.mjs @@ -187,6 +187,7 @@ const distFiles = new Set( "scan-dashboard", "scan-history-renderer", "scan-logs", + "scan-sessions", "targets", "trusted-executable", "version", diff --git a/sdk/typescript/src/cost.ts b/sdk/typescript/src/cost.ts index 3bab2937..a8cf6c72 100644 --- a/sdk/typescript/src/cost.ts +++ b/sdk/typescript/src/cost.ts @@ -1,5 +1,5 @@ import { open, readdir } from "node:fs/promises"; -import { isAbsolute, join, relative, sep } from "node:path"; +import { join } from "node:path"; import { estimateScanCost, tokenUsage, @@ -10,6 +10,11 @@ import { scanActivityFromSessionEvent, type ScanActivity, } from "./scan-activity.js"; +import { + isScanArtifactDirectory, + sessionParentThreadId, + sessionStartedAt, +} from "./scan-sessions.js"; import { scanProgressUpdatesFromEvent, type ScanProgress, @@ -210,6 +215,7 @@ export class ScanCostTracker { for (const session of this.#sessions.values()) { if ( session.threadId === null || + session.parentThreadId !== null || session.workingDirectory === null || scanStartedAt === null || session.startedAt === null || @@ -217,22 +223,11 @@ export class ScanCostTracker { ) { continue; } - const artifactsDirectory = join( - this.#options.scanDirectory, - "artifacts", - ); - const workers = join(artifactsDirectory, "deep_discovery", "workers"); - const workerDirectory = relative(workers, session.workingDirectory); - const components = workerDirectory.split(sep); if ( - relative(artifactsDirectory, session.workingDirectory) === "" || - (!isAbsolute(workerDirectory) && - components.length === 2 && - components[0] !== ".." && - relative( - join(workers, components[0]!, "output"), - session.workingDirectory, - ) === "") + isScanArtifactDirectory( + this.#options.scanDirectory, + session.workingDirectory, + ) ) { included.add(session.threadId); } @@ -470,16 +465,8 @@ function readSessionEvent( if (typeof payload["cwd"] === "string") { session.workingDirectory = payload["cwd"]; } - if (typeof payload["timestamp"] === "string") { - session.startedAt = Math.floor(Date.parse(payload["timestamp"]) / 1_000); - } - const source = payload["source"]; - const subagent = isRecord(source) ? source["subagent"] : undefined; - const spawn = isRecord(subagent) ? subagent["thread_spawn"] : undefined; - const parent = - payload["parent_thread_id"] ?? - (isRecord(spawn) ? spawn["parent_thread_id"] : undefined); - if (typeof parent === "string") session.parentThreadId = parent; + session.startedAt = sessionStartedAt(payload["timestamp"]); + session.parentThreadId = sessionParentThreadId(payload); session.events?.push(event); return; } @@ -493,7 +480,7 @@ function readSessionEvent( payload["type"] === "task_started" && typeof payload["started_at"] === "number" && session.startedAt !== null && - payload["started_at"] >= session.startedAt + payload["started_at"] >= Math.floor(session.startedAt / 1_000) ) { session.replaying = false; session.events?.push(event); diff --git a/sdk/typescript/src/scan-logs.ts b/sdk/typescript/src/scan-logs.ts index 36ca1164..97d6a7ed 100644 --- a/sdk/typescript/src/scan-logs.ts +++ b/sdk/typescript/src/scan-logs.ts @@ -1,8 +1,13 @@ import { createReadStream } from "node:fs"; -import { basename, dirname, isAbsolute, join, relative, sep } from "node:path"; +import { basename, dirname, join, relative } from "node:path"; import { createInterface } from "node:readline"; import { sessionFiles } from "./cost.js"; import { CodexSecurityError } from "./errors.js"; +import { + isScanArtifactDirectory, + sessionParentThreadId, + sessionStartedAt, +} from "./scan-sessions.js"; interface ScanLogOptions { scanId: string; @@ -30,20 +35,10 @@ export async function readScanLogs(options: ScanLogOptions) { const metadata = first["payload"]; const threadId = metadata["id"]; if (typeof threadId !== "string") break; - const source = metadata["source"]; - const subagent = isRecord(source) ? source["subagent"] : undefined; - const spawn = isRecord(subagent) ? subagent["thread_spawn"] : undefined; - const parent = - metadata["parent_thread_id"] ?? - (isRecord(spawn) ? spawn["parent_thread_id"] : undefined); - const startedAt = - typeof metadata["timestamp"] === "string" - ? Date.parse(metadata["timestamp"]) - : Number.NaN; logs.set(threadId, { threadId, - parentThreadId: typeof parent === "string" ? parent : null, - startedAt: Number.isFinite(startedAt) ? startedAt : null, + parentThreadId: sessionParentThreadId(metadata), + startedAt: sessionStartedAt(metadata["timestamp"]), workingDirectory: typeof metadata["cwd"] === "string" ? metadata["cwd"] : null, path, @@ -149,20 +144,7 @@ function belongsToScan( } for (const directoryRoot of roots) { - const artifacts = join(directoryRoot, "artifacts"); - if (relative(artifacts, session.workingDirectory) === "") return true; - const workers = join(artifacts, "deep_discovery", "workers"); - const directory = relative(workers, session.workingDirectory); - const components = directory.split(sep); - if ( - !isAbsolute(directory) && - components.length === 2 && - components[0] !== ".." && - relative( - join(workers, components[0]!, "output"), - session.workingDirectory, - ) === "" - ) { + if (isScanArtifactDirectory(directoryRoot, session.workingDirectory)) { return true; } } diff --git a/sdk/typescript/src/scan-sessions.ts b/sdk/typescript/src/scan-sessions.ts new file mode 100644 index 00000000..676f7a10 --- /dev/null +++ b/sdk/typescript/src/scan-sessions.ts @@ -0,0 +1,45 @@ +import { isAbsolute, join, relative, sep } from "node:path"; + +export function sessionStartedAt(timestamp: unknown): number | null { + const startedAt = + typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN; + return Number.isFinite(startedAt) ? startedAt : null; +} + +export function sessionParentThreadId( + metadata: Readonly>, +): string | null { + const source = metadata["source"]; + const subagent = isRecord(source) ? source["subagent"] : undefined; + const spawn = isRecord(subagent) ? subagent["thread_spawn"] : undefined; + for (const parent of [ + isRecord(spawn) ? spawn["parent_thread_id"] : undefined, + metadata["parent_thread_id"], + metadata["forked_from_id"], + ]) { + if (typeof parent === "string" && parent !== "") return parent; + } + return null; +} + +export function isScanArtifactDirectory( + scanDirectory: string, + workingDirectory: string, +): boolean { + const artifacts = join(scanDirectory, "artifacts"); + if (relative(artifacts, workingDirectory) === "") return true; + + const workers = join(artifacts, "deep_discovery", "workers"); + const directory = relative(workers, workingDirectory); + const components = directory.split(sep); + return ( + !isAbsolute(directory) && + components.length === 2 && + components[0] !== ".." && + relative(join(workers, components[0]!, "output"), workingDirectory) === "" + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/sdk/typescript/tests-ts/cost.test.ts b/sdk/typescript/tests-ts/cost.test.ts index e29731c6..50b46d27 100644 --- a/sdk/typescript/tests-ts/cost.test.ts +++ b/sdk/typescript/tests-ts/cost.test.ts @@ -8,7 +8,7 @@ import { writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { join, parse, sep } from "node:path"; import { afterEach, describe, expect, test } from "bun:test"; import { estimateScanCost, @@ -16,9 +16,23 @@ import { type ScanSessionEvent, } from "../src/cost.js"; import type { ScanActivity } from "../src/scan-activity.js"; +import { readScanLogs } from "../src/scan-logs.js"; +import { sessionParentThreadId } from "../src/scan-sessions.js"; import type { ScanProgress } from "../src/worker-progress.js"; const temporaryDirectories: string[] = []; +const parentFields = ["source", "parent_thread_id", "forked_from_id"] as const; +type SessionParentField = (typeof parentFields)[number]; + +function parentMetadata(parentThreadId: string, field: SessionParentField) { + return field === "source" + ? { + source: { + subagent: { thread_spawn: { parent_thread_id: parentThreadId } }, + }, + } + : { [field]: parentThreadId }; +} async function waitFor(check: () => boolean): Promise { for (let attempt = 0; attempt < 100; attempt += 1) { @@ -51,6 +65,7 @@ async function writeSession( parentThreadId?: string, workingDirectory?: string, timestamp?: string, + parentField: SessionParentField = "source", ): Promise { const directory = join(home, "sessions", "2026", "07", "26"); await mkdir(directory, { recursive: true }); @@ -66,13 +81,7 @@ async function writeSession( ...(timestamp === undefined ? {} : { timestamp }), ...(parentThreadId === undefined ? {} - : { - source: { - subagent: { - thread_spawn: { parent_thread_id: parentThreadId }, - }, - }, - }), + : parentMetadata(parentThreadId, parentField)), }, }), JSON.stringify({ @@ -119,6 +128,47 @@ function progressMessage( }; } +test.each([ + [ + "prefers the spawned parent over legacy parent fields", + { + source: { + subagent: { thread_spawn: { parent_thread_id: "spawn-parent" } }, + }, + parent_thread_id: "direct-parent", + forked_from_id: "fork-parent", + }, + "spawn-parent", + ], + [ + "prefers the direct parent over fork ancestry", + { parent_thread_id: "direct-parent", forked_from_id: "fork-parent" }, + "direct-parent", + ], + [ + "falls back from an empty spawned parent to the direct parent", + { + source: { subagent: { thread_spawn: { parent_thread_id: "" } } }, + parent_thread_id: "direct-parent", + }, + "direct-parent", + ], + [ + "falls back from an empty direct parent to fork ancestry", + { parent_thread_id: "", forked_from_id: "fork-parent" }, + "fork-parent", + ], + [ + "ignores a non-string direct parent when fork ancestry is present", + { parent_thread_id: null, forked_from_id: "fork-parent" }, + "fork-parent", + ], + ["recognizes independent CLI sessions", { source: "cli" }, null], + ["treats an empty parent as missing", { forked_from_id: "" }, null], +] as const)("session parent metadata %s", (_name, metadata, expected) => { + expect(sessionParentThreadId(metadata)).toBe(expected); +}); + describe("scan cost", () => { test.each([ [{ cache_write_tokens: 15 }, 15], @@ -547,7 +597,7 @@ describe("live scan cost tracking", () => { const events: ScanSessionEvent[] = []; const tracker = new ScanCostTracker({ codexHome: home, - scanDirectory: missing === "main" ? scanDirectory : undefined, + scanDirectory, model: "gpt-5.6-sol", onSessionEvent: (event) => events.push(event), }); @@ -585,340 +635,477 @@ describe("live scan cost tracking", () => { }, ); - test("counts independent Deep workers inside the scan directory only", async () => { - const home = await codexHome(); - const scanDirectory = join(home, "scans", "current"); - await writeSession( - home, - "scan-thread", - { input_tokens: 1_000, output_tokens: 10 }, - undefined, - scanDirectory, - "2026-07-26T12:00:00Z", - ); - await writeSession( - home, - "deep-worker", - { input_tokens: 250, output_tokens: 2 }, - undefined, - join( - scanDirectory, - "artifacts", + test.each([...parentFields])( + "counts independent Deep workers and %s descendants", + async (parentField) => { + const home = await codexHome(); + const scanDirectory = join(home, "scans", "current"); + const artifacts = join(scanDirectory, "artifacts"); + const workerDirectory = join( + artifacts, "deep_discovery", "workers", "worker", "output", - ), - "2026-07-26T12:01:00Z", - ); - await writeSession( - home, - "deep-reducer", - { input_tokens: 125, output_tokens: 1 }, - undefined, - join(scanDirectory, "artifacts"), - "2026-07-26T12:02:00Z", - ); - await writeSession( - home, - "deep-worker-child", - { input_tokens: 50, output_tokens: 1 }, - "deep-worker", - ); - await writeSession( - home, - "unrelated-thread", - { input_tokens: 1_000_000, output_tokens: 1_000_000 }, - undefined, - `${scanDirectory}-other`, - ); - await writeSession( - home, - "previous-scan", - { input_tokens: 1_000_000, output_tokens: 1_000_000 }, - undefined, - join(scanDirectory, "artifacts", "deep_discovery", "previous-worker"), - "2026-07-26T11:59:00Z", - ); - await writeSession( - home, - "unknown-start", - { input_tokens: 1_000_000, output_tokens: 1_000_000 }, - undefined, - join( - scanDirectory, - "artifacts", - "deep_discovery", - "workers", - "stale", - "output", - ), - ); - await writeSession( - home, - "nested-scan", - { input_tokens: 1_000_000, output_tokens: 1_000_000 }, - undefined, - join(scanDirectory, "nested", "artifacts"), - "2026-07-26T12:03:00Z", - ); - const events: ScanSessionEvent[] = []; - const tracker = new ScanCostTracker({ - codexHome: home, - model: "gpt-5.6-sol", - scanDirectory, - onSessionEvent: (event) => events.push(event), - }); - tracker.start("scan-thread"); - - expect((await tracker.stop()).usage).toMatchObject({ - input_tokens: 1_425, - output_tokens: 14, - }); - const labels = new Map( - events.map(({ threadId, worker }) => [threadId, worker]), - ); - expect(new Set(labels.keys())).toEqual( - new Set([ + ); + await writeSession( + home, "scan-thread", + { input_tokens: 1_000, output_tokens: 10 }, + undefined, + scanDirectory, + "2026-07-26T12:00:00.900Z", + ); + await writeSession( + home, "deep-worker", + { input_tokens: 250, output_tokens: 2 }, + undefined, + process.platform === "win32" + ? workerDirectory.toUpperCase() + : workerDirectory, + "2026-07-26T12:00:00.900Z", + ); + await writeSession( + home, "deep-reducer", + { input_tokens: 125, output_tokens: 1 }, + undefined, + (process.platform === "win32" ? artifacts.toUpperCase() : artifacts) + + sep, + "2026-07-26T12:02:00Z", + ); + await writeSession( + home, "deep-worker-child", - ]), - ); - expect(labels.get("scan-thread")).toBeUndefined(); - expect( - [...labels.values()].filter((worker) => worker !== undefined).sort(), - ).toEqual([1, 2, 3]); - }); + { input_tokens: 50, output_tokens: 1 }, + "deep-worker", + undefined, + undefined, + parentField, + ); + await writeSession( + home, + "unrelated-thread", + { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + undefined, + `${scanDirectory}-other`, + ); + await writeSession( + home, + "previous-scan", + { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + undefined, + join(scanDirectory, "artifacts", "deep_discovery", "previous-worker"), + "2026-07-26T11:59:00Z", + ); + await writeSession( + home, + "unknown-start", + { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + undefined, + join( + scanDirectory, + "artifacts", + "deep_discovery", + "workers", + "stale", + "output", + ), + ); + await writeSession( + home, + "nested-scan", + { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + undefined, + join(scanDirectory, "nested", "artifacts"), + "2026-07-26T12:03:00Z", + ); + const events: ScanSessionEvent[] = []; + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-sol", + scanDirectory, + onSessionEvent: (event) => events.push(event), + }); + tracker.start("scan-thread"); - test("excludes sessions beside the deep worker output directories", async () => { - const home = await codexHome(); - const scanDirectory = join(home, "scans", "current"); - await writeSession( - home, - "scan-thread", - { input_tokens: 1_000, output_tokens: 10 }, + expect((await tracker.stop()).usage).toMatchObject({ + input_tokens: 1_425, + output_tokens: 14, + }); + const labels = new Map( + events.map(({ threadId, worker }) => [threadId, worker]), + ); + expect(new Set(labels.keys())).toEqual( + new Set([ + "scan-thread", + "deep-worker", + "deep-reducer", + "deep-worker-child", + ]), + ); + expect(labels.get("scan-thread")).toBeUndefined(); + expect( + [...labels.values()].filter((worker) => worker !== undefined).sort(), + ).toEqual([1, 2, 3]); + const logs = await readScanLogs({ + scanId: "scan-example", + threadId: "scan-thread", + codexHome: home, + scanDirectory, + }); + expect(new Set(logs.sessions.map(({ threadId }) => threadId))).toEqual( + new Set(labels.keys()), + ); + }, + ); + + test.each([ + [ + "sessions beside the deep worker output directories", + (scan: string) => join(scan, "artifacts", "deep_discovery", "output"), + "2026-07-26T12:02:00Z", undefined, - scanDirectory, - "2026-07-26T12:00:00Z", - ); - await writeSession( - home, - "deep-worker", - { input_tokens: 250, output_tokens: 2 }, + "source", + ], + [ + "sessions on another Windows drive", + (scan: string) => + parse(scan).root.toLowerCase().startsWith("c:") + ? "D:\\output" + : "C:\\output", + "2026-07-26T12:02:00Z", undefined, - join( - scanDirectory, - "artifacts", - "deep_discovery", - "workers", - "worker", - "output", - ), - "2026-07-26T12:01:00Z", - ); - // A session running in a directory that sits beside workers/ under - // deep_discovery is not a worker output directory and must not be counted. - await writeSession( - home, - "bystander", - { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + "source", + ], + [ + "sessions earlier in the same second", + (scan: string) => join(scan, "artifacts"), + "2026-07-26T12:00:00.100Z", + undefined, + "source", + ], + [ + "sessions with an invalid timestamp", + (scan: string) => join(scan, "artifacts"), + "not-a-timestamp", undefined, - join(scanDirectory, "artifacts", "deep_discovery", "output"), + "source", + ], + [ + "sessions with an unrelated parent", + (scan: string) => join(scan, "artifacts"), "2026-07-26T12:02:00Z", - ); - const tracker = new ScanCostTracker({ - codexHome: home, - model: "gpt-5.6-sol", - scanDirectory, - }); - tracker.start("scan-thread"); + "unrelated-parent", + "source", + ], + [ + "sessions with an unrelated direct parent", + (scan: string) => join(scan, "artifacts"), + "2026-07-26T12:02:00Z", + "unrelated-parent", + "parent_thread_id", + ], + [ + "sessions forked from an unrelated parent", + (scan: string) => join(scan, "artifacts"), + "2026-07-26T12:02:00Z", + "unrelated-parent", + "forked_from_id", + ], + ] as const)( + "excludes %s from scan cost and logs", + async (_name, workingDirectory, timestamp, parentThreadId, parentField) => { + const home = await codexHome(); + const scanDirectory = join(home, "scans", "current"); + await writeSession( + home, + "scan-thread", + { input_tokens: 1_000, output_tokens: 10 }, + undefined, + scanDirectory, + "2026-07-26T12:00:00.900Z", + ); + await writeSession( + home, + "deep-worker", + { input_tokens: 250, output_tokens: 2 }, + undefined, + join( + scanDirectory, + "artifacts", + "deep_discovery", + "workers", + "worker", + "output", + ), + "2026-07-26T12:00:00.950Z", + ); + await writeSession( + home, + "bystander", + { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + parentThreadId, + workingDirectory(scanDirectory), + timestamp, + parentField, + ); + await writeSession( + home, + "bystander-child", + { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + "bystander", + ); + const events: ScanSessionEvent[] = []; + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-sol", + scanDirectory, + maxCostUsd: 0.01, + onSessionEvent: (event) => events.push(event), + }); + tracker.start("scan-thread"); - expect((await tracker.stop()).usage).toMatchObject({ - input_tokens: 1_250, - output_tokens: 12, - }); - }); + const snapshot = await tracker.stop(); + expect(snapshot.usage).toMatchObject({ + input_tokens: 1_250, + output_tokens: 12, + }); + expect(snapshot.cost?.estimatedUsd).toBe(0.00661); + const included = [ + ...new Set(events.map(({ threadId }) => threadId)), + ].sort(); + expect(included).toEqual(["deep-worker", "scan-thread"]); + const logs = await readScanLogs({ + scanId: "scan-example", + threadId: "scan-thread", + codexHome: home, + scanDirectory, + }); + expect(logs.sessions.map(({ threadId }) => threadId).sort()).toEqual( + included, + ); + }, + ); - test("ignores replayed parent history in forked worker sessions", async () => { - const home = await codexHome(); - const inherited = { - input_tokens: 1_000, - cached_input_tokens: 500, - cache_write_input_tokens: 100, - output_tokens: 100, - reasoning_output_tokens: 20, - }; - await writeSession(home, "scan-thread", inherited); - const worker = await writeSession(home, "worker-thread", inherited); - const command = - 'rg "password" "$CODEX_SECURITY_REPOSITORY/routes/login.ts"'; + test.each([undefined, "not-a-timestamp"])( + "does not infer independent workers when the scan timestamp is %s", + async (timestamp) => { + const home = await codexHome(); + const scanDirectory = join(home, "scan"); + await writeSession( + home, + "scan-thread", + { input_tokens: 1_000, output_tokens: 10 }, + undefined, + scanDirectory, + timestamp, + ); + await writeSession( + home, + "independent-worker", + { input_tokens: 1_000_000, output_tokens: 1_000_000 }, + undefined, + join(scanDirectory, "artifacts"), + "2026-07-26T12:01:00Z", + ); + await writeSession( + home, + "child-worker", + { input_tokens: 250, output_tokens: 2 }, + "scan-thread", + ); + const tracker = new ScanCostTracker({ + codexHome: home, + model: "gpt-5.6-sol", + scanDirectory, + }); + tracker.start("scan-thread"); + expect((await tracker.stop()).usage).toMatchObject({ + input_tokens: 1_250, + output_tokens: 12, + }); + }, + ); - await writeFile( - worker, - [ - { - type: "session_meta", - payload: { - id: "worker-thread", - timestamp: "2026-07-26T12:02:00.250Z", - source: { - subagent: { - thread_spawn: { parent_thread_id: "scan-thread" }, - }, + test.each([...parentFields])( + "ignores replayed parent history in %s worker sessions", + async (parentField) => { + const home = await codexHome(); + const inherited = { + input_tokens: 1_000, + cached_input_tokens: 500, + cache_write_input_tokens: 100, + output_tokens: 100, + reasoning_output_tokens: 20, + }; + await writeSession(home, "scan-thread", inherited); + const worker = await writeSession(home, "worker-thread", inherited); + const command = + 'rg "password" "$CODEX_SECURITY_REPOSITORY/routes/login.ts"'; + + await writeFile( + worker, + [ + { + type: "session_meta", + payload: { + id: "worker-thread", + timestamp: "2026-07-26T12:02:00.250Z", + ...parentMetadata("scan-thread", parentField), }, }, - }, - { - type: "session_meta", - payload: { - id: "scan-thread", - timestamp: "2026-07-26T12:00:00.000Z", - source: "exec", + { + type: "session_meta", + payload: { + id: "scan-thread", + timestamp: "2026-07-26T12:00:00.000Z", + source: "exec", + }, }, - }, - { - type: "event_msg", - payload: { type: "task_started", started_at: 1_785_067_200 }, - }, - { - type: "event_msg", - payload: { - type: "agent_message", - message: "Inherited parent commentary.", + { + type: "event_msg", + payload: { type: "task_started", started_at: 1_785_067_200 }, }, - }, - { - type: "response_item", - payload: { - type: "function_call", - name: "exec_command", - call_id: "inherited-search", - arguments: JSON.stringify({ cmd: command }), + { + type: "event_msg", + payload: { + type: "agent_message", + message: "Inherited parent commentary.", + }, }, - }, - { type: "response_item", payload: progressMessage(7) }, - { - type: "event_msg", - payload: { - type: "token_count", - info: { total_token_usage: inherited }, + { + type: "response_item", + payload: { + type: "function_call", + name: "exec_command", + call_id: "inherited-search", + arguments: JSON.stringify({ cmd: command }), + }, }, - }, - { - type: "event_msg", - payload: { type: "task_started", started_at: 1_785_067_320 }, - }, - { - type: "event_msg", - timestamp: "2026-07-26T12:02:01.000Z", - payload: { - type: "agent_message", - message: "Reviewing the login query.", + { type: "response_item", payload: progressMessage(7) }, + { + type: "event_msg", + payload: { + type: "token_count", + info: { total_token_usage: inherited }, + }, }, - }, - { - type: "response_item", - payload: { - type: "function_call", - name: "exec_command", - call_id: "worker-search", - arguments: JSON.stringify({ cmd: command }), + { + type: "event_msg", + payload: { type: "task_started", started_at: 1_785_067_320 }, }, - }, - { - type: "response_item", - payload: { - type: "function_call_output", - call_id: "worker-search", - output: - "Batch reviewed.\n" + - 'CODEX_SECURITY_SCAN_PROGRESS {"phase":"discovery","filesCompleted":3,"filesTotal":8}', + { + type: "event_msg", + timestamp: "2026-07-26T12:02:01.000Z", + payload: { + type: "agent_message", + message: "Reviewing the login query.", + }, }, - }, - { - type: "event_msg", - payload: { - type: "token_count", - info: { - total_token_usage: { - input_tokens: 1_300, - cached_input_tokens: 650, - cache_write_input_tokens: 150, - output_tokens: 130, - reasoning_output_tokens: 30, + { + type: "response_item", + payload: { + type: "function_call", + name: "exec_command", + call_id: "worker-search", + arguments: JSON.stringify({ cmd: command }), + }, + }, + { + type: "response_item", + payload: { + type: "function_call_output", + call_id: "worker-search", + output: + "Batch reviewed.\n" + + 'CODEX_SECURITY_SCAN_PROGRESS {"phase":"discovery","filesCompleted":3,"filesTotal":8}', + }, + }, + { + type: "event_msg", + payload: { + type: "token_count", + info: { + total_token_usage: { + input_tokens: 1_300, + cached_input_tokens: 650, + cache_write_input_tokens: 150, + output_tokens: 130, + reasoning_output_tokens: 30, + }, }, }, }, - }, - ] - .map((event) => JSON.stringify(event)) - .join("\n") + "\n", - ); - - const activities: ScanActivity[] = []; - const progress: ScanProgress[] = []; - const events: ScanSessionEvent[] = []; - const tracker = new ScanCostTracker({ - codexHome: home, - model: "gpt-5.6-terra", - repository: "/code/juice-shop", - expectedFilesTotal: 8, - onActivity: (activity) => activities.push(activity), - onProgress: (update) => progress.push(update), - onSessionEvent: (event) => events.push(event), - }); - tracker.start("scan-thread"); + ] + .map((event) => JSON.stringify(event)) + .join("\n") + "\n", + ); - expect(await tracker.stop()).toEqual({ - usage: { - input_tokens: 1_300, - cached_input_tokens: 650, - cache_write_input_tokens: 150, - output_tokens: 130, - reasoning_output_tokens: 30, - total_tokens: 1_430, - }, - cost: { + const activities: ScanActivity[] = []; + const progress: ScanProgress[] = []; + const events: ScanSessionEvent[] = []; + const tracker = new ScanCostTracker({ + codexHome: home, model: "gpt-5.6-terra", - inputTokens: 1_300, - cachedInputTokens: 650, - cacheWriteInputTokens: 150, - outputTokens: 130, - estimatedUsd: 0.003065, - }, - }); - expect(activities).toEqual([ - expect.objectContaining({ - kind: "message", - description: "Reviewing the login query.", - worker: 1, - }), - expect.objectContaining({ - id: "worker-thread:worker-search", - kind: "command", - status: "running", - worker: 1, - }), - expect.objectContaining({ - id: "worker-thread:worker-search", - kind: "command", - status: "completed", - worker: 1, - }), - ]); - expect(progress).toEqual([ - { phase: "discovery", filesCompleted: 3, filesTotal: 8 }, - ]); - const workerEvents = events.filter( - ({ threadId }) => threadId === "worker-thread", - ); - expect(workerEvents).toHaveLength(6); - expect(JSON.stringify(workerEvents)).not.toContain( - "Inherited parent commentary.", - ); - }); + repository: "/code/juice-shop", + expectedFilesTotal: 8, + onActivity: (activity) => activities.push(activity), + onProgress: (update) => progress.push(update), + onSessionEvent: (event) => events.push(event), + }); + tracker.start("scan-thread"); + + expect(await tracker.stop()).toEqual({ + usage: { + input_tokens: 1_300, + cached_input_tokens: 650, + cache_write_input_tokens: 150, + output_tokens: 130, + reasoning_output_tokens: 30, + total_tokens: 1_430, + }, + cost: { + model: "gpt-5.6-terra", + inputTokens: 1_300, + cachedInputTokens: 650, + cacheWriteInputTokens: 150, + outputTokens: 130, + estimatedUsd: 0.003065, + }, + }); + expect(activities).toEqual([ + expect.objectContaining({ + kind: "message", + description: "Reviewing the login query.", + worker: 1, + }), + expect.objectContaining({ + id: "worker-thread:worker-search", + kind: "command", + status: "running", + worker: 1, + }), + expect.objectContaining({ + id: "worker-thread:worker-search", + kind: "command", + status: "completed", + worker: 1, + }), + ]); + expect(progress).toEqual([ + { phase: "discovery", filesCompleted: 3, filesTotal: 8 }, + ]); + const workerEvents = events.filter( + ({ threadId }) => threadId === "worker-thread", + ); + expect(workerEvents).toHaveLength(6); + expect(JSON.stringify(workerEvents)).not.toContain( + "Inherited parent commentary.", + ); + }, + ); test("forwards actions from this scan's delegated workers only", async () => { const home = await codexHome(); diff --git a/sdk/typescript/tests-ts/scan-logs.test.ts b/sdk/typescript/tests-ts/scan-logs.test.ts index f29bb87a..be57c441 100644 --- a/sdk/typescript/tests-ts/scan-logs.test.ts +++ b/sdk/typescript/tests-ts/scan-logs.test.ts @@ -255,12 +255,12 @@ describe("saved scan logs", () => { [ "sibling-directory", join(artifacts, "deep_discovery", "output"), - "2026-08-11T12:03:00.000Z", + "2026-08-11T12:01:00.000Z", ], [ "nested-scan", join(scanDirectory, "nested", "artifacts"), - "2026-08-11T12:03:00.000Z", + "2026-08-11T12:01:00.000Z", ], ] as const) { await writeSession(