Sensorium
sensorium
Record what a program actually did; ask it questions afterward.
Sensorium records one run of a program — every call, return and exception, with captured values — into a SQLite trace, and answers debugging questions from that trace in dense plain text shaped for a language model to read. It exists because reading logs is reading a diary; this is watching the execution.
Three recorders write that trace, and one command line reads it:
Python —
sensorium run -- <command>wraps one run with PEP 669 (sys.monitoring) instrumentation. Python 3.12+, no runtime dependencies.Rust —
cargo sensorium test|runinstruments a workspace's own crates at build time and writes one trace per process. Stable rustc, Linux.TypeScript —
sensorium ts run [--focus <spec>]… -- vitest run …(or-- node --test …) transforms the consumer's own sources at load time and writes one trace per test-file process. Node 24, vitest 4.1.
All three write trace format 4, so every query below
answers on any of the three kinds of trace — and where a recorder declares a
capability it does not have, the query refuses by name instead of answering
from data that was never recorded. Python is documented first; Rust
and TypeScript have their own sections here, and
rust/README.md and
typescript/README.md are their full references.
Two commitments run through all of it:
The instrument never answers from data it does not have. Truncated captures are marked and counted, sites a predicate could not be evaluated at are counted rather than skipped, a recording that died is labelled, and a rerun that turned out to be a different execution says so permanently.
Nothing here guesses. Every answer is a deterministic function of the recorded trace, and where the trace cannot settle a question the output says so instead of inferring. Sensorium reports; the agent reading it reasons. (
refocusis the one command that executes anything — it re-runs your program, which is why its whole job is telling you whether what came back was the same execution.)
Install
uv venv .venv && uv pip install -p .venv/bin/python -e ".[dev]"Requires Python 3.12+ — the recorder is sys.monitoring and nothing else, so
on 3.11 it refuses to start rather than falling back to something weaker. No
runtime dependencies; the trace is a SQLite file written with the standard
library. One capability is version-gated: witnessing a multiprocessing
spawn needs the _posixsubprocess.fork_exec audit event, which arrived in
3.14 — below it such a spawn is unwitnessed, and refocus says so by
withholding the "no child process witnessed" line rather than claiming it (see
"What the answers claim"). Everything else works
identically on 3.12 through 3.14, all three of which CI exercises. The Rust
recorder is a separate build and install: Install and
record, under Rust.
Related MCP server: uacos
Use
sensorium run -- pytest tests/test_fog.py # record
sensorium runs # what have I recorded
sensorium info last # what am I looking at
sensorium tree last --depth 3 # what actually ran
sensorium frame last --fn compute # one activation, in full
sensorium grep last compute --kind RETURN # every event that mentions it
sensorium exceptions last # what blew up, what got caught
sensorium flow last --value 1800 # where did that number come from
sensorium flow last --object build_key:record # what happened to that object
sensorium diff RUN_A RUN_B # where two runs part
sensorium refocus last --focus fog:compute # re-run deeper, verified
sensorium redact --all --dry-run # what an older store still holds
sensorium mcp --allow-run # serve all of the above to a model (MCP, stdio)Per-line state is opt-in at record time, so a predicate over locals needs a run that captured them:
sensorium run --focus fog:compute -- pytest tests/test_fog.py
sensorium watch last --at fog:compute --expr 'visible > 100'Asked against a run recorded without that --focus, watch does not report
zero hits — it reports NOTHING WAS CHECKED and prints the exact re-recording
command. Every example above was run as typed, against a small fog.py whose
compute sums cells into a visible local and whose build_key(record)
takes a dict, and a two-test tests/test_fog.py.
On an asyncio program tree groups by task, and from trace format 3 every
traced code object opens a frame — function, generator, coroutine, or async
generator alike — so a coroutine's callees nest under it exactly like a
plain function's do, tagged [generator], [coroutine], or
[async_generator] when the kind isn't function. A frame that suspended
carries a derived state as its tail: ~ cancelled (CancelledError thrown in at Ln), ~ abandoned (GeneratorExit thrown in at Ln), ~ unwound by X thrown in at Ln for any other exception thrown in, or ~ suspended at Ln at end of recording for one still parked when recording stopped. A caller
named but not framed is still never re-parented — it started running before
recording began (<- worker (no frame: started before recording)), and a
trace from before frames existed keeps arc 1's (unframed) wording exactly.
--focus, watch and LINE capture all work inside async def: a focused
coroutine's locals are captured at every LINE and interleaved with its
~ YIELD/~ RESUME rows in frame's timeline. --window is an ancestry flag, not a call-stack depth, so it
survives a suspension: another task's calls made while the windowed frame is
parked are outside the window, and the windowed frame's own calls after it
resumes are still inside. A generator or coroutine resumed on a different
thread from the one that started it — a sync generator first stepped where
it was made and finished from a thread pool, which is what a streaming
response does — keeps the frame it opened: its suspensions, its return and
anything it calls stay on that frame, and every row still names the thread
that produced it.
Recording captures calls, returns, raises and handled-events for code under
the working directory the run started in — so sensorium run -- pytest ...
traces your tests and your code, and not pytest's. --focus module:qualname
adds line-level capture with local-variable deltas for the named code;
--window QUALNAME limits that capture to what runs inside one function's
activations. Traces land in ~/.sensorium/traces (or
$SENSORIUM_DIR/traces), one SQLite file per run — the Rust and TypeScript
recorders write to the same directory, so one runs lists all three.
A run reference is a full run id, a unique prefix, or last (the most
recently written trace). Every query takes one — runs takes none and diff
takes two. Events are addressed as eN and frames as fN, and those ids are
stable, so an answer from one command is a runnable argument to the next:
--fn NAME, in both grep and frame, matches a qualname exactly
first, then falls back to substring over the trace's distinct qualnames —
one substring hit is used as if it had been named exactly, more than one is
an ambiguous call, refused with every candidate listed rather than guessed
among. frame --fn NAME --nth N picks among repeated activations and says
how many there are when N is out of range, and flow --object takes
either e<id>:<name> — any name captured at that event — or
<qualname>:<name>, which resolves to that function's first CALL and so
names one of its arguments (<qualname>:return follows the same
activation to what it handed back). A name that was not captured at the
event a spec resolves to is refused, with the names that were captured
there listed.
MCP
sensorium mcp serves the same store to a model over the Model Context
Protocol — one process on stdio, no runtime dependency, every query command
above except redact a tool, and record and refocus only under
--allow-run. Register the project's own venv, the interpreter a recording
runs under:
claude mcp add sensorium -- <venv>/bin/sensorium mcp --allow-runThe tools, the result header, the cap, the audit file and what the server
refuses: docs/mcp.md. Any MCP client, not only Claude,
registers the same binary on stdio — the {command, args} shape is on
that page.
For agents
Install, record one run, query it, serve it over MCP: ten steps in
docs/agent.md. After install, prove the path with
.venv/bin/python -m pytest tests/test_agent_smoke.py -q.
Exit statuses
The exit status is the caller's next action, not a health code:
exit | meaning | examples |
0 | the question was answered affirmatively — the trace says yes |
|
1 | the question was answered negatively — the trace says no, or none |
|
2 | the call is wrong — edit the command and ask again | an ambiguous |
3 | the trace cannot settle it — change the recording and re-record |
|
run exits with the target's own status — it never applies this table to
itself.
Every invocation is appended to <trace root>/invocations.jsonl (default
~/.sensorium/invocations.jsonl, or under $SENSORIUM_DIR) as one JSON
line with utc, argv, exit and error — the exception class name or
null — and nothing else: never the environment, never the working
directory. Set SENSORIUM_NO_INVOCATION_LOG to any non-empty value other
than 0 to turn it off for one process — =0 leaves logging on.
What the answers claim
This is the part worth reading. Each command's output is written to be exact about its own limits, and the commands differ in how much they can establish.
diff — shape, not location
diff compares two runs' causal streams on (file, qualname, kind); a MATCH
is the same shape of execution, not the same values. A function moved to
another file changes file on every one of its events, so a pure move reads
DIVERGED at the first moved CALL. diff --ignore-moves pairs a function that
left one file with the same-named function that appeared in another — only
when that pairing is unique on both sides — and prints the pairing with the
verdict as moved: helper a.py -> b.py. A name present under two files on
one side is left unpaired and any divergence inside it is still reported. A
planted call-site swap under the same move reads DIVERGED
(tests/test_diff_moves.py).
refocus — three verdicts, and a bounded licence
refocus re-runs the recorded command with deeper capture and then asks
whether the rerun was the same execution — two gates, two different exit
codes, because "the call is wrong" and "the recording can't settle it" are
different next actions (see "Exit statuses").
Cannot refocus at all (exit 2, no rerun was attempted): original trace
is INCOMPLETE; original run consumed stdin; the target no longer resolves;
the original working directory is gone; the original was recorded under the
per-thread fingerprint basis and ran asyncio tasks; the original trace
records no command to re-run, or no working directory to re-run from; or
the recorder declares capabilities.refocus: false. Every one of these is
caught before the program runs again, so the reader's next move is a
different command, not a different recording.
The program DID re-run, and the verdict below is about what came back:
verdict | exit | means |
MATCH | 0 | every thread that left a fingerprint in both runs produced the identical sequence of |
DIVERGED | 1 | the causal streams part, and the first divergence is named with a drill-in command for each side |
REFUSED | 3 | the rerun happened — a new trace exists and is queryable — but no verdict could be issued against it. Four post-rerun reasons: there was nothing to compare (neither side recorded a causal event); a side could not be trusted (INCOMPLETE, or writes dropped after its database sealed); the two traces define a thread stream differently (a pre-0.4.0 trace against a 0.4.0 one, whenever either ran a task); or a trace ran asyncio tasks and holds no task fingerprint rows, so what those tasks did would drop out of the comparison in silence. Treat the new trace as a separate, UNVERIFIED execution — a new recording is what would settle it, which is what 3 means |
diff --task NAME diffs one task's stream by name; unnamed tasks match only
unnamed tasks. Traces recorded before 0.4.0 define a thread stream to
include task events (info says per-thread basis); comparing one of those
with a 0.4.0 trace is REFUSED whenever either ran a task. Asyncio's own
default Task-<N> names count as unnamed too — the number is creation
order, not an identity, so diff --task refuses a literal Task-<N> rather
than pretend it picks anything (Ruling 4). The name a task is compared under
is the one it had when it first ran traced code: the recorder reads
get_name() once, at the moment it mints that task's identity, so a
set_name afterwards is never seen by any comparison. A thread's fingerprint row can
hold zero events under this basis: a thread whose traced code all ran inside
asyncio tasks still gets a row of its own, just with nothing in it outside
those tasks.
A MATCH is a statement about the shape of the execution, not a statement
that the two runs were the same. It licenses one conclusion: the rerun took
the same path, so the deeper capture describes that same path. What no verdict
compares, sensorium prints beneath every one of them: argument and return
values, per-line state, timing, the order threads ran in relative to each
other — and the recorder's own footprint, which is structural and
unfixable. Deeper capture runs the program's __repr__ inside hooks that
suppress themselves, so an instrument that perturbs the program it is watching
leaves no mark on the fingerprint at all. Comparing the two runs' captured
output is the only cross-check available, and a side effect that prints
nothing is invisible to that too.
Beside the verdict, refocus prints a licence — verified against <run> on exactly these points, and no others, followed by the list of checks that
actually ran and agreed (source files unchanged by content, environment
variables compared, threads started, children witnessed). It is a bounded
enumeration of what was verified, not a summary judgment. Any check that
could not run withholds the licence too, with the reason itemised, because
"no git repository, so I could not tell" is a fact about the check and not
evidence that nothing moved. Two things it will not call a change are the
tool's own: a target directory that merely moved, and the recorder's own
--extern sensorium_rt=… fragment inside RUSTDOCFLAGS, whose hash moves
with every driver build — the first re-rooted before the compare, the
second removed from it, and both named on the line. A third,
session set 1 (the handles a shell, terminal or agent session hands a
process, listed by name in docs/query.md), is counted and named without
withholding, while every other differing variable withholds exactly as
before. The verdict and the licence are both stamped into the new trace, so
info and runs keep saying so long after the output has scrolled away.
DIVERGED is not a failure of the tool. For a program whose control flow depends on state outside the process, DIVERGED is the correct answer; the new trace is still recorded and queryable, and permanently labelled.
tree — derived parentage, and what a task group claims
A parent link is the caller frame, verified by code identity, never
"the last frame opened on this thread" — coroutines resumed by the event
loop, generators resumed by their consumer and callbacks from C all break
that assumption, and v1 made it. From trace format 3 a generator or
coroutine body opens a frame like any other, so NULL now means only that
the caller was never traced (the event loop, a library) or that it started
running before recording began — not, as on an older trace, that it was a
generator or coroutine tree could not frame. A trace recorded by a
format-1 sensorium is labelled parentage: ASSUMED because its links were
the guess.
Every non-function frame is marked with its kind ([generator],
[coroutine], [async_generator]) and closes with the state
Trace.frame_state derived from its YIELD/RESUME rows — returned and
raised render exactly as a plain function's do, and the suspension states
each name where the frame stopped and why: cancelled, abandoned, unwound by
some other exception thrown in, or still suspended at the end of the
recording. A root frame whose caller is named but not framed shows that
caller's name and, from format 3, the reason is always the same one —
(no frame: started before recording) — never the arc-1 (unframed)
reading, which is kept byte-for-byte on older traces because it names a
limitation this version no longer has.
Grouping by task is a statement about causality within a task (one
task is sequential) and says nothing about order between tasks beyond
wall-clock event ids; the footer says so. Task identity is a serial
minted per task object, not the task's name — two tasks named alike do
not merge — and is NULL for everything that did not run inside an
asyncio task (code before/after the loop, and loop callbacks such as
call_soon/add_done_callback).
exceptions, watch and flow — in full, one file away
The three commands whose claims need the most saying are
docs/query.md, moved there 2026-09-06 so this file stays
under 800 lines, wording and order unchanged — and, since 2026-09-07, that
file also holds a fourth section, refocus on a Rust trace, added there
rather than moved. What each of the three claims, in one paragraph:
exceptions classifies every raise as swallowed, uncaught,
re-raised, propagated or ambiguous, and SWALLOWED is claimed only when
the recording establishes it — anything short of that is ambiguous with
the reason printed. On a Rust trace it prints one block per SHAPE rather than
one per chain, and it refuses outright on a trace whose recorder declares
err_flow: false. docs/query.md carries the five dispositions, the Rust
disposition rules, the grouping key and what was measured about it.
watch evaluates a restricted predicate at every recorded site of the
named code — a CALL's arguments, a LINE's deltas — and prints a tally that
accounts for all of them: sites, evaluated, hits, not-captured,
errors. Zero hits never reads as "the invariant held": a site the
predicate could not be evaluated at is counted with its reason, and a run
where nothing could be checked says NOTHING WAS CHECKED instead of
hits: 0.
flow follows a captured value by equality (--value) or one object by
address-plus-type (--object) through calls and returns. It is lineage over
captured values, not static dataflow analysis, and the command says so in
its own header; --object is corroborated rather than asserted, because
CPython recycles addresses.
info, runs, and the state of the recording itself
infoprintsrecorder,lang, and acapabilitiesline — the recorder's own declaration when the trace carries one, andundeclared (pre-format-4 Python recorder; read as full by every command)when it does not, because reading an absent declaration as full is what a command does to decide whether to refuse, not something the trace ever said; from trace format 4, a bookkeeping field a trace's declared capabilities say should exist but does not is printed as the recorder's own declaration of that gap — never as a printed0, and never as the pre-format-4 "predates that bookkeeping" wording, which is kept for traces that actually predate it.Truncated captures are marked where they appear — a clipped string ends
~, a sampled container ends, ...— and counted ininfo.A run whose recording died is labelled INCOMPLETE, in
infoand in therunslisting both — its causal stream can stop anywhere without saying so.Writes dropped under load are reported as a lower bound (
>=N): writes that arrive after the count was taken cannot be counted either.Subprocesses that were noticed are listed as unwitnessed, never silently ignored — and an empty list is not evidence that none ran. A child that can only be counted and not named (a
multiprocessingspawn, which reaches the OS without going throughsubprocess) is reported as a count on Python 3.14+, where the underlying syscall raises an audit event; on 3.12 and 3.13 that event does not exist, so such a spawn is unwitnessed entirely (which is whyrefocuswithholds the "no child witnessed" licence there). Counted too are the threads a run started and any malfunction of the hook. None of these is printed when it is zero: a printed0would read as proof nothing was started, which is exactly what it is not.
What a trace file holds
A trace is one SQLite file under $SENSORIUM_DIR/traces (default
~/.sensorium/traces), created 0600 in a 0700 directory from 0.15.0
on — 0644 under your umask before that. It holds:
the process environment at record time, variable by variable, minus what rule v1 withheld: a secret-named variable is stored as
<redacted>beside an HMAC under a key that never leaves the store, sorefocuscan still say whether it changed;infonames what it took;everything the program wrote to stdout and stderr, interleaved with the events it wrote them between;
the command line, the working directory, the git commit, and content digests of every source file the run traced;
captured argument, return and local values, clipped to the caps
infoprints and under that same rule by NAME, with any secret-shaped SPAN inside a stored text replaced where it stands (docs/redaction.md);which asyncio task each event ran in, and the tasks' names;
one causal fingerprint per thread (events outside any task) and per asyncio task.
The file layout is trace format 4; docs/TRACE-FORMAT.md is the contract, with
conformance vectors under docs/trace-format/vectors/. A Rust trace holds the
environment, command line, source digests and captured Debug values the same
way; its tasks are libtest tests and spawned threads, its output is declared
absent, and its spool holds the same until conversion (rust/README.md).
info refuses to print the environment and refocus the variables it
compared: both carry secrets. What rule v1 does not reach is stored as it
was — a secret in a variable named x or a bare key, one no content
pattern knows, the command line, a token split across two write()s,
anything under SENSORIUM_NO_REDACT, every mode bit on Windows, and every
trace already on disk until sensorium redact runs over it. Treat a trace as
you would a core dump or a .env: sharing one shares all of the above, and
SENSORIUM_DIR is the only control over where it lands.
What sensorium sees at all
Code that this run traced — for the Python recorder, Python code in files under the run's own root; for the Rust recorder, the workspace's own crates; for the TypeScript recorder, the eligible ES modules under the invocation's root. Nothing else. On a Python trace, no command here says anything about:
any child process, by any mechanism;
any thread not started through Python's own
threading/_thread;any file the program read or wrote — only source files are hashed, so config, fixtures, databases and inputs move unseen;
any code outside the run's root: the stdlib, site-packages, installed dependencies,
PYTHONPATHmodules, and whatever--include/--excludefiltered out;the environment beyond the variables a command names as compared;
the clock, the network, and everything else the machine did.
This is stated as a category rather than as a list of mechanisms on purpose.
Five review rounds of refocus each found a mechanism the tool could not see;
an enumeration that looks complete is more dangerous than no enumeration,
because a reader who checks the list concludes their case was covered. The
Rust and TypeScript recorders draw their boundaries the same way, in
rust/HONESTY.md and
typescript/HONESTY.md with their blind-spot files
beside them.
Overhead
Recording overhead and read-back cost, measured on this machine with python corpus/run_corpus.py --bench and reported table by
table, are docs/overhead.md, moved there 2026-09-16 so this file stays under 800 lines, wording unchanged.
The headline: 2.9× on the corpus's typical workloads, up to 194× on the call-dense one under --focus; record failing tests, never benchmarks.
Corpus
python corpus/run_corpus.py # verify against seeded bugs
python corpus/run_corpus.py --show # print the questions and commands
python corpus/run_corpus.py --bench # report recording overhead
python corpus/run_corpus.py --require-driver # a skipped Rust or TypeScript case is exit 1
python corpus/run_corpus.py --via mcp --compare-cli # every tool question through the MCP server, diffed against the CLISmall programs with deliberately planted bugs, and questions registered
before any output was looked at: the question in plain language, the known
ground truth, the exact invocation expected to yield it, and why a print()
cannot answer it. Ground truth is known because the bugs were planted. This is
the regression suite, and it includes the honesty cases — the ones whose
pinned answer is a REFUSAL. 115 cases and 253 questions: twenty-two Python
programs with forty-eight questions, forty-five Rust cases and forty-eight
TypeScript cases, thirteen of the last recorded under a --focus. All of them
case by case in docs/corpus.md, moved there 2026-09-09 so this
file stays under 800 lines, wording unchanged. A case whose recorder is not built is
reported skipped BY NAME and counted apart from the passes, never as them;
--require-driver turns such a skip into exit 1, which is what CI passes.
--bench reports; it never gates. Overhead is a tracked fact about a machine
and a workload, not a pass/fail property of the tool.
Rust
cargo sensorium test/cargo sensorium run record a Rust workspace's own
crates the same way sensorium run records a Python program: one sensorium
trace per process, trace format 4, read by the same sensorium command line.
rust/ ships sensorium-rt 0.7.0 (zero dependencies, the runtime linked into
every instrumented unit, and the owner of the one sha256 the other two hash
with), sensorium-transform 0.5.0 (the syn rewriter), and cargo-sensorium 0.8.0 (driver, workspace wrapper, target runner, converter — one binary, four
roles). What it does and does not see is rust/HONESTY.md
with rust/HONESTY-BLIND-SPOTS.md;
rust/README.md is the full build/install/record reference.
What the licence is worth on a real workspace, measured. refocus over
61 #[test] pairs of a workspace nobody wrote this recorder for was
re-measured 2026-09-08 as E4″
(docs/superpowers/acceptance/2026-09-08-sensorium-rung4-e4pp.md,
pre-registered and byte-locked before the instrument existed): H1–H7 PASS,
H8 a STOP on a cell of that record's own reader. The licence is granted on
57 of the 61 and withheld on the four tests that really do start threads,
and the recorder's own RUSTDOCFLAGS fragment is out of the compare on 61
of 61 — under a driver build different from the originals', which is the
only condition that tests the second claim at all. The eighth row missed
because the reader compared a bare case name against a listing that spells
Rust cases rust/<name>; its three commands came back green, the STOP stands
as measured, and the one-line fix is ruled for the next slice.
Which versions, and when. E4″ ran on 2026-09-08 under
cargo-sensorium 0.5.2, sensorium-transform 0.4.3 and
sensorium-rt 0.4.0 — main as it stood that day — and its answers were
read by Python 0.8.6. The 61 originals were recorded earlier, by
cargo-sensorium 0.5.0: that difference between the recording driver and
the re-running one is the CONDITION the second claim needs, not an accident of
bookkeeping. The crate numbers at the top of this section are today's
(0.5.0 / 0.5.0 / 0.6.0) and Python 0.13.0 reads these traces now. All
four moved after the measurement — for the sha256 consolidation, then for the
LINE row's unbound — and none is a version that produced a number above.
Install and record
cd rust && cargo build --release
cargo install --path rust/cargo-sensorium # from the repository root
cargo sensorium test [--tier off|call] <cargo test args…>
cargo sensorium run [--tier off|call] <cargo run args…>Cargo stays the runner and the builder; sensorium only changes what gets
compiled and watches what comes out. --tier off compiles the same
artifacts and gates emission at runtime, so switching tiers rebuilds
nothing. Traces land beside the Python ones, in $SENSORIUM_DIR/traces/.
What it answers
Tier call — the only tier this version ships, and the default — records
CALL and RETURN with an outcome (ok/err/panic/none) and a captured
Debug return value, panics, per-thread MAP_SHARED spools, and libtest
tests plus spawn_child-named worker threads as tasks. From 0.3.0 it also
records err flow: a RAISE at every ? on a Result, a HANDLED at each of
the four written sinks (.ok(), .unwrap_or(..), .unwrap_or_else(..),
.unwrap_or_default()), at let _ = <value>, and at every Err(..) => arm
or if let Err(..) body, classified by what its body does. runs, info,
tree, frame, grep, diff — including diff --ignore-moves across a
refactor — and now exceptions all answer on a Rust trace; info adds the
toolchain, per-unit instrumentation counts, child runs, live threads at exit,
and the ? sites the transformer could not reach (partial fns: N), which it
declares rather than losing.
What exceptions was measured to be worth. On the bloomery clone's
--lib suite it printed 15 SWALLOWED lines and one was a false accusation
— an Err(e) => arm whose format! product is the value the function
returns — so the rung's pre-registered endpoint read STOP, the rule was
amended, and the repair was re-measured rather than declared:
0 false accusations of 14, on two selectors and under both readings of the
endpoint. Both records stand and both are worth reading before quoting any of
it: docs/superpowers/acceptance/2026-09-04-sensorium-rung3-acceptance.md
(§4, §5.1) and
docs/superpowers/acceptance/2026-09-05-sensorium-rung3-e6ppp.md (§4, §5).
One gap, measured, then fixed. diff --ignore-moves pairs code objects
correctly across a file split (28/28 paired, 0 added, 0 removed, in the
acceptance run's own split of a real file), but a spawned worker thread's task
NAME embedded its spawn site (<parent task> :: spawn@<file>:<line>), so moving
that call site during the same split renamed the task and the comparison read
DIVERGED even though nothing about the program's behaviour changed. Measured on
bloomery's own registry.rs split — four spawned-task names moved, their stream
hashes identical pairwise on both sides:
docs/superpowers/acceptance/2026-09-02-sensorium-rung2-acceptance.md §3–§4,
endpoint E5. Fixed 2026-09-03 (rung-3 entry decision, Brice's ruling (b)): a
spawned task is now named <parent task> :: spawn@<qualname>#<k> — the
enclosing named item's file-local qualname plus a source-order ordinal among its
wrapped spawn sites, neither of which a file move changes
(docs/superpowers/plans/2026-09-03-sensorium-rung3-entry-spawn-names.md,
decisions N1–N6; rust/HONESTY.md §3). Verified by E5′ on the endpoint the fix
exists for, which reads PASS (§4): across that same split, 28 code objects
pair and all ten task streams pair, where rung 2 read DIVERGED; the four spawned
children carry byte-identical names on both sides (E5′-names' first conjunct, 8
of 8 exactly the predicted string). E5′-coverage reads PASS (0 units
fell back). The record's overall line is STOP, on a third endpoint,
E5′-names, whose second conjunct asked that the multiset of (name, hash)
pairs be equal across the split while naming the trace's STORED hash as the
source — and that hash is defined over file, so a file move changes it by
construction. That is a defect in the pre-registration rather than in the naming
rule; it was read once, no repair was applied after the number, and it was
ruled 2026-09-04: (b) withdrawn; see the record §5.1. Read §4 and §5.1
before citing any of this:
docs/superpowers/acceptance/2026-09-03-sensorium-rung3-entry-e5prime.md.
Per-line answers, under --focus
cargo sensorium --focus <qualname> … (repeatable, and a container value
selects its children on the :: boundary) splices a probe after every
statement of each function it names, so the trace carries one LINE event per
completed statement with the bindings that statement wrote. That is what
makes watch and flow answer on a Rust trace: watch last --at fill --expr 'b == 2' settles the predicate at the statement that wrote b rather than at
the function, and flow last --value 3 follows the value through LINE deltas
as well as returns. --at Counter selects Counter::new on the same
boundary rule Pot uses for Pot.add, so it never quietly answers about
Counters::new. What the tier does not reach — closure and async
bodies, place writes, macro bodies, and every function no focus named — is
rust/HONESTY-BLIND-SPOTS.md item 3, narrowed to exactly that list.
From sensorium-rt 0.5.0, a block-like statement's LINE row also lists the
names it unbound, so watch's fold does not read one as still in scope once
its block has closed.
A Rust capture is Debug text, not a typed value, so the reading rule is
written down: a literal is compared against its Debug rendering (5, 2.5,
true, None, and a string WITH its quotes), a truncated capture never
matches, and len() over one is NOTHING WAS CHECKED rather than a
comparison to nothing. watch --expr and flow --value are inverses on that
domain — what one calls a sighting the other cannot deny at the same site.
x == 5 compared text, and docs/TRACE-FORMAT.md's LINE row says so where
a reader meets it.
What refuses
watch and flow refuse on an UNFOCUSED trace — never answering from a
capability the recorder declares it does not have. They print why and exit
3 — the recording, not the call, is what would have to change — because
both need capabilities.line, which a build with no --focus declares
false.
refocus answers from cargo-sensorium 0.5.0: capabilities.refocus is
true for every trace that driver converts, and whether one PARTICULAR
recording can be re-run is a refusal about that run, not about the recorder.
Five such refusals exit 2 with nothing was re-run in them — --window,
which the Rust runtime has no per-activation check for; a run that is one of
n processes of its invocation, so no single trace is the answer; a trace
recording no workspace_root; a workspace that has since moved; and no
cargo-sensorium to re-run with. What the verdict then claims, and the two
licence checks it cannot run at all, is docs/query.md.
exceptions answers from 0.3.0, and refuses on exactly one thing: a
trace an older runtime wrote. The gate is capabilities.err_flow, so such a
trace exits 3 naming the recorder and the capability, and no rule ever
sees its records — what it lacks is a record, not a rule. Program output under
libtest and per-line state in an unfocused build are the remaining "not yet"s:
declared absent in the trace, never silently missing.
Cost, beside Python's
Measured on this same box
(docs/superpowers/acceptance/2026-09-02-sensorium-rung2-acceptance.md §3.1):
cargo test -p bloomery-daemon --lib plain against
cargo sensorium test -p bloomery-daemon --lib at tier call, n=5 each,
binaries pre-built — 0.058 s plain, 0.125 s call, ×2.1552, conversion
INSIDE the timed command. That is not comparable to rung 1's ×1.0103,
which timed the whole 8.25 s suite with conversion outside it (§5.3), and it
is not comparable to Python's 4–9 µs/event above either: a whole-suite wall
ratio and a per-event figure are not the same unit, and no section here
states one as a translation of the other. The driver's own fixed cost is
0.073 s (n=5, no-op --tier off --no-run against straight cargo).
TypeScript
sensorium ts run -- vitest run records a TypeScript or JavaScript test suite the
way cargo sensorium test records a Rust workspace: one trace per test-file
process, trace format 4, read by the same sensorium command line. typescript/
ships sensorium-ts 0.6.0 — a transform whose edits never contain a newline,
a runtime on AsyncLocalStorage, a vitest plugin, a node --test hook — with
driver and converter in Python, so reading a trace needs no Node. What it sees and
does not is typescript/HONESTY.md with its blind-spot
file; typescript/README.md is the full reference.
npm ci --prefix typescript
npm --prefix typescript run check # type-check
sensorium ts run [--tier off|call] -- vitest run # or: -- node --test src/
sensorium ts run --focus diceQueue.ts:parseDiceGroups -- npx vitest run src/libEverything after -- is yours, spawned as typed, and the driver exits with the
harness's status. Tier call records calls and returns with a captured value,
YIELD/RESUME at every await/yield, RAISE at every throw, HANDLED at every
catch, and tests as tasks named as vitest names them, so tree groups by
test and diff compares a test against itself. exceptions answers — the same
five dispositions, from a how word the transform decides from each handler's
syntax, merged across a whole invocation — while a trace an 0.1.x runtime wrote
refuses at exit 3 on err_flow: false. flow --object answers on any recording
from 0.3.0 on, focused or not, and answers exactly: identity is a per-object
serial minted once and never reused, so the footer reads continuity: exact (serial identity). refocus re-runs the whole recorded invocation and pairs by test file
(docs/query.md); package scripts, jest, a project with no typescript of its own
and a vitest projects/workspace config are refused by name.
From sensorium-ts 0.4.0, a RETURN row follows the rows of any finally the
return passed through.
Per-statement answers, under --focus
--focus <spec> (repeatable, <qualname> or <file>:<qualname>, and a
container value selects its members on the . boundary) is resolved against
your own sources with your own TypeScript before anything is spawned: a
spec that names nothing is refused at exit 2 with the closest eligible
qualnames, and one that names only functions this recorder does not
instrument is refused with the reason and its count. What it selects, the
transform splices a probe into — one LINE event per completed statement,
its deltas the bindings that statement wrote, a guarded body's head names as
a synthetic first row at each entry, and the block-scoped names a block-like
statement declared listed unbound on its own row, which is the key watch's
fold pops. The focused function's CALL carries its arguments; an unfocused
one in the same recording still reads helper() <unread: locals>. So watch
and flow --value answer on a focused run and refuse at exit 3 on an unfocused
one, naming the recorder the trace itself carries; frame answers either way,
saying timeline: not captured (record again with …) at exit 0.
A TypeScript capture is node's util.inspect text, so the reading rule is
written down, and it is Rust's opposite in the place a reader meets first: flow --value 5.0 sights a JavaScript 5, because JavaScript has one number type.
null, undefined, true and false are predicate constants in every
language; a string is spelled with its quotes, and one past 100 characters was
cut by the formatter, so it matches nothing rather than matching a prefix. Every
spelling is generated rather than argued — 41 measured rows in
typescript/test/fixtures/inspect-table.json — and
docs/trace-format/TYPESCRIPT-KEYS.md §
Under a focus is the reading. What the tier does not reach — place writes,
this, a conditional assignment's write-or-not, a switch discriminant, a
nested function no spec's prefix reached — is typescript/HONESTY-BLIND-SPOTS.md
items 28–35 and 39, each a present row with a stated hole.
What four rungs measured
One lens under nearly every number, and it is somebody else's code: a
tabletop VTT frontend at 0091e97 — 372 test files, 4,278 tests — under
vitest 4.1.9, node v24.16.0, 16 cores. Each rung was pre-registered and
byte-locked before its own code existed. Two things none of it licenses: no
endpoint says a TypeScript trace answered a debugging question nobody
planted, and none was measured on a second consumer. Every figure, and what
each rung left open, is in docs/superpowers/acceptance/ and in
typescript/README.md.
Rungs 1–3. DONE-WITH-STOP, then DONE, then DONE: 372 containers of one test file each and 5,378 of 5,378 eligible sites instrumented,
call/plain1.1324 withE6′STOPping on one timing clause of four;exceptionsat 0 false SWALLOWED of 30 hand-adjudicated shapes; the catch-all blocks naming a reason at 0 false names of 20.Rung 4 — DONE-WITH-STOP, the focus tier, on 830 files of that same frontend. H3 is the endpoint the rung exists for and it PASSed on the first reading: nine LINE rows for one
parseDiceGroups('1d20')activation, their lines, every delta name and the oneunboundlist row for row against a hand count sha256-locked before the code existed, empty diff. H1 4/4, H6 4/4, H8 6/6, H7 reported at ×2.3816 plus 1.132 s of resolution. H2, H4 and H5 STOP — H2 because three typed specs select six sites (a container's spec reaches what is nested in it, which the pre-registration counted by names typed), H4 and H5 because the reading INSTRUMENT could not tell a loop's head row from its completion row nor see a CALL sighting its own transcript prints. All three were found after their numbers, so all three stand as findings, and the next slice re-registers them.
Not yet
What each slice deferred, and the ruling each deferral is waiting on, is
docs/CARRIED-DEBT.md — appended at every merge,
resolved items struck through rather than deleted.
Subprocess following, attach-to-live-server flight recording, native (rr)
substrates. See
docs/superpowers/specs/2026-08-21-sensorium-arc2-inspectable-coroutines-design.md
(extends 2026-08-21-sensorium-async-design.md, arc 1's spec).
License
MIT — see LICENSE. Copyright © 2026 Brice Lancaster.
Versions after the Proprietary interlude are again under MIT (matching releases through 0.14.0).
Available Tools
9 toolsdiffdiffBRead-onlyIdempotent
first causal divergence between two runs. Aligns the two causal streams and names the first event where they part, with drill-in commands; MATCH or DIVERGED, or REFUSED when no verdict exists.
| Name | Required | Description | Default |
|---|---|---|---|
| task | No | compare one recorded task's stream by name instead of the thread streams; what a task IS is the recorder's own, and `info` names the ones a run has | |
| run_a | Yes | the first run: run id, a unique prefix of one, or `last` (the newest trace) | |
| run_b | Yes | the second run: run id, a unique prefix of one, or `last` (the newest trace) | |
| context | No | common causal steps to show before a divergence | |
| ignore_moves | No | pair a function that left one file with the same-named function that appeared in another, then compare; the pairing is printed with the verdict |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, covering the safety profile. The description adds the REFUSED outcome and mentions drill-in commands, but doesn't disclose more about side effects or error behavior. Given the annotations, this is adequate but not rich.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise, with two sentences that front-load the purpose and then add details on output and behavior. It avoids fluff, though the first sentence is a fragment and the phrasing is somewhat jargon-heavy, preventing a perfect score.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With five parameters, two required, and no output schema, the description should clarify the return format more thoroughly. It gives verdicts (MATCH, DIVERGED, REFUSED) and says it 'names the first event', but doesn't describe the event structure or the drill-in commands. The schema covers parameters, so this gap is in the output semantics, making it incomplete but not severely lacking.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema provides 100% description coverage for all five parameters, so the schema already documents them. The description doesn't add extra meaning to parameters beyond referencing 'two runs' (run_a, run_b), so it remains at the baseline of 3.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states the tool finds the 'first causal divergence between two runs' and explains it aligns streams and names the first event. This is a specific function distinct from siblings like 'runs' or 'tree' which are not comparative. However, it doesn't explicitly contrast with alternatives, so it stops short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies the tool is for comparing two runs, but it doesn't explicitly state when to use it versus other tools or list exclusions. It mentions 'REFUSED when no verdict exists', hinting at a condition, but that's not enough to guide tool selection clearly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
exceptionsexceptionsARead-onlyIdempotent
raises, handles, swallows. Every raise classified swallowed, uncaught, re-raised, propagated or ambiguous, with the reason; SWALLOWED is claimed only when the recording proves it.
| Name | Required | Description | Default |
|---|---|---|---|
| run | No | a run id (or a unique prefix), 'last', or an invocation id as `runs` prints it; --after is refused for an invocation | last |
| after | No | event ref to resume from | |
| limit | No | most rows to print |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already convey read-only, idempotent, and non-destructive behavior, so the description adds meaningful context by stating that SWALLOWED is claimed only when the recording proves it. This reveals an evidence-based classification policy and a conservative bias in the tool's output.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two short sentences with no filler. The core actions are front-loaded, and the evidence rule about SWALLOWED earns its place by conditioning how agents should interpret that category.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only inspection tool with fully documented parameters and safety annotations, the description adequately conveys what the tool returns: classified raises with reasons. It does not detail output formatting or ordering, and it lacks sibling routing, so it is not fully complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the parameters are already documented in the schema. The description adds no additional parameter-level meaning, such as format or interaction between run, after, and limit, so the baseline score of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description specifies that the tool classifies exception raises into categories (swallowed, uncaught, re-raised, propagated, ambiguous) with reasons, which is a specific verb+resource pairing. It does not explicitly differentiate itself from sibling tools like flow or info, though the exception-classification focus is fairly distinctive.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no guidance on when to use this tool versus alternatives. It does not mention sibling tools, preconditions such as an active run, or scenarios where another tool would be more appropriate; the intended use is only weakly implied by the name and description.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
flowflowARead-onlyIdempotent
provenance of a value or an object. --value follows a literal by equality through captured arguments, locals and returns; --object follows one identity from the event that captured it; one of the two is required, and each refuses by name what the recording did not capture.
| Name | Required | Description | Default |
|---|---|---|---|
| run | No | run id, a unique prefix of one, or `last` (the newest trace) | last |
| after | No | event ref to resume from | |
| limit | No | most rows to print | |
| value | No | literal matched by equality; quote to force a string, as in "'1800'" | |
| object | No | e<id>:<name>, where <name> was captured at that event (a CALL's argument, a LINE's local); or <qualname>:<name>, which resolves to that function's first CALL and so names one of its ARGUMENTS -- plus <qualname>:return for what that activation returned |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the annotations (readOnlyHint=true, idempotentHint=true), the description adds genuine behavioral context: it follows through captured arguments/locals/returns, resolves object names against the event that captured them, and 'refuses by name what the recording did not capture,' which tells an agent how the tool fails on uncaptured identifiers. This is informative failure-mode disclosure that the annotations do not provide.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two dense sentences with the core purpose front-loaded and every clause earning its place. The telegraphic style ('provenance of a value') is efficient, though slightly cryptic for a first-time caller, which keeps it from a 5.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 5-parameter tool with two exclusive modes and no output schema, the description covers the critical disambiguation between modes and the mandatory-selection rule. The main gap is that it never hints at what the provenance output looks like, which matters more given there is no output schema to fill that void.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds meaning the schema lacks: the semantic difference between equality-tracking (`--value`) and identity-tracking (`--object`), the mutual exclusivity requirement, and the refusal behavior for uncaptured names. It connects the parameters to tool behavior rather than merely restating formats.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a clear purpose — tracing the provenance of a value or object — and elaborates two distinct modes (`--value` by equality, `--object` by identity), which is specific enough to separate it from the sibling set. It stops short of 5 because it never explicitly names or contrasts any sibling tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage context is implied through the behavior described: equality-following vs identity-following, plus the explicit constraint that one of `--value`/`--object` is required. However, there is no statement of when to prefer `flow` over siblings like `grep`, `watch`, or `tree`, and no exclusions or alternative routing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
frameframeARead-onlyIdempotent
one activation in full. Arguments, return or raise, and the per-line locals timeline when the run was recorded with --focus; frame ids fN come from exceptions, tree and grep.
| Name | Required | Description | Default |
|---|---|---|---|
| fn | No | qualname of the function: exact match first, else substring | |
| nth | No | which activation (1-based) | |
| run | No | run id, a unique prefix of one, or `last` (the newest trace) | last |
| frame | No | frame ref (f12) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint and idempotentHint, so the description correctly does not restate those. It adds useful behavioral context beyond annotations: the per-line locals timeline is only available when the run was recorded with `--focus`, and the tool reports either a return value or a raise.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is short and front-loaded with the core purpose ('one activation in full'), and the rest of the sentence packs in the key behavioral condition without filler. It is slightly telegraphic but every phrase earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description does explain what the tool returns: arguments, return/raise, and the conditional locals timeline. It also tells the agent where frame ids originate. It could be more explicit about the two addressing modes (`fn`/`nth` vs `frame`), but overall it is adequate for selecting and invoking the tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the core parameter meanings are already in the schema. The description adds a small amount of context by clarifying that frame ids come from sibling tools, but it does not explain the relationship between `fn`/`nth` and `frame`. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description identifies the resource ('one activation in full') and the key output contents: arguments, return/raise, and per-line locals timeline. It lacks an explicit verb like 'show' or 'return', so it is not quite a 5, but it is clear enough to distinguish the tool's purpose from siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies when to use the tool by stating that frame ids fN come from `exceptions`, `tree`, and `grep`, which tells an agent where to obtain a valid frame reference. It does not explicitly state when to prefer this tool over alternatives or when not to use it, so guidance remains implicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
grepgrepARead-onlyIdempotent
search events by name or value. Every CALL, RETURN, RAISE, HANDLED or LINE event whose name or rendered value contains the pattern; --after resumes from an event id a previous answer showed.
| Name | Required | Description | Default |
|---|---|---|---|
| fn | No | qualname filter: exact match first, else substring | |
| run | No | run id, a unique prefix of one, or `last` (the newest trace) | last |
| kind | No | keep events of this kind only | |
| after | No | event ref to resume from | |
| limit | No | most rows to print | |
| pattern | Yes | substring matched against event names and rendered values |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already provide readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the tool is non-mutating and safe. The description adds context about the matching behavior (substring matching on names and rendered values) and the resume capability, which is not in the annotations. It does not contradict annotations, and the behavioral coverage is strong.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, focused sentence that states the primary function, the matching criteria, and the important resume capability. It is front-loaded with the action and purpose, with no wasted words. It is slightly dense but not overlong, earning a 4 rather than a 5 due to the use of a semicolon that could be simplified.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool is a read-only search with a rich schema that covers all parameters. The description covers the matching semantics and the resume feature, which is the key non-obvious behavior. Given the annotations and schema, an agent has everything needed to call it correctly. No output schema is needed as the description implies event rows are returned. It is complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all parameters are described in the schema (e.g., 'pattern', 'fn', 'run', 'kind', 'after', 'limit'). The description adds value by clarifying that 'pattern' is a substring and lists the event kinds, but it duplicates schema info. It does not add new meaning beyond the schema, so a baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states a specific verb ('search') and the resource ('events'), and specifies exactly which events match ('CALL, RETURN, RAISE, HANDLED or LINE event whose name or rendered value contains the pattern'). It distinguishes itself from siblings like 'tree' or 'flow' by focusing on pattern-based search across event types, making the tool's purpose unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly mentions the `--after` option and its use case ('resumes from an event id a previous answer showed'), indicating when to use it for continuing a previous exploration. It also implies this is the primary search tool for events, distinct from siblings like 'runs' (listing runs) or 'info' (details). This is clear enough for an agent to know when to select it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
infoinfoARead-onlyIdempotent
summarize one trace. Recorder, language, the capabilities the trace declares, what was recorded and what was not, focus, the redaction stamp; read it before any other question on a run.
| Name | Required | Description | Default |
|---|---|---|---|
| run | No | run id, a unique prefix of one, or `last` (the newest trace) | last |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, covering the safety profile. The description adds value by specifying what the summary includes (recorder, language, capabilities, etc.), which is not in annotations. It does not disclose any surprising side effects, but none are expected given the annotations. It could mention that it reads the most recent trace by default, but that is covered in the schema default.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single sentence that front-loads the core purpose ('summarize one trace') and then lists the key components concisely. There is no fluff or redundancy. It is efficient and well-structured.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description's list of what the summary includes (recorder, language, capabilities, etc.) effectively informs the agent of the return content. The tool is simple with one optional parameter, and the description covers its purpose and usage. It doesn't explain the 'redaction stamp' term, but that is domain-specific and not necessary for invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema describes the single parameter 'run' fully (run id, unique prefix, or 'last'). The description does not add any additional semantics about this parameter. Since schema coverage is 100%, the baseline of 3 is appropriate; the description does not need to compensate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's function: 'summarize one trace' and lists the specific content included (recorder, language, capabilities, etc.). It is not a tautology and provides a distinct purpose. It doesn't explicitly differentiate from sibling tools like 'runs' or 'tree', but the 'summarize' verb and the list of fields make its role clear.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description includes a usage directive: 'read it before any other question on a run.' This tells the agent when to use this tool first, implying it's a starting point. It does not explicitly mention alternatives or when not to use it, but the 'before any other question' guidance provides clear context.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
runsrunsARead-onlyIdempotent
list recorded traces. One line per trace, oldest first: run id, exit, event count, the command; a refocus rerun names its original and its verdict.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is clear. The description adds useful behavioral context: output is one line per trace, oldest first, and includes specific fields (run id, exit, event count, command). It also explains the 'refocus rerun' naming behavior, which is beyond what annotations provide. However, it doesn't mention pagination, limits, or what happens with no traces.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single, information-dense sentence. It front-loads the core action ('list recorded traces') and then packs the output format and a special case (refocus rerun) into a compact structure. Every word earns its place; no filler or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a zero-parameter, read-only listing tool with strong annotations, the description is nearly complete. It tells the agent what the output looks like, the ordering, and the fields. The only minor gap is that it doesn't describe edge cases like empty results or whether the list is truncated, but given the tool's simplicity and annotation coverage, this is acceptable.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has zero parameters, and schema description coverage is 100% (the schema is empty). With no parameters to document, the description doesn't need to explain parameter semantics. The baseline for 0 params is 4, and the description doesn't introduce any confusion about parameters.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool lists recorded traces, with a specific verb ('list') and resource ('recorded traces'). It distinguishes itself from siblings like info, tree, frame, and flow by focusing on the run-level trace listing. However, it doesn't explicitly name a sibling alternative, so it loses a point for not differentiating from potential similar tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage: when you want to see recorded traces at a glance. It provides output format details (one line per trace, oldest first, fields) which helps an agent know what to expect. But it doesn't explicitly state when to use this vs alternatives like flow or info, nor does it mention any exclusions or prerequisites.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
treetreeARead-onlyIdempotent
call-tree slice. Parentage derived from the event stream; --around centres the slice on an event id, --root on a frame id; nested dict contents are elided, frame prints them.
| Name | Required | Description | Default |
|---|---|---|---|
| run | No | run id, a unique prefix of one, or `last` (the newest trace) | last |
| root | No | frame ref (f12) | |
| depth | No | levels of the call tree to print below the root | |
| limit | No | most rows to print | |
| around | No | event ref (e40) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare the tool read-only, idempotent, and non-destructive. The description adds useful behavioral context beyond annotations: parentage is derived from the event stream, nested dict contents are elided by default, and `frame` is the alternative for printing them. No behavior contradicts the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is one dense, purposeful sentence with no filler. It front-loads the core purpose, then conveys the two slicing modes and the key behavioral distinction from `frame` in a compact way.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has no output schema, the description covers the essential behavioral details: slice centering, depth default behavior implied by schema, and elision of nested dict contents. It could be slightly more complete by explicitly mentioning defaults like `run=last`, but those are fully documented in the schema, so nothing critical is missing for invoking the tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema already documents every parameter. The description adds meaning on top by clarifying the role of `around` versus `root` ('centres the slice' on an event id versus a frame id), which is not fully captured by the schema's terse 'event ref (e40)' and 'frame ref (f12)'.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description identifies the resource and scope clearly with 'call-tree slice' and explains the two slicing modes (`--around` on event id, `--root` on frame id). It stops short of a 5 because it lacks an explicit verb like 'prints' or 'shows', though the intended action is unambiguous and it distinguishes itself from the sibling `frame`.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives concrete context for when to use the tool's two main modes: `--around` for event-centered slices and `--root` for frame-centered slices. It also implies when the sibling `frame` is preferable ('nested dict contents are elided, `frame` prints them'), though it does not explicitly enumerate exclusions versus other siblings like `grep` or `flow`.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
watchwatchARead-onlyIdempotent
predicate over captured state. A gdb-style watchpoint evaluated after the fact at every recorded site of one function; needs a --focus recording, and NOTHING WAS CHECKED is a refusal, never a pass.
| Name | Required | Description | Default |
|---|---|---|---|
| at | Yes | module:qualname or qualname | |
| run | No | run id, a unique prefix of one, or `last` (the newest trace) | last |
| expr | Yes | names, literals, one comparison, and/or/not, arithmetic, len(name) | |
| after | No | event ref to resume from | |
| limit | No | most rows to print | |
| misses | No | how many near-misses to show when nothing hit |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish read-only, idempotent, and non-destructive behavior. The description adds non-obvious behavioral context: evaluation happens after the fact, covers every recorded site of one function, requires a focus recording, and treats "nothing checked" as a refusal rather than a pass. No contradiction with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact and front-loaded with the core concept. Each clause contributes: predicate semantics, evaluation timing and scope, the focus prerequisite, and result interpretation. The phrasing is dense and domain-specific but contains no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With all 6 parameters fully documented in the schema and annotations covering safety, the description covers the main unstated constraints: focus requirement, per-function scope, and the nothing-checked failure mode. An example or explicit output shape would improve completeness, but the core contract is sufficiently clear.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds meaning beyond the schema: "predicate" and "gdb-style watchpoint" clarify the role of expr, while "at every recorded site of one function" and "needs a `--focus` recording" clarify at/run semantics. The refusal rule also gives practical meaning to near-miss and no-hit behavior.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific resource and operation: evaluates a gdb-style watchpoint predicate over captured state at every recorded site of one function. It distinguishes the tool from a generic search or dump, though it does not explicitly compare against siblings like grep or exceptions.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives a clear prerequisite ("needs a `--focus` recording") and an interpretation rule ("NOTHING WAS CHECKED is a refusal, never a pass"). However, it never explains when to prefer this tool over alternatives such as grep, tree, flow, or exceptions, so usage selection is left mostly to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
9 tool updates
v0.18.1- First observed
diff - First observed
exceptions - First observed
flow - First observed
frame - First observed
grep - First observed
info - First observed
runs - First observed
tree - First observed
watch
TDQS
Scored across 9 tools
Each of the nine tools owns a distinct slice of the trace-analysis workflow: run listing, run summary, call-tree navigation, frame detail, event search, exception classification, value/object provenance, predicate evaluation, and run comparison. Even close neighbors like tree/frame and grep/exceptions are cleanly separated by granularity and purpose. An agent should rarely misselect.
All tool names are concise lowercase single-word commands, so the naming style is predictable and visually consistent. The only minor deviation is a mix of nouns/verb forms like info, tree, frame vs. grep, watch, diff, but there is no mixed casing or chaotic variation. This is a small style inconsistency rather than a real usability problem.
Nine tools is an ideal scope for a specialized trace-analysis server: broad enough to cover the main investigative workflows, yet small enough that every command has a clear role. It is comfortably within the well-scoped range and does not feel over- or under-built.
The tool surface covers the full analytical lifecycle: discovering runs, reading summaries, exploring call trees and frames, searching events, classifying exceptions, tracing value/object provenance, evaluating predicates, and diffing two runs. The tools explicitly reference each other's outputs such as frame ids and event ids, enabling iterative investigation without dead ends. A raw event timeline is absent, but grep, tree, and exceptions cover that need effectively.
Maintenance
Related MCP Connectors
Hosted MCP memory: save sessions/decisions once, search from Claude, Cursor, ChatGPT. EU-hosted FTS.
Private-by-default, local-first memory/context/task orchestrator for MCP apps and agents.
- memnodeOAuthdev.memnode
Persistent, inspectable memory for AI agents with lineage, correction, and a hosted MCP endpoint.
Self-hosted MCP server: 26 deterministic dev, security, and EVM tools.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceRecords runtime events from any app via zero-code instrumentation and exposes them to LLMs through MCP for autonomous debugging.8 npmMIT
- AlicenseNot gradedqualityCmaintenanceLocal-first code intelligence and safety layer for AI coding agents. MCP server exposes dependency graph, impact analysis, and AST-compressed repo context, backed by typed local memory, patch-scope safety gates, and git-independent transaction rollback.1MIT
- FlicenseNot gradedqualityAmaintenanceLocal MCP server that lets your AI coding agent query its own cross-tool project history - file/command freshness, past test failures, cost & token spend, cache status, and session handoff - over stdio, 100% local, no telemetry.46-
- AlicenseAqualityAmaintenanceRecords your terminal sessions per command (PTY + OSC 133) into local SQLite, so AI agents can search, retrieve, and diff what commands actually printed. Secret redaction is applied by default to everything served over MCP.46MIT