Skip to content

feat(web): Zorp mode runs one investigate attempt and shows the ledger - #82

Merged
adityak74 merged 1 commit into
mainfrom
feat/web-zorp-mode
Aug 22, 2026
Merged

feat(web): Zorp mode runs one investigate attempt and shows the ledger#82
adityak74 merged 1 commit into
mainfrom
feat/web-zorp-mode

Conversation

@adityak74

@adityak74 adityak74 commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

The browser gets a "Zorp mode" button. It runs one pre-registered investigate
attempt on the session, streams it on the event stream a turn already uses, and
reads back what landed in the aryabhatta ledger.

Three routes, all behind a new non-default research feature on zorp-web:
POST /api/sessions/:id/investigate, GET /api/investigate/status,
GET /api/investigate/ledger.

It is not a fifth capability, on purpose

The ask was to "call the aryabhatta engine". There is no aryabhatta engine.
aryabhatta is record plus readers, nine modules inside zorp-track, and it
ships no command on purpose. What writes to it is investigate.

So the faithful browser-facing shape is one investigate attempt plus a read.
Inventing an engine to sit in front of the record would have added the one
thing the design says not to add.

It mirrors panel

An attempt occupies the session exactly as a turn does, shares the session's
sequence counter, answers the existing stop endpoint, and closes with
investigate_done then done. That is the shape panel already established
for a non-turn operation driven from the browser. A second transport would
have given the page a third state machine to keep in step with the first two.

A person launches it and no model can

The same rule panel holds, for a sharper reason: an attempt writes to a
pre-registered evidence record and to the ledger
, so a model that could start
one could feed the record it is later read against.

No tool reaches the endpoint. agent.rs carries
no_builtin_tool_can_start_an_investigation, asserted against the unfiltered
builtin set rather than against the filter, and zorp-web carries a second
test over the set this server actually hands out.

Opt-in, and the routes exist either way

research on zorp-web pulls in zorp-track and DuckDB, the most expensive
build in the workspace, and the ordinary chat server has no use for it.

The routes are registered unconditionally and answer 501, not 404, without
the feature. A 404 cannot tell "this server does not do that" apart from "you
typed the URL wrong", and GET /api/investigate/status lets the page say which
one it is before anybody clicks.

Checkpoints are auto-approved, and the page says so

There is no terminal behind a browser, so CheckpointMode::terminal refuses
outright. This is the CLI's --yes, chosen explicitly rather than fallen back
into.

What it cannot do is skip the pre-registered Kill Threshold: a breach kills the
track unconditionally without consulting the checkpoint mode at all, so the
commitment still holds from the browser. What is missing is the human judgement
call on top of it, and that gap is recorded rather than hidden, because
checkpoint_mode is one of the conditions every attempt writes and it reads
auto-approve in the ledger.

Ruled out: a browser checkpoint decider on the existing approval card. It would
have meant an unanswered prompt killing a track after five minutes, which is a
product decision worth taking on purpose rather than acquiring as a side effect
of building this.

Forecasting is reported and never set from the browser

The status endpoint says whether ZORP_FORECAST is on where the server runs.
There is no control that turns it on: it costs a model call on every attempt,
and one page flipping it would change what the server does for everyone else
using it. Off stays the default, and the ledger view says out loud that an
empty expectations list is why nothing can be scored.

The ledger view names no model-authored column

Integrity rules 5 and 7 bind detectors and the search layer, not a page, but
the cheapest way to keep them checkable is for no read path anywhere to name
such a column. expectations.assumptions is not in the frame, and a test
asserts the serialized ledger never carries it.

Nothing read back is fed to a model either. The lines on the page are recorded
rows and arithmetic over them, the same split critique and the detectors
already use.

Tests

Rust, zorp-web/tests/investigate.rs: an attempt runs and lands in the ledger;
an attempt is refused while a turn is running; a request that cannot run is
refused without recording anything; an unknown session cannot be investigated;
the status endpoint reports forecasting and never sets it; reading a ledger
creates nothing.

Rust, zorp-agent/src/agent.rs: no_builtin_tool_can_start_an_investigation.

TypeScript, web/test/zorp-mode.test.ts: an approved attempt and a killed track
do not read the same; a missing run record and an empty ledger say different
things; the ledger shows the conditions an attempt ran under; an attempt with no
forecast says so rather than showing nothing; a recorded forecast shows its
interval and stated coverage; forecasting off is said plainly, because it is why
the ledger is empty; nothing rendered becomes markup.

