Repository navigation
Project T3's activity stream onto mindwalk's Trace model #5
Description
Part of #1
Question
Define the projection from a T3 thread's activities to mindwalk's model.Trace — the ordered stream of file-touch events its scenes and playback deck consume.
This is the core of the port. Everything downstream renders whatever this produces.
Read first: .repos/mindwalk/internal/model for the Trace / Event / Target / Mark shapes, and internal/adapter/claudecode/adapter.go + internal/adapter/codex/adapter.go for how they populate them from JSONL.
T3 side: OrchestrationThreadActivity (packages/contracts/src/orchestration.ts:315) gives id, tone, kind, payload, turnId, sequence, createdAt. Ordering is turnId + sequence, continuous across resumes and compactions — one thread is one trace, no stitching.
Known mappings:
context_compactionis already a canonical item type → mindwalk's◇compaction mark maps 1:1.collab_agent_tool_call→ mindwalk's○subagent-launch mark.
Settled by research — this runs server-side
The "server-side or client-side" question is closed. projectActivityPayload (ActivityPayloadProjection.ts:167-201) replaces data with a six-key whitelist — item, command, files, toolCallId, kind, rawOutput — before anything reaches a client. toolName, input, rawInput, locations, and state are all dropped at the wire. A client-side classifier would require widening a whitelist that exists deliberately for wire-size reasons.
So this projection reads the event store directly, server-side, before the whitelist, alongside the citymap service. No wire changes needed. See Classify read and search tool calls per provider adapter for the full analysis and the per-adapter payload shapes.
Reuse, do not reinvent: packages/shared/src/toolActivity.ts already has classifyToolAction returning command | read | file_change | search | other plus a path walker. Widen that into classifyFileTouch rather than adding a sixth path-walker.
Per-provider notes from the research:
- Claude —
{ toolName, input, result }; classify by tool name. Input key casing unverified (Capture live tool-call payloads for Cursor/Grok and OpenCode #11). - Codex — no read/grep tool; everything is
command_execution. ButcommandActions[]: { type: "read" | "listFiles" | "search", path, query }is already in every payload and unused. Best source of the five. - Cursor / Grok (ACP) — use
data.kind. Note ACP mapskind: "search"toweb_search(AcpCoreRuntimeEvents.ts:55-57), colliding codebase greps with web fetches; needs disambiguating. - OpenCode —
{ tool, state }, shape unverified (Capture live tool-call payloads for Cursor/Grok and OpenCode #11).
Settled by Close the agent-lenses data gap — the main trace excludes subagent work
Once Persist the subagent attribution edge lands, this projection drops any item carrying a parentToolCallId. Mindwalk's Main lens is the root session file only and never merges subagent work; T3 today inlines it, unattributed.
This is a real behavior change, not cosmetic: fewer lit buildings, fewer edits, different regressionRate and churnFiles, and the trace goes quiet during a heavy Task. That is what mindwalk shows. Subagent work is reachable through its lens instead — see Project the agent graph and per-agent traces, which reuses this same projection restricted to one launch's subtree.
Threads recorded before the edge ships, and providers with no attribution, keep their subagent work inlined. That inconsistency is accepted and invisible in practice: such a thread has no lens to look inconsistent against.
Still to decide
- On-demand or persisted. The trace is derived; caching it has the same invalidation questions as the citymap.
- Tool calls that touch no file. Mindwalk keeps them as cool events on the histogram, so dropping them changes the timeline's shape and breaks the faithful-port rule. Confirm they survive.
- "Weak" targets. Mindwalk's builder skips
target.Weakwhen adding ghost files (internal/citymap/builder.go:47). Work out what makes a target weak and what the T3 equivalent is.
Provider scope — narrowed
Capture live tool-call payloads was closed as out of scope: the dev does not use Cursor, Grok, or OpenCode (real history is 18 claudeAgent threads, 2 codex, zero of the rest). This ticket is no longer blocked on it.
Implement and verify against Claude and Codex, whose shapes are both confirmed without needing a live capture:
- Claude — classify by
data.toolName; path atdata.input.file_path(snake_case, confirmed against real session logs). - Codex — no read/grep tool; everything is
command_execution. ReadcommandActions[]: { type: "read" | "listFiles" | "search", path, query }, already persisted and currently unused. Best source of the five.
Keep the seam provider-agnostic. ACP (Cursor/Grok) and OpenCode stay implemented-but-unverified: use data.kind for ACP and { tool, state } for OpenCode as documented in docs/internals/research/provider-tool-call-classification.md, and fall back to "touched, kind unknown" rather than throwing or silently dropping when a shape does not match. Adding a provider later should be adding a mapping, not reworking the classifier.
Note ACP maps kind: "search" to web_search, colliding codebase greps with web fetches — handle it in the mapping even though it is unverified, since the research established it from source.
Do not gate correctness on the unverified providers: no test should assert ACP or OpenCode behaviour that has not been observed.
Activity
docs(internals): correct ACP locations survival in the projection
docs(internals): correct ACP locations survival in the projection
Dillpickleschmidt commentedon Aug 1, 2026
Answer
The projection runs server-side over
projection_thread_activities.payload_json(full, unstripped), and ships as one endpointGET /api/mindwalk/threads/:threadIdreturning{ trace, citymap }— mirroring mindwalk's/snapshot, which is what its own UI fetches (App.tsx:183).One endpoint, because the two artifacts are mutually dependent and the ticket didn't note it: the citymap needs the trace's target paths for ghost buildings, and the trace needs the citymap for
Target.fileIdandStats.FilesInRepo. Mindwalk resolves this with a strict order in a single request (server.go:834-841): build the trace with paths only → build the citymap with the trace → backfillfileIds → recompute stats with the real file count. We copy that. The 752 KB gzipped citymap also wants sending once, not twice.Copy mindwalk's classifier verbatim; leave
toolActivity.tsaloneThe ticket says in bold "reuse, do not reinvent — widen
classifyToolActionrather than adding a sixth path-walker." Both halves of that premise are wrong, and the measured data settles it decisively.The research it rests on states
toolActivity.tsis "already shared between web and mobile." It isn't.classifyToolActionis not exported; it's a private helper whose only consumer in the repo isAcpRuntimeModel.ts:362, server-side, generating a readable title string for Cursor/Grok activities at ingest. Zero client consumers. Reusing it would mean growing a single provider's ingest-path helper into a cross-provider measurement engine — a dependency pointing the wrong way, since it's handed one live in-flight tool call, not a stored activity.The tool census across all 21 threads decides it outright:
Grep/Globat all. So the ~250 lines of path scraping are load-bearing, not optional: without themsearchwould be ~0, thehit/"seen" touch level would never light, and the histogram would be one giantexecbar.Readunder itemTypedynamic_tool_call. T3's own classifier would have called itotherand missed every read in the repo. Branching ondata.toolNameis what saves it.So: port
actionFor,targetsFor, the scraping helpers (extractPaths,commandReadPaths,parsePathHits,normalizePath,repoPathExists), theverify/search/readcommand matchers, andComputeStatsverbatim. Two classifiers in the repo is correct — different questions, different consumers, different stages, neither constraining the other. One source of truth (payload_json), two readers.Claude's payload already is mindwalk's shape
Verified against real rows:
{ toolName, input, result: { content, is_error } }. So the Claude adapter is close to a one-liner, andfile_pathis snake_case — exactly whattargetsForexpects. The ticket's "input key casing unverified (#11)" is now verified, no live capture needed.is_erroris structural, soObservability.Errorsis"exact"for Claude.Agent(8 calls) is already covered by mindwalk's"Task" || "Agent"subagent check.Codex's
commandActions[]confirmed present and typed:{ command, name, path, type: "read" | "unknown" }— withtype: "unknown"falling through to the scraping path, exactly as needed.Everything T3-specific lives in a thin per-provider adapter reshaping an activity into
{ tool, input, result }— the same boundary mindwalk already has (internal/adapter/over a sharedadapter.go). Adding OpenCode later is one more adapter.Which activities become events
One event per tool call, folded at
tool.completed. Measured on the largest thread (8,011 activities):tool.completedis 1,283 rows / 15.4 MB, whiletool.updatedis 2,564 rows / 16.6 MB — so a SQL filter skips over half the bytes before parsing.tool.denied→ an event withisError: true, no targets. Matches what mindwalk renders for a rejected tool in Claude's log, and denials are genuinely interesting on the timeline.errorRatedenominator.approval.*,runtime.*,checkpoint.captured,turn.plan.updated,task.*,context-window.updated→ dropped. Mindwalk'sEventmeans "the agent used a tool"; folding these in would inflate the histogram against a denominator mindwalk never had.approval.requestedandruntime.errorare the tempting two — recorded as fog, as future mark types rather than events.Marks — one correction
collab_agent_tool_call. Clean 1:1; both a mark and an event, as mindwalk does forTask.context_compactioncanonical item type. It doesn't. Non-tool item types are dropped before ever becoming activities (ProviderRuntimeIngestion.ts:640). It actually arrives as athread.state.changedevent withstate: "compacted", stored under activity kindcontext-compaction(:574-593). Still 1:1, different source.createdAt, tiebreaking a message before an activity at the same instant. Turn-anchoring was the first instinct and is unavailable: user messages are always storedturnId: null(decider.ts:795) — only assistant messages get one — and the messages table has nosequencecolumn, while activities'sequenceis the provider's session sequence.createdAtis the only shared key, and it's safe here: adapters stamp activities with the T3 server's own clock (nowIso) at observation, and messages withcommand.createdAtfrom the same process. One clock, no skew.Weak targets
weakmeans "guessed out of prose" — the safety valve on the scraping. A weak target is discarded if the file doesn't exist, never creates a ghost, dragsObservability.Readsto"estimated", and is skipped when the trail picks a file. Copying the scraping carries all of that over: scraped from text → weak; typed field → not weak.One deviation: Codex's
commandActionsis not weak. Mindwalk marks every Codex target weak because shell strings are all it has; T3 has a typed discriminator with an absolute path from Codex's own sandbox parser. Marking that weak would make every Codex thread report"estimated"read confidence — precisely the lie the field exists to prevent, since it's meant to separate a real signal from a blind spot. Same rule, better data. Cost: Codex traces now produce ghosts for read-but-absent files, which mindwalk never shows.Since Bash is 66% of calls and all Bash targets are weak,
Observability.Readswill read"estimated"on essentially every Claude thread. That is honest and correct.On-demand or persisted → in-memory cache keyed on the thread's activity count
The ticket frames this as "the same invalidation questions as the citymap." It isn't, and that's the answer. The citymap's key is fuzzy —
commit + dirtycan't see an edit to an already-dirty file, which is why it needs a 30s TTL and why that hole is live fog on the map. The trace's input is an append-only log for one thread: rows are never updated or deleted, so the row count is a perfect fingerprint. Same count, same trace, guaranteed.Exact invalidation, an index-covered
COUNT(*), no TTL, no migration, no second source of truth. Live-watch is coming and will poll this endpoint; a count key makes a no-change poll nearly free instead of re-parsing 15 MB.Root and degradation
Root is
worktreePath ?? workspaceRoot— identical to the citymap, or nothing lights up.On citymap build failure, return an empty citymap and the trace anyway, not a 500. This is mindwalk's own behaviour (
server.go:835) and it isn't hypothetical: six threads have/home/dylankas workspace root, which is not a git repo, so the citymap there falls through to walking the entire home directory — the case most likely to fail or take forever. You still get the timeline, stats, marks and playback deck; you just lose the buildings.Mindwalk already degrades correctly if the root vanishes, and copying inherits it:
normalizePathis pure string math and never touches disk;repoPathExistsfails closed so weak targets simply drop; the citymap falls back to empty. Three independent degradations, free.Investigated and dismissed: whether a thread can be left pointing at a deleted worktree. It can't, through T3.
worktreeCleanup.ts+useThreadActions.ts:354-362couple removal to thread deletion —getOrphanedWorktreePathForThreadonly returns a path when no other thread shares it, and removal runs after the thread is deleted. The invariant is "a worktree goes only when its last thread goes." Deliberate, correct, not a T3 bug. A user can still remove one by hand from T3's terminal, but the degradation above covers that for free.Lens parameter from day one
The projection takes a lens:
main(drop items carryingparentToolCallId) /agent(launchId)(keep only that launch's subtree). Both are no-ops today — Persist the subagent attribution edge hasn't shipped, so no item carries the field, makingmaineverything andagentempty. Both correct.When that lands, both light up with zero restructuring, and Project the agent graph and per-agent traces becomes "call the same function with a different lens" rather than a refactor — the same bet the map already made for 3D Diff.
"Weak" targets, resolved (the ticket's third open item)
Answered above: weak is mindwalk's heuristic-confidence flag, it comes free with the verbatim copy, and
commandActionsis the one T3-specific exception.Dillpickleschmidt commentedon Aug 1, 2026
Resolved — see the resolution comment above. Build lands in Land the trace projection and snapshot endpoint.