Skip to content

Land the trace projection and snapshot endpoint #22

Closed

Description

@Dillpickleschmidt

Part of #1

Question

Build the trace projection decided in Project T3's activity stream onto mindwalk's Trace model. That ticket settled every design question; this one lands the code. Read its resolution comment first — it is the spec.

Shape:

  • New endpoint GET /api/mindwalk/threads/:threadId returning { trace, citymap }, alongside the existing apps/server/src/citymap/.
  • Order per mindwalk server.go:834-841: trace (paths only) → citymap with ghostPaths → backfill Target.fileId → recompute ComputeStats with the real file count.
  • Copy verbatim from .repos/mindwalk/internal/adapter/adapter.go and internal/model/stats.go: actionFor, targetsFor, extractPaths, extractCommandPaths, commandReadPaths, parsePathHits, normalizePath, repoPathExists, verifyCommand, searchCommand, readCommand, ComputeStats. Same bet as scene/ and playback/ — mistranslation renders almost right and costs days.
  • Write fresh: a per-provider adapter reshaping an activity into { tool, input, result }. Claude is near-trivial (its payload already is that shape). Codex maps commandActions[].
  • Fold at tool.completed; SQL-filter to tool.completed / tool.denied / context-compaction before parsing.
  • In-memory cache keyed on the thread's activity count.
  • Lens parameter (main / agent(launchId)) — a no-op today, consumed by Project the agent graph and per-agent traces #21.
  • Empty citymap instead of a 500 when the build fails.

Do not widen packages/shared/src/toolActivity.ts. See the resolution for why.

Verification

Port the citymap builder to the T3 server set the bar: differentially verified against the real Go binary, zero diffs. Do the same here — run mindwalk serve against a Claude session and diff its /api/sessions/:key/trace against ours for the same thread, or golden-fixture the ported string functions against the Go tests in adapter_test.go (1,068 lines of them, free).

Note the input differs — mindwalk reads Claude's JSONL, we read T3's event store — so expect divergence where T3 stores less. Characterise each difference rather than chasing zero.

Verified facts to build against (measured, not assumed): Bash is 66% of all tool calls and Grep/Glob are absent entirely; file_path is snake_case; Claude's payload is { toolName, input, result: { content, is_error } }; Codex's commandActions[] is { command, name, path, type }; the largest thread is 1,283 tool.completed rows / 15.4 MB.