Open

The ledger is read by question rather than browsed, so there is no way to see a
track you did not just run. Deliberate for now: a browser that can list every
track is a different feature with its own scope.

Rebased

Rebased onto origin/main twice, since #83 landed partway through. The branch
fast-forwards onto 195c8cc0 and nothing from #79, #80, #81 or #83 was dropped.

zorp-web/Cargo.toml was the real conflict: search (#81), recall (#83) and
research (this PR) now sit under one [features] block, and [dependencies]
carries zorp-recall, zorp-track and sha2 together. Also resolved:
zorp-web/src/api.rs (kept /api/capabilities and the three investigate
routes), web/src/api.ts (kept #81's getCapabilities and this PR's
bindings), web/src/main.ts (one import line, kept RecallStatus and
Preregistration), docs/DECISIONS.md (entries newest first), and
Cargo.lock (regenerated from main's rather than hand-merged, one line of
diff). The full account is in a comment below.

Why research-pr sat pending on the first push

Not stuck, and not failing to trigger. It ran and passed in 20m49s, finishing
after the other five. The cache restore was a partial hit rather than a full
one, because this PR moves zorp-web/Cargo.toml and Cargo.lock. The step
this PR adds, Test zorp-web (research feature), then spent about twelve and a
half minutes compiling libduckdb-sys from source: -p zorp-web --features research resolves a different feature set across the shared graph than
-p zorp-track does, so DuckDB is a different unit of work and the zorp-track
step's build does not cover it.

That left a real risk this PR introduced. On a genuinely cold cache the job
would pay a cold zorp-track build plus that new twelve and a half minutes,
and the workflow's own comment puts a cold DuckDB build at about 35 minutes,
which is over the old 45 minute cap. A job killed by the cap reads like a
broken test rather than a cold cache, so both research jobs now cap at 60
minutes with a comment saying why. Nothing about how they trigger changed.

Local verification

Run on the rebased head, f197b99:

command result
cargo fmt --all --check clean
cargo build --workspace --locked ok
cargo test --workspace --locked 1252 passed, 0 failed
cargo test -p zorp-agent --features research 669 passed, 0 failed
cargo test -p zorp-web --features research 178 passed, 0 failed
cargo test -p zorp-web -p zorp-recall --features zorp-web/recall --locked 191 passed, 0 failed
cargo clippy --workspace --exclude zorp-track --all-targets --locked -- -D warnings clean, no diagnostics
npm run check (from web/) clean
npm test (from web/) 203 passed, 0 failed
npm run build (from web/) ok

Three guard tests were mutation-checked, since a guard test that passes with
its guard deleted is worth nothing: the model-authored-column test fails when
assumptions is added to the frame, the no-tool test fails when a real tool
name is added to its list, and the no-create test fails when read_ledger's
existence check is bypassed.

Also smoke-tested in a browser against a scratch server on a throwaway
workspace: the panel opens, the status line reports forecasting off, the empty
question and missing pre-registration paths produce the right errors, the
ledger read reports no run record without creating one, and the console is
clean.

Provenance

The agent that wrote this branch pushed it and then stalled on a harness
watchdog before it could open the PR. The commit and the branch are its work,
and this description was assembled from the docs/DECISIONS.md entry it
committed. It has since returned, rebased, and reported the results above.

https://claude.ai/code/session_01KGPVQ8wUG7h36zashWYCp4

There is no aryabhatta engine to call. aryabhatta is record plus readers,
nine modules inside zorp-track, and it ships no command on purpose. What
writes to it is investigate: every attempt records the conditions it ran
under before the work starts, and, behind ZORP_FORECAST, a forecast
before that. So Zorp mode in the browser is one pre-registered
investigate attempt plus a read of what landed, and not a fifth
capability.

It mirrors panel. An attempt occupies the session exactly as a turn does,
shares the session's sequence counter, answers the existing stop
endpoint, and closes with investigate_done then done. Three routes:
POST /api/sessions/:id/investigate, GET /api/investigate/status,
GET /api/investigate/ledger. All of it sits behind a new, non-default
research feature on zorp-web, and the routes answer 501 without it so
the page can say why the button is off instead of showing a 404 nobody
can explain.

A person launches a run and no model can. An attempt writes to a
pre-registered evidence record and to the ledger, so a model that could
start one could feed the record it is later read against. There is no
tool that reaches the endpoint; agent.rs asserts it over the unfiltered
builtin set and zorp-web asserts it over the set this server hands out.

Checkpoints are auto-approved from the browser because there is no
terminal to ask, which is the CLI's --yes chosen explicitly rather than
fallen back to. The pre-registered kill threshold is still enforced in
code and auto-approve cannot wave it through, and the choice is recorded:
checkpoint_mode reads auto-approve in the ledger.

Forecasting is reported and never set. It costs a model call on every
attempt, it stays off by default, and one page flipping it would change
what the server does for everyone using it. The ledger view says out
loud that an empty expectations list is why nothing can be scored.

The ledger reader names no model-authored text column, so
expectations.assumptions is not in what it returns, and a test asserts
the serialized ledger never carries it. Nothing read back is fed to a
model: the lines on the page are recorded rows and arithmetic over them.
The view builds DOM nodes and puts every string through textContent.

Claude-Session: https://claude.ai/code/session_01KGPVQ8wUG7h36zashWYCp4
@adityak74

Copy link
Copy Markdown
Contributor Author

Rebased onto origin/main twice, since #83 landed partway through. The branch
now fast-forwards onto 195c8cc0 and nothing from #79, #80, #81 or #83 was
dropped.

Conflicts and how they were resolved

Spot checks that nothing was lost: web/src/search-indicator.ts and
web/test/search-indicator.test.ts are present, /api/capabilities is still
routed, web/test/copy-response.test.ts still carries its 26 cases, and the
web suite went 159 to 203 tests as the three merged PRs' tests joined it.

On research-pr never reporting

It was not stuck and it was not failing to trigger. It ran and passed in
20m49s on the previous head, finishing after the other five had. The log says
where the time went:

  • The cache restore was a partial hit, not a full one:
    Restored from cache key "v0-rust-research-stack-Linux-x64-0b9fd15e-843e1462" full match: false.
    This PR changes zorp-web/Cargo.toml and Cargo.lock, which moves the key.
  • Test zorp-track was fast off that partial cache: step started 05:18:44,
    tests were running by 05:19:30.
  • The step this PR adds, Test zorp-web (research feature), is the expensive
    one. It began compiling libduckdb-sys at 05:21:52 and did not reach
    duckdb until 05:34:14. That is about twelve and a half minutes rebuilding
    DuckDB's C++ amalgamation from source.

The reason it rebuilds when the zorp-track step did not is that
-p zorp-web --features research resolves a different feature set across the
shared graph, so libduckdb-sys gets a different metadata hash and is a
different unit of work. The research job on main will fill the shared cache
entry with both variants once this merges, so the steady state is fine.

That does leave a real risk I introduced: on a genuinely cold cache the job
would pay a cold zorp-track build plus this new twelve and a half minutes,
and the workflow's own comment puts a cold DuckDB build at about 35 minutes.
That is over the 45 minute cap, and a job killed by the cap reads like a
broken test rather than a cold cache. So both research jobs now cap at 60
minutes, with a comment saying why. I did not touch anything else about how
they trigger.

Local verification, on the rebased head

command result
cargo fmt --all --check clean, exit 0
cargo build --workspace --locked ok
cargo test --workspace --locked 1252 passed, 0 failed
cargo test -p zorp-agent --features research 669 passed, 0 failed
cargo test -p zorp-web --features research 178 passed, 0 failed
cargo clippy --workspace --exclude zorp-track --all-targets --locked -- -D warnings clean, exit 0, no diagnostics
npm run check (from web/) clean
npm test (from web/) 203 passed, 0 failed
npm run build (from web/) ok, 102.8kb

Toolchain: rustup update stable reported stable-aarch64-apple-darwin unchanged, rustc 1.98.0.

One note on scope: CI's clippy line excludes zorp-track and does not enable
research, so zorp-web/src/investigate.rs is not linted by that job.
cargo clippy -p zorp-web --features research,search --all-targets is clean
on the files this PR adds. It cannot run under -D warnings because
zorp-track itself has two pre-existing lints, on prereg.rs:170 and
track.rs:344, which are why that crate is excluded in the first place. This
PR does not touch zorp-track and I have not tried to fix them here.

@adityak74
adityak74 merged commit f84413e into main Aug 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant