Skip to content

minimax-ai/mcode-trajectory-studio

v0.1.3Apache-2.0

只读查看本机 MiniMax Code 会话轨迹:轮次、步骤、LLM 与工具的墙钟耗时、token 用量、工具入参与结果、上下文拆分、压缩与子代理任务,并附带一个本地交互式面板。数据取自本地运行时自身的 SQLite 投影;插件不写入任何文件、不联网、不收集遥测。面板仅监听 127.0.0.1,每个 API 路由都要求一份随进程新生成的访问凭据。需要 Node.js 22.13.0+(内置 node:sqlite 的下限),已验证范围与 mcode engines 一致:>=22.19 <23 或 >=24 <27;全文检索另需内置 SQLite 带 FTS5,22.13.0-22.18.x 与 23.x 缺失时搜索降级并告警。 / Read-only inspection of local MiniMax Code session trajectories: turns, steps, LLM and tool wall-clock time, token usage, tool arguments/results, context breakdown, compactions, sub-agent tasks, plus a local panel. Data comes from the local runtime SQLite projection; the Plugin writes no files, no network, no telemetry. The panel listens on 127.0.0.1 only and requires a per-process capability token on every API route. Requires Node.js 22.13.0+ (the built-in node:sqlite floor); the verified range matches mcode engines: >=22.19 <23 or >=24 <27. Full-text search also needs FTS5 in that bundled SQLite; absent in 22.13.0-22.18.x and 23.x, where search degrades with a warning.

English | 简体中文

MCode Trajectory Studio

A read-only flight recorder for local MiniMax Code sessions, backed by the runtime's own SQLite projection, with an interactive local Studio panel.

MiniMax Code's local runtime mirrors every session into a SQLite projection at <dataDir>/v2/sqlite/runtime-state.sqlite. This Plugin reads that projection strictly read-only and turns it into structured trajectory summaries plus a browsable timeline — so you can answer "how long did this take", "where did the time go", "which tools ran and which failed", "how many turns and steps", "where did the context budget go", "when did compaction run", and "which sub-agents were dispatched" — without opening the raw session files.

Try it

Use the mcode-trajectory-studio skill to summarize my most recently updated session:
turns, steps, LLM versus tool wall-clock time, tool failures, and compactions.

Expected result: the agent calls trajectory_summary, reports the folded statistics, and names ttftMs as unavailable rather than estimating it.

Open the Trajectory Studio panel for that session so I can look at the timeline myself.

Expected result: the agent calls trajectory_studio, receives a 127.0.0.1 URL carrying a per-process capability fragment, and opens it verbatim in the host browser. The panel shows a time overview, a per-turn record table, an inspector for each record, filters, and the background-task list.

Capabilities

ToolPurpose
trajectory_listRecent sessions with non-content metadata; filter by agent, kind, or time
trajectory_summarydsh sessionStats-equivalent fold: turns, steps, llmMs, toolMs, decodeMs, tokens, tool calls and failures, compactions, sub-agents, assets, trigger sources
trajectory_turnsPer-turn progress in one call: records, LLM milliseconds, failed tool calls and output tokens per turn — failureCount is what makes a session's toolFailures drillable
trajectory_assetsThe files a session delivered or referenced — the list behind trajectory_summary.assets
trajectory_agentThe agent definition a session was dispatched with: model, tool allowlist and denylist, MCP servers, skills, system prompt
trajectory_findLiteral-text search inside one session; scoped on purpose, since the same search across the machine measures seconds. Searches the message, the thinking and tool arguments alike, and field names which of the three a hit is in
trajectory_getPaged trajectory records; summary or full detail
trajectory_searchFull-text search over session ids, titles, agents, statuses, kinds, workspace paths, and the sub-agent dispatch purpose the runtime records — which is why a query can be a content query. These queries are not selective, so total is the match count and nextCursor is the cursor to hand back as cursor
trajectory_tasksBackground tasks and sub-agent dispatches with status, duration, command or objective, sub-agent name, and child session ID
trajectory_task_outputBounded tail of one task's captured output
trajectory_studioStart or stop the local Studio panel. The only tool here that is not read-only: it opens a loopback listener and mints a capability, so a client must prompt rather than auto-approve

Paging. Two cursor fields, and which one a tool uses is not arbitrary. trajectory_get, trajectory_find, trajectory_turns, trajectory_tasks and trajectory_assets order by an append-only key, so nextOffset handed back as offset is a position in a list nothing reorders. trajectory_list and trajectory_search order by updated_at_ms DESC — the column the runtime rewrites every time it writes to a session — so they hand back nextCursor to be passed back as cursor, and pin each walk to one instant (walkPinnedAtMs). A cursor carries the walk it came from, so passing one back after changing the query or any filter returns page one of the new question with a note rather than a page that looks like a continuation and is not. A walk by position over that order silently drops any session written to while it is running and still reports that it finished; measured on a twelve-session fixture, one ended claiming twelve of twelve and had delivered eleven distinct ones.

The Studio panel is built as a narrative rather than a stack of rows. Each surface answers one question and does not repeat another surface's answer:

SurfaceAnswers
Agent 与能力What was this session configured with — model, tool allowlist, skills, system prompt
统计条What are the session totals — turns, steps, LLM/tool/decode wall-clock, tokens, failures
时间轴Where in time did it happen — navigation and zoom only, no text
轨迹流What happened — one scannable line per message and per tool call
检查器The full detail of exactly one selected row
  • Sidebar is a minimal tree. Sessions group by git repository, not by path: git rev-parse --git-common-dir folds every worktree of one project into a single collapsible group, so a long-lived repository does not fragment across the sidebar. Directories outside a git tree fall back to path grouping. Sub-agent sessions nest under the session that dispatched them, recursively, with a caret to expand — so a fan-out reads as a tree instead of a flat list of look-alike task sessions. Rows carry only a caret and a title; agent, branch, workspace and age live in the tooltip, and expansion state persists across reloads.
  • Timeline uses three lanes sharing one zoomable axis — INPUT (human messages in blue, harness-injected context in violet), MODEL (requests split into thinking and output segments) and TOOLS (measured task spans in orange, gaps derived from record spacing in grey and labelled as derived). Wheel zooms, drag pans, double click resets.
  • Sidebar rows toggle on the whole row, not just the caret: clicking a collapsed parent selects and opens it, clicking the selected open parent closes it. The agent filter is a dropdown whose options are read from the data (mavis, verifier, explore, … as configured on that machine), because sub-agent presets differ per install.
  • Stream merges messages and tool calls into one narrative: TOOL bash ▸ {payload} ⇉ {result} with the measured duration, an agent badge for sub-agents, and a one-click jump into the sub-agent's own session. Injected context is visually distinct from things the user typed. Filter by human input, injected context, tool calls, or failures only; filter by turn ID or text.
  • Themes: light and dark, defaulting to the OS preference and switchable with one click. Compact 2px radii throughout, and thin scrollbars via both the standard properties and the WebKit pseudo-elements.
  • Turn headers lead with a human ordinal (第 3 轮) and keep the runtime's turn ID as secondary text.
  • Performance: the stream renders 150 rows and appends the next batch as you scroll, rows are cached and filtering never rebuilds the DOM, text filters are debounced, and turn totals are folded server-side so a paged view still shows whole-turn numbers. A session switch on a 600-row session went from ~700 ms with 227 ms main-thread stalls to ~120 ms with none. Very large sessions (10k+ records) had a separate problem: the statistics fold re-parsed every row's JSON in a correlated subquery, so it now expands tool_calls in a single join and caches folded totals against the session's updated_at_ms — a revisit costs 1 ms instead of 383 ms. Records are paged from the server (200 at a time) instead of fetching 1000 with full content up front, and the timeline reads a separate compact projection so the axis always covers the whole session.
  • Inspector is tabbed: 概要 (hierarchy, status, token usage, context split), 载荷 (tool arguments or message body, with the result text and a failure evidence block alongside), and 计时 (recorded instant, request/thinking/decode time, and whether the tool duration was measured or absent). There is no Schema tab.
  • Failure localisation is first-class: the header carries a ⚠ N 处失败(定位) jump, the stream marks each failure inline, and the result tab separates hard failures (Traceback, stderr, is_error) from soft ones (a non-zero exit code, which is often just grep finding nothing). A call can report success while its output contains a Traceback, so the classification reads the text, not only the status code.

What this reads

SourcePathUsed for
SQLite projection<dataDir>/v2/sqlite/runtime-state.sqlitePrimary. Opened mode=ro; never written.
Session artifact<dataDir>/v2/sessions/YYYY/MM/DD/<stamp>-<id>/messages.jsonlFallback for sessions the projection has not indexed, and for content when a projection field is absent.

<dataDir> resolves in this order: MINIMAX_DATA_DIR, MAVIS_DATA_DIR, then ~/.minimax — where ~ is HOME, or USERPROFILE on Windows. Nothing about the location is baked in.

Inside the data directory the Plugin tries the canonical layout first — v2/sqlite/runtime-state.sqlite and v2/sessions — and falls back to a short list of alternative layouts before giving up, so a build that arranges its files differently is still readable. A hit outside the canonical layout is reported as a warning rather than passing silently, and --doctor prints the path that was actually opened on this machine.

Fields worth knowing

  • usage.request_duration_ms — per-request model wall-clock time (llmMs).
  • thinking_duration_ms — thinking-phase duration per record.
  • tool_calls[].tool_call_args, tool_call_result_data, tool_call_status — tool input, output, and status (a status other than 2 did not succeed).
  • local_runtime_background_tasks — created_at_ms/ended_at_ms give exact tool and sub-agent wall-clock time (toolMs).
  • kind: compaction records carry messagesBefore/messagesAfter and tokensBefore/tokensAfter.
  • context_usage.components — per-request split of SYSTEM_PROMPT / MEMORY / TOOLS / SKILLS / MESSAGES.
  • source — what triggered the model call: api, thread-goal, questionnaire, background-task, task, agent, greeting.
  • local_runtime_sessions_fts — FTS5 index over session metadata.

Honest limitations

  • Time-to-first-token is not persisted by the runtime. ttftMs is always null and is never estimated. The Studio overview uses a thinking segment versus output segment instead.
  • decodeMs is an approximation: Σ(request_duration_ms − thinking_duration_ms).
  • The local_runtime_* tables are an internal runtime detail. Every optional table, column, and JSON field is probed before use and degrades to null; a future runtime change should reduce fidelity rather than break the Plugin. If SQLite is unreadable the Plugin falls back to messages.jsonl, where timing fields are absent.
  • Tool wall-clock time is only available for calls that the runtime recorded as background tasks; toolMs therefore covers those, not every inline call.
  • The projection indexes sessions as they are written. A very recent session may not be present yet; the JSONL fallback covers that case.
  • SQLite needs write access to the directory, even for a read-only connection, because a WAL database requires its -shm file to exist. On a read-only mount, or when the data directory belongs to another user, the projection cannot be opened: the Plugin reports sqlite_unavailable:attempt to write a readonly database and falls back to messages.jsonl, where the timing fields are absent. Reading a live WAL database that another process is writing works normally.

Privacy

  • The Plugin is read-only: it never writes to, moves, or deletes session storage, and it creates no files of its own. It holds no state on disk at all.
  • The Studio panel binds 127.0.0.1 only, and the address is a constant with no override — no environment variable, argument, or config key can widen it.
  • Every API route requires a capability token, minted fresh per process with 256 bits of CSPRNG entropy and handed to the caller in the URL's fragment (#t=…). It lives in memory for the life of the MCP server process and is never written anywhere. Two consequences worth knowing:
    • The fragment never reaches the server, so the capability stays out of request logs, out of Referer for asset requests, and out of anything the server can echo. Open the URL exactly as returned — a URL without #t=… loads the page shell and no data.
    • mcode runs one MCP server per session, so two sessions get two panels with two capabilities, and one session's URL cannot open the other's panel.
  • The Host check, the Origin check, and the Sec-Fetch-Site check are retained, but they are cross-site request forgery defences, not authorization: they stop a web page, not a local process. The capability is what stops a local process. Static assets ship a deny-by-default CSP (script-src 'self', no inline script, no remote origin) and every node the page renders is built with createElement and written with textContent — a session body is text, never markup.
  • summary detail returns no message text, thinking, tool arguments, or tool results.
  • full detail is opt-in and passes through a two-tier secret redactor. The shape a credential actually takes on disk is a JSON document stored inside a JSON string — tool results live in tool_call_result_data as text (109,462 of 118,109 rows in a real projection) — so it reaches the redactor escaped, as {\"api_key\":\"…\"}:
    • Tier A (structured). A string value that parses as JSON is walked again as structured data and judged by key name. The string is rewritten only when a redaction actually fired, so the JSON tool results in the panel are never reformatted to protect nothing.
    • Tier B (escape-aware text rules). The key/value rule tolerates any number of backslashes on either delimiter and re-emits them, so it also fires on text that is not valid JSON at all (truncated, or interleaved with prose).
  • The text rules cover private key blocks, credentials inline in a connection string (including a password that itself contains @), the whole Authorization header, a bare scheme and token, provider-prefixed keys (sk-, ghp_, glpat-, xox* …), length-anchored tokens (github_pat_, npm_, hf_, Google AIza, ya29.), AWS key ids, an Azure AccountKey=, an unlabelled JWT, and key/value pairs.
  • Credential keys are matched by word boundary rather than by an exact name list, so camelCase variants such as clientSecret, refreshToken, accessToken, authToken, apiSecret and xApiKey are redacted too, while counters (inputTokens) and addressing identifiers (sessionId) survive. Task descriptions and commands are redacted at the data source, and session titles have credential substrings removed in place. Every outbound payload is swept once more at the boundary, and the MCP error branch goes through that same sweep — an egress that is only redacted when it succeeds is not a boundary.
  • The rules are idempotent: text swept three times (data source, per record, egress) equals text swept once, so a marker cannot grow a character per pass.
  • Personal data is masked only on the egress that leaves the machine. An MCP result reaches a model context, so that sweep also masks e-mail addresses and phone numbers. The panel is the reader's own screen, where masking a customer's address would destroy the answer they opened it for, so the panel stays faithful.
  • Path folding. The home directory, the data directory (MINIMAX_DATA_DIR, including a deployment that places it outside the home directory) and any extra root named in MCODE_TRAJECTORY_REDACT_ROOTS are collapsed to ~ wherever they appear — inside a warning string, and in the doubled-backslash form a Windows path takes inside a JSON column. Another account's home keeps its shape and loses the name (/home/<user>/…). That setting can only fold more, never less, so it is not a configuration surface that can weaken anything.
  • Responses are bounded by total size, not only per string: an event page is trimmed to a byte budget and reports truncated/omitted so the client follows the cursor instead of assuming it received everything. That budget applies to every record-list tool — trajectory_tasks with limit: 2000 was the one that could still return tens of megabytes — and to the panel's task list. The cursor is the first record the caller did not get, not the first one the server read, so a trimmed page resumes where it actually stopped instead of skipping the records in between.
  • The SQLite read also has a per-row ceiling (8 MiB), measured in UTF-8 bytes: a row past it is reported as oversized with its byte count rather than parsed into the process or silently dropped. (length() on a TEXT column counts characters, so the test is cast to BLOB — otherwise a row of 4-byte code points passes the cap at a quarter of its real size.) A JSONL record with no newline can never grow the read buffer past the 2 MiB line cap — the buffer is capped as chunks arrive, not after the fact — and lines dropped by that cap are reported in droppedOversized.
  • The JSONL fallback pages like SQLite does, rather than re-reading the artifact from the top and reporting the first page as the whole file. Its total is null: the fold stops one record past a full page, so it never counts the file it deliberately did not read.
  • Diagnostics are bounded too: warnings keeps the most recent 64 entries and reports how many were dropped in warningsDropped.
  • Filesystem containment canonicalizes every traversed component, refuses a symlinked final component, and — on Linux — re-verifies the opened descriptor via /proc/self/fd so an intermediate directory swapped between canonicalization and open is refused. Where /proc is unavailable (macOS, Windows) that residual race is the documented, accepted limit: it needs a same-UID process to win a microsecond window, and such a process can already read the file directly.
  • The one place the Plugin runs an external command is git rev-parse (used to fold one repository's worktrees into a single group). It receives an allowlisted environment rather than the whole process.env, so no credential the host exports is inherited, and core.fsmonitor and the credential helper are fixed off. (Measured: an alias.rev-parse planted in a repository's own .git/config cannot shadow the builtin.)
  • Nothing is uploaded anywhere; there are no network destinations and no telemetry.

Known limits

Stated plainly, rather than left to look covered:

  • The redactor recognises credential shapes and credential-named keys. A high-entropy string with no label and no provider shape is not a credential to it.
  • PII masking covers e-mail addresses and phone numbers only, and only on the MCP egress. Names, postal addresses and customer names are out of pattern range.
  • An absolute path under a root you did not configure (say /mnt/customers/<account>/…) is returned verbatim — including workspaceDir in summary detail. Add it to MCODE_TRAJECTORY_REDACT_ROOTS to fold it.
  • The panel capability travels into the session transcript with the tool result. trajectory_studio returns the URL carrying #t=…, the runtime persists tool results in session storage, and the context is sent to the model service on later turns. What contains it is that the capability is process-scoped (it exists only in that MCP server's memory) and the panel is loopback-only, so it is unreachable once the process ends. If you want no capability in a model context at all, start the panel yourself with node server/main.mjs --serve — the URL is then printed only to your terminal.
  • --doctor prints the data directory and the database path verbatim on purpose: it is the diagnostic you read and decide whether to share, and folding those paths would remove what makes it useful.

Requirements

  • MiniMax Code 0.4.0+ (mcode) with Agent Plugin and MCP support.

  • Node.js 22.13.0 or newer on PATH — the hard floor, because node:sqlite only exists, without --experimental-sqlite, from that release. Below it the Plugin exits with that sentence rather than a module-resolution error.

  • Verified range: >=22.19 <23 || >=24 <27 — the same range mcode itself declares in its engines field. The Plugin mirrors it rather than inventing its own, so the two can never disagree.

  • Full-text search additionally needs FTS5 in the bundled SQLite, which is not monotonic in the Node version — measured:

    NodeBundled SQLiteFTS5
    22.12.0—node:sqlite absent entirely
    22.13.03.47.2absent
    22.15.03.49.1absent
    22.19.03.50.4present
    22.21.13.50.4present
    23.4.03.47.1absent
    23.11.03.49.1absent
    24.0.03.49.1present
    24.19.03.53.3present
    25.9.03.51.3present
    26.12.03.54.0present

    Inside the verified range FTS5 is always present, so trajectory_search works. In 22.13.0–22.18.x and all of 23.x the Plugin still runs, but search degrades to no matches with an explicit warning instead of throwing.

  • The Plugin ships no dependencies and no native module, so it adds no ABI constraint of its own. mcode writes the projection with better-sqlite3 (a native module bound to one Node ABI); the Plugin reads that same SQLite file with the runtime's built-in node:sqlite instead of a second, differently-bound copy. Only SQLite's own on-disk format is shared.

  • A local runtime with the v2 SQLite projection, or v2 session artifacts for the fallback path.

  • A host browser capability for automatic panel opening; without it the panel URL still works.

Supported platforms

  • Linux (x86_64, Node.js 24.19.0, mcode 0.4.12) — the full test suite, the MCP handshake and the Studio panel were exercised there.
  • macOS and Windows — the whole suite was run on windows-latest and macos-latest during this review round, with 0 failures on both (see the evidence table below for the exact numbers and for what that first non-Linux run found). No platform-specific handling is needed: paths go through node:path, the home directory resolves from HOME or USERPROFILE, git is invoked with execFile and an argument array rather than a shell, and the SQLite driver is Node's built-in node:sqlite. Node.js 22.13.0+ is required on every platform. Worth noting for anyone reading the test suite: its first run off Linux found three platform assumptions, all of them in the tests rather than the Plugin — a temporary path compared before canonicalization (/var on macOS, a short RUNNER~1 path on Windows), a POSIX-only assertion about a resolved data directory, and a temporary directory removed while SQLite still held the file open (EBUSY on Windows, harmless on POSIX).
  • The Node matrix above was measured by running the Plugin's own suite on each release, not inferred from release notes. Those runs reported 115 pass / 0 fail on 22.19.0, 22.21.1, 24.0.0, 24.16.0 and 24.19.0, and 114 pass / 0 fail / 1 skipped on 22.13.0, 22.15.0, 23.4.0 and 23.11.0 — the single skip being the FTS5 search test, which reports itself as skipped rather than failing. That is why the floor and the FTS5 boundary are stated as two separate numbers. The suite has since grown to 166 cases (see the table below); the capability boundaries these rows establish are unaffected, because they are properties of the runtime rather than of the tests. tools/compat-matrix.mjs re-checks this table on demand and asserts it rather than printing it. The moving 22.x and 24.x lines are not pinned here because they resolve to a new release several times a year; they were 22.23.2 / SQLite 3.51.3 and 24.20.0 / SQLite 3.53.4 when this row was last checked.
  • git is optional. It is used only to group sessions by repository; without it, grouping falls back to workspace paths.

Test evidence

npm run validate                                 # the repository validator
cd plugins/weekbin/mcode-trajectory-studio
node --test test/*.test.mjs                     # the Plugin suite
#   Spelled out on purpose. A bare `node --test` globs `test/**` by Node's own
#   default rules, which also picks up `test/panel-dom.mjs` — a helper module with
#   no cases in it — and reports one extra passing "test" that is not one. The
#   explicit list is what the documented count and `doc-test-counts` both measure.
node tools/compat-matrix.mjs                     # assert this Node behaves as claimed
node tools/release-gates.mjs                     # assert the invariants no behavioural test can see
node server/main.mjs --doctor                    # data-source diagnostics against this machine
node tools/panel-e2e.mjs --smoke                 # panel over a real loopback socket, self-checking
node tools/panel-e2e.mjs                         # seed a hostile session and serve the panel

Release gates

Three review rounds ended with the same sentence from the reviewer: current tests do not cover the reproduced cases, while three green fixtures existed. The gap was structural — a stub store can express getAgentDefinition: () => null, and a null definition skips the branch that reads the overview's redaction context, so the /api/overview defect was unreachable from every fixture in the repository. Two things close that class of gap, and both run in CI (.github/workflows/mcode-trajectory-studio.yml):

  • test/egress-contract.test.mjs has no stubs. It seeds the real projection, opens the Store the way main.mjs does, and drives all thirteen tools and every API route. Its assertions are structural — no route 500s on a populated session, every record-list tool reports truncated/omitted, the cursor names the first undelivered record — because value-level assertions pass happily when a field silently disappears.

  • tools/release-gates.mjs checks what no behavioural test can see, because the property is the relationship between code, docs and shipped surface:

    GatePrevents
    version-coherencea release advertising one version in plugin.json and serving another over the wire — the version lives in four places
    engine-claimsa Node floor or verified range in the manifests that server/node-version.mjs no longer enforces
    byte-not-char-sqla byte ceiling written as SQLite length() on a TEXT column, which counts characters — invisible to an ASCII-only fixture
    doc-anchorsdocumentation quoting an implementation the code no longer has (DESIGN.md described the character-count SQL as a byte bound for two rounds)
    record-list-egressa record-list tool with no frame budget: every tool is declared a list or a single object, so adding one forces the decision that trajectory_tasks never got

Each gate names the defect it prevents, and each was verified by breaking its invariant and confirming the gate fails — including trajectory_search losing its bound, which the gate caught in this tree during development, along with a version-skew and a stale-doc anchor.

Three claims in the manifest would otherwise only be prose, so they ship with a script instead of a promise. tools/compat-matrix.mjs runs the suite under whatever Node executes it and fails unless this release behaves the way the manifest says it does: it asserts fail === 0, that the skip count matches whether this runtime's SQLite has FTS5, and that every skip is the FTS5 search test. It was mutation-checked — an injected failure and an injected unrelated skip both make it exit 1 — so a green run means something.

Thirty-three mutation scripts live in tools/mutate-*.sh, one per review finding: each breaks one invariant and fails unless a test catches it. They are run serially and through one entry point — node tools/mutation-sweep.mjs — because these scripts rewrite files in this tree, so anything reading the tree at the same time makes every verdict untraceable. The runner exists to make the tally reproducible: it reports the problem categories by name (survivor, anchor-missing, did-not-land, tree-drift, silent, crash) and exits non-zero if any of them fires. An earlier sweep reported problems: 3 from a tally whose definition existed in one shell invocation and could not be reproduced; the first run under the named categories reported problems: 2, and both were mutation tiers that had silently stopped producing evidence.

Verified withResult
compat-matrix on Node 22.23.2 (latest run)763 tests, 763 pass, 0 fail, 0 skipped (FTS5 present, SQLite 3.51.3)
compat-matrix on Node 24.19.0 (previous revision)588 tests, 588 pass, 0 fail, 0 skipped (FTS5 present, SQLite 3.53.3)
compat-matrix on Node 22.13.0 (previous revision)115 tests, 114 pass, 0 fail, 1 skipped (FTS5 absent, SQLite 3.47.2)
compat-matrix on Node 22.19.0, 22.23.2, 24.0.0, 24.20.0 (previous revision)115 tests, 115 pass, 0 fail, 0 skipped
node --test on windows-latest and macos-latest (previous revision)115 tests, 0 fail on both
node server/main.mjs --doctor on Node 22.12.0refuses to start, naming the floor and node:sqlite

Rows marked "previous revision" are the numbers actually measured then and are left as they were. The suite has grown from 115 in those rows to 763 as the fixes' canaries and the egress-contract and release-gate suites were added; the latest pass contributed 28 cases — escaped-form redaction and idempotence, the MCP error branch and its own sweep, the per-row and warning bounds, the git child environment, root parsing, the panel's privacy default, and the JSONL drop-count case. A later pass added the panel's authorization matrix over a real socket, the Skill's own claims checked against the code, and strict command-line parsing. The most recent pass added three cases on the redaction sweep's cost: the boundary a key: value match may start from, a quoted key that has to survive that boundary when its value is itself a nested document, and the ratio between a payload that carries key separators and one that does not. Re-measured green on Node 24.19.0. The other Node releases and the Windows/macOS runners have not been re-run since, so the next CI run covers them. The last three rows were run on Windows and macOS for the first time during this review round, and that run is the reason the suite still changes: it found three platform assumptions in the tests (a temporary path compared before canonicalization — /var on macOS, a short RUNNER~1 path on Windows — a POSIX-only assertion about a resolved data directory, and a temporary directory removed while SQLite still held the file open, which is EBUSY on Windows and harmless on POSIX). All three are fixed here.

This Plugin ships its own CI in .github/workflows/mcode-trajectory-studio.yml, scoped by paths: to this Plugin so it cannot spend the repository's minutes on anyone else's change. It runs the release gates, the suite on a Node matrix across ubuntu-latest, macos-latest and windows-latest, and a separate engines job that pins the declared Node floor. A workflow file introduced by a pull request is not executed for that pull request, so on this PR these jobs only gate once the file lands on main; the runs quoted below are dispatched from the fork, the route tool-map-windows.yml already documents.

The Plugin's own tests cover the SQLite reads, the JSONL fallback, the git grouping (including a real worktree merge), input provenance, the tool-call/task join, the agent definition lookup, redaction, the MCP protocol surface, the module graph (that it is acyclic, that no client surface imports the orchestrator, and that every relative import resolves), client formatting edge cases, the local panel's request fences and error contract, path portability (no machine-local path is hardcoded, and the projection is found in a non-canonical layout), and the package declarations (both manifests and the MCP descriptor, version coherence, package hygiene and the size limits).

Five suites exist because the previous revision was exploitable in ways its tests did not cover, and each one is written so that "refuse everything" cannot pass it:

  • containment.test.mjs plants a canary outside the data directory and reaches for it through a symlinked task directory, a two-hop link, a relative link, a symlinked output.log, a symlinked session directory, and a symlinked messages.jsonl. Every case asserts the canary is absent and that the read is reported unavailable, with a positive control alongside that must still succeed. It also pins the resource behaviour: a large unterminated JSONL line takes the incremental buffer cap's discard path (droppedOversized), a line after it still folds, and the post-open gate refuses a descriptor that resolves outside the root while falling back to the documented boundary where /proc is absent.
  • redact.test.mjs pins each encoding that used to escape: JSON object forms, nested envelopes, the escaped form ({\"api_key\":\"…\"} — the runtime stores tool results as JSON text, so this is what a credential actually looks like on disk), double escaping, Authorization: Bearer …, Basic, proxy-authorization, a header inside a shell command, a tool-call description, and the camelCase credential keys (clientSecret, refreshToken, accessToken, authToken, apiSecret, xApiKey) that an exact-name set leaked. Each asserts the secret is gone and that the surrounding payload survives, that the identifiers the API is keyed on (sessionId) and the usage counters (inputTokens) are not mistaken for credentials, that a home path is collapsed wherever it appears, and that the depth and aggregate-byte bounds hold. Four assertions are properties rather than examples: idempotence (redact(redact(x)) === redact(x)) over the whole corpus × four option sets, that no sweep ever grows a marker a bracket at a time, that a truncation marker is stable across sweeps, and that a JSON string with nothing to redact is returned byte-identical — tier A must not reflow the reader's data to protect nothing. Over-redaction is pinned too: npm_config_registry and HF_HOME must survive, a 13-digit epoch must not be read as a phone number, and a connection string whose password contains @ must not leak the tail the earlier rule left behind.
  • panel-security.test.mjs covers the capability (the old fixed header alone is now 403, a wrong-length value is refused without throwing, a repeated header is refused, the right one is accepted and not echoed), process isolation (one panel's capability does not open another's), the loopback bind plus unreachability on every non-loopback interface and a peer-address fence, and a mutation-checked scan that fails if any shipped asset ever uses a markup injection sink.
  • protocol.test.mjs spawns the real MCP server over stdio, the way mcode does, and drives it as a client: version negotiation, the thirteen declared tools and their annotations, a call against a fixture projection, full detail redacted on the wire (including structured credential keys that only the key name can catch, and a credential hidden inside a tool result — stored, as the runtime stores it, as JSON text behind an escaped key), the failure branch swept through the same path as the success branch, a reply trimmed to its byte budget with truncated/omitted reported, unknown tool and unknown method, a notification left unanswered, --doctor, and the lifecycle — closing stdin has to end the process with the panel running, because a listener left behind leaks a port on every session. Every wait is bounded, so a server that stops answering fails the suite instead of hanging it.
  • node-version.test.mjs asserts the floor and the verified range, that the FTS5 claim matches an actual CREATE VIRTUAL TABLE … USING fts5 on whatever runtime is executing, that all four declarations state the same numbers, and that no native module is pulled in.

The panel was additionally driven end to end with a browser: session switching, tree expansion, timeline navigation, all inspector tabs, the theme toggle and the failure-evidence block, at five viewport widths, and a session seeded with <img src=x onerror=…> in its body and tool arguments to confirm it renders as text with no script execution. The repository suite runs the same validator CI runs.

The security hardening re-checked the affected surface in a real browser (tools/panel-e2e.mjs driven by a browser, with <img src=x onerror=…>, <svg onload=…> and "><script>…</script> planted in the session): at the default summary detail the first paint fetches no message text at all (the toggle is unchecked and no payload is in the DOM); after ticking 显示正文 all three payloads render as literal text, the DOM holds zero on* inline handlers, zero javascript: URLs, no <img> and no <svg onload>, and the console stays clean; a credential is already [redacted] by the time it reaches the page (a ghp_… in the session title arrives as [redacted]); no token in localStorage and no cookie; and the response headers are the default-src 'none' CSP plus no-referrer. The five-viewport matrix and the theme/timeline items are from the earlier round and were not re-run. The checklist tools/panel-e2e.mjs prints was updated for the new summary default — left as it was, it would suggest the payload should be visible on load.

Install

This repository hosts Plugins as source. Point MiniMax Code at this directory as a local Plugin, or copy plugins/weekbin/mcode-trajectory-studio/ into your Plugin directory. The MCP server entry starts automatically.

How the package is declared

The package follows the same layout as every other Plugin merged into this repository — the separate Marketplace-submission layout (.minimax-plugin/plugin.json plus *.mcp.json) is not shipped here:

FilePurpose
plugin.jsonthe portable Agent Plugins registry manifest
.claude-plugin/plugin.jsonthe mcode 0.4.0+ layout; points at the Skill and declares the MCP server
mcp.jsonthe MCP descriptor the runtime reads
skills/mcode-trajectory-studio/SKILL.mdthe one Skill, under a directory named for it

The runtime manifest points at the nested Skill path directly, so exactly one SKILL.md ships rather than a second copy. tests/plugins/mcode-trajectory-studio/smoke.test.mjs audits the package against this layout, with negative injection so the audit cannot pass by accident.

Diagnostics

node server/main.mjs --doctor   # resolved data dir, SQLite/FTS availability, latest session stats
node server/main.mjs --serve    # run the Studio panel standalone on 127.0.0.1
node server/main.mjs --help     # every accepted flag, with what each one does

An unrecognised flag, or a flag missing its value, prints the offending token and exits non-zero rather than starting the default stdio server and exiting 0.

Code layout

The Plugin ships no dependencies and no build step, and the source is split by layer so that no file has to hold more than one concern:

  • server/ — config · json · sqlite · fsutil (foundations), redact · git (services), sessions · stats · tasks · events · search · jsonl (domain), store (facade), then mcp · http · main (interface). Domain modules take the Store facade as their first argument, so the layer graph stays a DAG.
  • web/ — app.js only boots; the surfaces live in web/js/: state · bus · format · icons · results (leaves), api · storage · theme · banner · intents (foundations), sidebar · capability · stats · timeline · stream · inspector (surfaces), and flow · controller · wire (orchestration). Surfaces announce an intent on the bus rather than importing the action, so the client graph is acyclic too — a property a test enforces. The panel is served as ES modules.
  • test/ — the suite is grouped by what it defends, and release-gates checks that the counts and claims in these documents still match the code: store (the data layer), protocol and egress-contract (the MCP surface, the latter with no stubs), http and panel-security (the panel's routes and its rendering), panel-flow (the client's race guards, run against a hand-written DOM), redact (credentials and length bounds), read-consistency (one session compared against itself across every surface), containment and portable-paths (the data directory boundary), modules (the import graph is acyclic and every import resolves), format (client formatting edge cases), node-version and release-gates. The package audit lives in the repository at tests/plugins/mcode-trajectory-studio/smoke.test.mjs.

License

Apache-2.0. See LICENSE.

Design notes

The research and interface comparison behind this Plugin — including why the runtime SQLite projection is used instead of session JSONL artifacts — is recorded in DESIGN.md.