Activity

  1. Dillpickleschmidt commented on Aug 1, 2026

    @Dillpickleschmidt
    OwnerAuthor

    Answer

    Landed on branch feat/trace-projection-endpoint (commit f70e59227, pushed, no PR). GET /api/mindwalk/threads/:threadId?lens= returns { trace, citymap }, built in mindwalk's order (server.go:834-841): trace with paths only → citymap with those paths as ghostPaths → backfill Target.fileId → recompute ComputeStats with the real file count. Code lives in apps/server/src/mindwalk/; the wire contract is packages/contracts/src/trace.ts, shaped byte-identically to apps/web/src/mindwalk/types.ts.

    Differentially verified against the real Go binary

    The citymap builder set the bar at zero diffs against mindwalk. That bar isn't reachable here — the inputs genuinely differ — so here is the characterisation instead. Thread c4e71440 ("Redesign plant growth system", the largest in the store, 1,287 rows) ran as Claude session 21d3c20e; mindwalk trace was run over that session's JSONL and diffed against our projection of the same thread.

    • Action classification: 1204/1204 identical. Every event the two traces share got the same verdict. Not one divergence in the ported classifier.
    • Targets: 1284/1284. Every one of our events has an exactly matching target set — same paths, same touch levels, same weak flags — somewhere in mindwalk's trace for the same tool.
    • Final per-file touch map — the thing the colour ramp actually renders — 146/150 paths identical, 0 differing, 0 extra. The 4 gaps are all files touched only by events T3 never recorded.

    Event count differs: 1,284 ours vs 1,819 mindwalk's, and ours is the more correct number. The JSONL has 1,819 tool_use blocks but only 1,348 unique ids — Claude Code replays history into the file on resume and compaction, and mindwalk emits an event per replay. T3's activity log is append-only and records each call once. The remaining 65 (1,348 unique − 1,283 tool.completed) are calls the JSONL has and T3's store does not, spread evenly at ~5% across every tool (Bash −23, Edit −15, Read −13, Write −2 …) rather than concentrated in any one type — the fingerprint of dropped ingest events, not of a projection gap. Recorded as fog; it is a T3 ingest-completeness question, not this ticket's.

    Ran over all 21 real threads: 3,805 events, no crashes, largest thread projected in 76 ms.

    Deviations from mindwalk, all deliberate

    1. Codex's commandActions targets are not weak — as the design decided. Typed discriminator with an absolute path from Codex's own sandbox parser beats a shell string.
    2. Codex's error signal is exact, not estimated — a second deviation of the same character, not previously enumerated. mindwalk sniffs Codex failures out of output text because rollout JSONL is all it has; T3 stores status and exitCode structurally. Reporting "estimated" here would be exactly the lie that grade exists to prevent.
    3. No exec / js / js_repl branches in actionFor/targetsFor. Those parse Codex's free-form JavaScript orchestration wrapper out of rollout JSONL. T3 talks to the Codex app-server, which delivers a commandExecution item with a plain shell string, so the ~200 lines of JS-source parsing behind them are unreachable. Everything else in the copy list is verbatim.

    What the port confirmed

    • The scraping is load-bearing, as predicted. Across the whole store: 2,120 exec, 724 edit, 543 read, 265 search. Of 3,971 targets, 3,030 are weak — scraped out of Bash. Without the scraping, search and the "seen" level would be near-empty.
    • Observability.Reads is estimated on essentially every thread, and unavailable on the few that only ran shell commands. Honest and correct.
    • verify fires almost never in practice: 1 event across 3,805. mindwalk's patterns are go test / npm test / pnpm test / pytest / cargo test / make test; this environment runs vp test run and CMake. Faithful to upstream and left alone — recorded as fog, since editsAfterLastVerify and the verify bar are dead here until the list grows.

    Two things the build settled that the design had not

    • Provider comes from the session row, not the thread shell. getThreadShellById only returns active threads, so an archived thread would have projected with the wrong adapter and rendered a dark, empty city. Caught by the service test. Now reads ProjectionThreadSessionRepository.
    • An unnamed provider is read by payload shape, not assumed. Defaulting an unknown provider to one adapter fails silently — a Claude payload read as ACP finds nothing and looks like "the agent touched nothing" rather than like a failure. The fallback now dispatches on whether data.toolName / data.item / data.tool is present.

    Cache

    Exact, keyed on COUNT(*) of the thread's activities (per lens), no TTL — the log is append-only, so the count is a perfect fingerprint. A repository test pins that an upsert of an existing row does not move it. Read is SQL-filtered to the three kinds the projection wants, which skips the majority of a long thread's payload bytes before parsing. Ghost re-layout on a cache miss measures 40–80 ms on a 15.7k-file repo; the expensive walk stays cached per root in CitymapBuilder.

    Degradation

    A failed citymap build returns an empty citymap and the trace anyway — you lose the buildings, not the timeline. Verified by test.

    Tests

    107 in apps/server/src/mindwalk/ plus 4 for the new repository queries. TestCommandReadPaths, TestBuildEventClassifiesBashSearchCommands, TestSummarizeToolTruncatesCommandAtRuneBoundary and all three of stats_test.go are ported case for case from the Go suite. Claude and Codex adapter fixtures are trimmed copies of real payload_json rows. Nothing asserts ACP or OpenCode behaviour beyond the graceful fallback, per the ticket.

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

Metadata

Metadata

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions