M6.6b: the status page's walkthrough (X6), as planned and approved 2026-10-25 (Jinja2; localhost behind an SSH tunnel).
What it shows (new package nisse-ui):
Sources: agents (--source NAME=STATE_DB) and runs the benchmark kept (--runs DIR, from benchmark.py run --keep-runs).
Work items of a source, plus the agent-level log.
An item's walkthrough: steps (operator and peer messages, facts, gates, plans) and runs, in time order. Delegations nest inside driver runs, and steps taken during a run sit inside it. Each turn shows its purpose, model, time, tokens and prompt reuse. Work from before recording began has its steps; the full log is always below.
A turn: each message as sent, beside the log events it came from, tagged unchanged / labelled / narrated / rewritten / shortened by N / compaction, with dropped and compacted events listed. Messages new since the turn before are marked, along with the first change (where prompt reuse had to stop), next to the server's cache_n. Then the reply with its reasoning and tool calls, the settings and tools offered, and the raw JSON.
Loops flagged: repeated near-identical replies, one correction given 3+ times, repeated apologies.
In nisse-core:
loop/circles.py: loop detection as a pure function, for M7.8 to reuse.
models/render.py: message_indexes(), from the same loop render() uses, so the page's matching can't drift. If rendering the sent events doesn't reproduce the request's message count, the page says so and lists the events apart.
models/calls.py: started_at in meta; CallReader.calls_for(item) and call(id).
state/readonly.py: mode=ro alone creates -wal/-shm for a database whose writer has closed (it did here). With no -wal, files are now opened immutable=1, so nothing is created or locked. This also matters for M6.6d's read-only mounts.
Safety: Jinja2 autoescape on everywhere; CSP default-src 'none'; style-src 'self' (no script at all; <details> does the collapsing); no external assets; binds 127.0.0.1; never shows headers or the token.
Tests:
nisse-ui/tests (26): stores written by nisse-core's own StateStore, CallRecorder and Driver against a fake LiteLLM.
Message matching and change marks; steps nested in runs; agent-level calls; before-recording and no-record cases; a circling run flagged.
Escaping of hostile model text, CSP and no token on the page.
Byte-identical files and no new files after browsing a live store, which can still write afterwards.
Unknown schema/layout named, not guessed; malformed item data renders.
Every agent's SCHEMA_VERSION is known to the page; nisse-dev and nisse-review stores open; kept runs discovered; the real server sends the headers; it binds 127.0.0.1 by default.
nisse-core: test_circles (6), test_readonly (3), two test_render index tests, one test_calls test.
Mutation checks: autoescape off, readonly without immutable, an off-by-one in message indexes, no nesting, an off-by-one in circle repeats, a missing known schema, no new-message marks. Each made a test fail.
Live check: two benchmark tasks run once on qwen3.8-27b with --keep-runs (both passed). All 14 real turns matched every message to its log events exactly, with nothing unmatched; the reasoning (up to 11,796 characters) and prompt reuse per turn show.
To view it on this box:
uv run nisse-ui --runs <kept-dir> --source dev=<state.db>
ssh -L 8765:localhost:8765 <this VM> # then http://localhost:8765/
The Dockerfile copies nisse-ui into the source stage, since the workspace needs every member; there's no image for it until M6.6d.
M6.6b: the status page's walkthrough (X6), as planned and approved 2026-10-25 (Jinja2; localhost behind an SSH tunnel).
**What it shows** (new package `nisse-ui`):
- **Sources:** agents (`--source NAME=STATE_DB`) and runs the benchmark kept (`--runs DIR`, from `benchmark.py run --keep-runs`).
- **Work items** of a source, plus the agent-level log.
- **An item's walkthrough:** steps (operator and peer messages, facts, gates, plans) and runs, in time order. Delegations nest inside driver runs, and steps taken during a run sit inside it. Each turn shows its purpose, model, time, tokens and prompt reuse. Work from before recording began has its steps; the full log is always below.
- **A turn:** each message as sent, beside the log events it came from, tagged unchanged / labelled / narrated / rewritten / shortened by N / compaction, with dropped and compacted events listed. Messages new since the turn before are marked, along with the first change (where prompt reuse had to stop), next to the server's `cache_n`. Then the reply with its reasoning and tool calls, the settings and tools offered, and the raw JSON.
- **Loops flagged:** repeated near-identical replies, one correction given 3+ times, repeated apologies.
**In nisse-core:**
- `loop/circles.py`: loop detection as a pure function, for M7.8 to reuse.
- `models/render.py`: `message_indexes()`, from the same loop `render()` uses, so the page's matching can't drift. If rendering the sent events doesn't reproduce the request's message count, the page says so and lists the events apart.
- `models/calls.py`: `started_at` in meta; `CallReader.calls_for(item)` and `call(id)`.
- `state/readonly.py`: `mode=ro` alone creates `-wal`/`-shm` for a database whose writer has closed (it did here). With no `-wal`, files are now opened `immutable=1`, so nothing is created or locked. This also matters for M6.6d's read-only mounts.
**Safety:** Jinja2 autoescape on everywhere; CSP `default-src 'none'; style-src 'self'` (no script at all; `<details>` does the collapsing); no external assets; binds 127.0.0.1; never shows headers or the token.
**Tests:**
- `nisse-ui/tests` (26): stores written by nisse-core's own `StateStore`, `CallRecorder` and `Driver` against a fake LiteLLM.
- Message matching and change marks; steps nested in runs; agent-level calls; before-recording and no-record cases; a circling run flagged.
- Escaping of hostile model text, CSP and no token on the page.
- Byte-identical files and no new files after browsing a live store, which can still write afterwards.
- Unknown schema/layout named, not guessed; malformed item data renders.
- Every agent's `SCHEMA_VERSION` is known to the page; nisse-dev and nisse-review stores open; kept runs discovered; the real server sends the headers; it binds 127.0.0.1 by default.
- nisse-core: `test_circles` (6), `test_readonly` (3), two `test_render` index tests, one `test_calls` test.
- Mutation checks: autoescape off, readonly without immutable, an off-by-one in message indexes, no nesting, an off-by-one in circle repeats, a missing known schema, no new-message marks. Each made a test fail.
**Live check:** two benchmark tasks run once on qwen3.8-27b with `--keep-runs` (both passed). All 14 real turns matched every message to its log events exactly, with nothing unmatched; the reasoning (up to 11,796 characters) and prompt reuse per turn show.
**To view it** on this box:
```
uv run nisse-ui --runs <kept-dir> --source dev=<state.db>
ssh -L 8765:localhost:8765 <this VM> # then http://localhost:8765/
```
The Dockerfile copies `nisse-ui` into the source stage, since the workspace needs every member; there's no image for it until M6.6d.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
A new package, nisse-ui: a read-only page for studying how the agents
work. For each work item it shows the steps (what people and the
orchestrator said, and each run), each run's turns, and each turn's
request read against the log it was made from:
- each message beside the events it came from, tagged unchanged,
labelled, narrated, shortened (by how much) or a compaction, with what
fitting dropped;
- what is new since the turn before, and where the request first
differs (where prompt reuse had to stop), next to the server's count;
- the reply with its reasoning, and the raw JSON.
Runs going in circles are flagged by nisse_core.loop.circles (repeated
near-identical replies, one correction given again and again, repeated
apologies), a pure function M7.8 can reuse in Driver.run.
In nisse-core:
- render.py: message_indexes(), from the same code render() uses, so
the page's matching can't drift from what was sent.
- calls.py: the record notes when recording began; the reader gains
calls_for(item) and call(id).
- state/readonly.py: opens an agent's SQLite file without creating its
-wal/-shm files (mode=ro alone does, once the writer has closed).
The page reads only through read-only connections, escapes everything,
and sends a Content Security Policy that allows no script. It listens on
127.0.0.1; until M6.6d puts it behind Authelia, reach it over an SSH
tunnel. Jinja2 is its one new dependency, for autoescaping.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
M6.6b: the status page's walkthrough (X6), as planned and approved 2026-10-25 (Jinja2; localhost behind an SSH tunnel).
What it shows (new package
nisse-ui):--source NAME=STATE_DB) and runs the benchmark kept (--runs DIR, frombenchmark.py run --keep-runs).cache_n. Then the reply with its reasoning and tool calls, the settings and tools offered, and the raw JSON.In nisse-core:
loop/circles.py: loop detection as a pure function, for M7.8 to reuse.models/render.py:message_indexes(), from the same looprender()uses, so the page's matching can't drift. If rendering the sent events doesn't reproduce the request's message count, the page says so and lists the events apart.models/calls.py:started_atin meta;CallReader.calls_for(item)andcall(id).state/readonly.py:mode=roalone creates-wal/-shmfor a database whose writer has closed (it did here). With no-wal, files are now openedimmutable=1, so nothing is created or locked. This also matters for M6.6d's read-only mounts.Safety: Jinja2 autoescape on everywhere; CSP
default-src 'none'; style-src 'self'(no script at all;<details>does the collapsing); no external assets; binds 127.0.0.1; never shows headers or the token.Tests:
nisse-ui/tests(26): stores written by nisse-core's ownStateStore,CallRecorderandDriveragainst a fake LiteLLM.SCHEMA_VERSIONis known to the page; nisse-dev and nisse-review stores open; kept runs discovered; the real server sends the headers; it binds 127.0.0.1 by default.test_circles(6),test_readonly(3), twotest_renderindex tests, onetest_callstest.Live check: two benchmark tasks run once on qwen3.8-27b with
--keep-runs(both passed). All 14 real turns matched every message to its log events exactly, with nothing unmatched; the reasoning (up to 11,796 characters) and prompt reuse per turn show.To view it on this box:
The Dockerfile copies
nisse-uiinto the source stage, since the workspace needs every member; there's no image for it until M6.6d.🤖 Generated with Claude Code
https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.