Skip to content

feat: Add live scan session details - #403

Open
ianw-oai wants to merge 1 commit into
mainfrom
codex/add-interactive-scan-details
Open

feat: Add live scan session details #403
ianw-oai wants to merge 1 commit into
mainfrom
codex/add-interactive-scan-details

Conversation

@ianw-oai

Copy link
Copy Markdown
Collaborator

Why

The scan dashboard only showed selected activity. Users could not inspect the prompts, reasoning, tool calls, outputs, and worker events already saved in Codex session logs while a scan was running.

What changed

  • Stream existing parent and worker session events through the new onSessionEvent SDK callback. Exclude unrelated scans and inherited parent history, and keep observer failures from stopping a scan.
  • Add a live Details view. Press d to toggle it, then use a, m, or 1-9 to show all sessions, the main session, or one worker.
  • Keep events in time order, remove duplicate reasoning and messages, preserve script output and Markdown, color event types, and cache rows for responsive scrolling.
  • Keep Details and saved session logs unredacted. They may contain source code or credentials. The normal activity feed remains redacted, terminal escapes are removed, and encrypted reasoning is not displayed.

Verification

  • Full randomized suite: 1,066 passed, 11 skipped, zero failures.
  • Focused dashboard and cost suites after the final Details rename: 52 passed.
  • pnpm run types
  • pnpm run format
  • pnpm run build

@ianw-oai
ianw-oai marked this pull request as ready for review August 14, 2026 04:31
@ianw-oai ianw-oai changed the title [codex] Add live scan session details feat: Add live scan session details Aug 14, 2026
@github-actions github-actions Bot added the enhancement New feature or request label Aug 14, 2026

@ting-hong-shieh ting-hong-shieh left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one source-attribution bug in the Details filter.

Validation snapshot:

  • Exact PR head: b5a30d0ab865167b23dee0f375417ec2f2b2e034
  • Latest main: 216212b70a421b0bf30319d241de47a64050ec0f; the no-commit merge is clean.
  • Existing focused suites pass: 52/52 tests on the exact head and 59/59 on the latest-main merge. The focused API callback test, TypeScript checking, formatting, build, and whitespace checks also pass on both trees.
  • validation/codex_security_403_independent_worker_order.test.ts sends a parentless independent Deep worker event before the main scan event, then selects m. On both unmodified trees, the expected main line is absent and the frame instead contains main · assistant: Independent worker.
  • A local minimal check that propagates the tracker's known main thread ID makes the reproduction pass while preserving the 59-test focused suite, API test, TypeScript, formatting, and whitespace results.

The reproduction is in-process. It made no model or provider request, used no credentials, and did not scan an external target.

this.#view === "details" && this.#scrollOffset !== 0
? this.#activityLines(this.#width()).length
: 0;
this.#mainThreadId ??= session.parentThreadId ?? session.threadId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not infer the main scan from the first event

Independent Deep workers selected from the scan directory can have parentThreadId: null, and sessionFiles() uses filesystem enumeration order. If one of those workers is emitted before the main session, this assignment permanently records the worker as #mainThreadId; selecting m then shows the worker while the real main scan is labeled worker 1.

At exact head b5a30d0a, and again after a clean merge onto main 216212b7, the same two-event fixture expected main · assistant: Main scan. but rendered main · assistant: Independent worker. Propagating the tracker’s known scan thread ID makes the case pass without changing the existing focused results. Please identify the main session explicitly and add an independent-worker-first regression.

@mldangelo-oai mldangelo-oai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice addition. I tested the current head with the full randomized suite (1,066 passed, 11 skipped), focused dashboard and cost tests, the API callback test, type checks, formatting, and a production build.

I also tried a smaller implementation that uses the cost tracker's existing worker numbers. It removes two dashboard state fields and seven production lines, fixes the independent-worker ordering bug, and keeps the full suite green. My inline notes cover that simplification, startup event loss, avoidable historical-session memory use, inaccessible Details subscriptions, event-ordering overhead, and one regression-test gap.

Leaving this as a comment-only review. Thanks for putting this together.

readonly #startedAt: number;
readonly #activities: TimedScanActivity[] = [];
readonly #details: (ScanSessionEvent & { recordedAt: number })[] = [];
readonly #workers = new Map<string, number>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the worker number ScanCostTracker already assigns instead of keeping a second #workers map and inferring #mainThreadId here? I tried an optional worker?: number on ScanSessionEvent and passed through the tracker's existing number. That removed both dashboard fields and seven production lines, fixed independent workers arriving before the main scan, and still passed the full randomized test suite. It also keeps worker labels consistent between Activity and Details.

const index = this.#details.findLastIndex(
(event) => event.recordedAt <= entry.recordedAt,
);
this.#details.splice(index + 1, 0, entry);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this just append events and defer sorting until the Details view is opened or otherwise needs an updated batch? findLastIndex plus splice scans and shifts the array for every older event, including when the user stays in Activity. In a synthetic benchmark, 12,000 chronological events took 3.1 ms to record; the same events in reverse timestamp order took 395.56 ms. Worker transcripts are read one file at a time, so timestamps from another session can naturally arrive out of order. Deferring the sort should also remove some insertion and cache-invalidation bookkeeping.

prose: new Set(),
reasoning: null,
reasoningCount: 0,
...(this.#options.onSessionEvent === undefined ? {} : { events: [] }),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One memory concern here: adding onSessionEvent creates an event queue for every saved session before checking whether it belongs to the current scan. With 32 unrelated saved sessions containing 1 MiB of synthetic output each, I measured a 40.13 MiB peak heap versus 8.37 MiB with the observer disabled. Could we classify sessions before retaining raw events, or read and forward only transcripts associated with this scan?

usage = addTokenUsage(usage, session.usage);
}
if (session.threadId === null || !included.has(session.threadId)) {
session.events?.splice(0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clears a session's events before a later refresh has a chance to connect it to the current scan. I reproduced missing first messages in two cases: a grandchild worker appears before its parent's log, and an independent worker appears before the main session metadata. Removing the clear makes both cases pass, but it also keeps unrelated history resident: the same synthetic fixture stayed at 40.14 MiB after refresh. Replaying a newly associated session from its saved transcript, or retaining only sessions with unresolved relationships, could preserve those early messages without collecting every old session.

Comment thread sdk/typescript/src/cli.ts
dashboard.setStage("inspecting repository files");
}
},
onSessionEvent: dashboard?.recordDetails.bind(dashboard),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we skip onSessionEvent when process.stdin.isTTY is false? The dashboard is enabled from stderr's TTY state, while the Details keyboard listener is installed only for TTY stdin. I reproduced the redirected-stdin case: the callback is still present even though the user cannot open Details. Checking stdin here would preserve the regular dashboard while avoiding unnecessary transcript parsing and retention.

onSessionEvent: (event) => events.push(event),
});
tracker.start("scan-thread");
await waitFor(() => events.length === 4);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be open to adding a refresh where a grandchild worker already has output but its parent's session file does not exist yet, then creating the parent and checking that the original output is delivered? A second case with an independent worker appearing before the main session metadata would cover the same startup gap. This fixture currently creates all related sessions before tracker.start, so neither missing-history case is exercised.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants