Skip to content

feat(runtime): publish a labelled instruction line beneath the session title - #223

Merged
gcko merged 4 commits into
mainfrom
feat/session-instruction-line
Aug 27, 2026
Merged

feat(runtime): publish a labelled instruction line beneath the session title#223
gcko merged 4 commits into
mainfrom
feat/session-instruction-line

Conversation

@gcko

@gcko gcko commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Implements DRC-4264 (Codex sessions carry no instruction line) and DRC-4266 (Claude session titles are the session's opening instruction, not its current work). They ship together because they are one UI change: the same second line, on the same cards, from the same payload fields.

The problem

A title answers "which session is this". "What is it working on now" is a different question, and two harnesses were answering it wrongly in opposite directions.

Codex published nothing. Its title came from a bounded tail read of the event_msg/user_message record. Of the 276 local rollouts holding a genuine prompt, 171 (62.0%) have the newest one outside tail_bytes, because reasoning records flood the tail with encrypted blobs, and CLI 0.149 writes the prompt under a different record shape entirely.

Claude published something stale and looked healthy doing it. Its ai-title is generated once and never refreshed: of the 425 transcripts carrying two or more of those records, 424 carry a single distinct value. On sessions with four or more real prompts the title shares no content word with the newest one in 112 of 152 decidable cases (73.7%).

The design

Line 1 keeps its job. A second, labelled line carries the current instruction.

line 1  the session's identity, unchanged in meaning
line 2  the newest genuine prompt where it states work   -> "asked, 4m: ..."
        a bare continuation ("proceed") instead          -> the agent's own turn-start
                                                            statement of intent, above an
                                                            explicit task_started floor,
                                                            "agent, 4m: ..."
                                                         -> else the newest older prompt that
                                                            states work, and only when no
                                                            compaction boundary intervenes,
                                                            "earlier, 2h: ..."
        none of those                                    -> nothing at all

Codex publishes no asked line, because there line 1 already is that prompt verbatim.

Two clauses are load-bearing:

  • Publish nothing rather than a guess. The page resolves a title through a || chain, so a wrong value does not mislead once, it masks the project name for the life of the row. On the local corpus 177 of 457 rollouts (38.7%) reach that branch, closely matching DRC-4264's predicted 39.5%. That is the design working: 143 of them have <recommended_plugins> as their only user record.
  • Never render the line without its label and age. "agent" says an agent is quoting itself; "earlier" says this is not the newest thing asked. records.instruction_line refuses an unlabelled reading outright.

Measured coverage of the shipped readers

Run through the shipped functions, not a reimplementation, so the measurement and the filter cannot diverge.

corpus line 1 line 2 nothing
Codex, 457 rollouts 280 (61.3%) 58 (12.7%) — 48 agent, 10 earlier 177 (38.7%)
Claude, 3,771 transcripts unchanged (ai-title) 1,906 (50.5%) — 1,733 asked, 173 earlier 1,865 (49.5%)

Walk cost: Codex median 2.6 ms, p95 11.9 ms, max 84.7 ms. Claude median 0.3 ms, p95 2.3 ms.

Both preamble shapes are read, and the per-version table is why. The Codex turn-start record moved at CLI 0.149; a single-shape reader finds nothing on the build the operator actually runs.

cli_version files event_msg/agent_message item_completed/AgentMessage
0.142.5 48 47 0
0.144.6 8 7 0
0.145.0 145 141 0
0.146.0 75 72 0
0.146.1 29 27 0
0.149.0-alpha.4.1 1 0 1
0.149.1 88 0 78

Both user-record shapes plus 0.149's third (item_completed/UserMessage) are read for the same reason, each with its own test.

Recomputed byte pins

Recomputed from the assets, never patched textually. Old values are this branch's merge base (4f0be9b), which is ahead of the figures quoted in the issues.

pin old new
next-boot.js size 4,918 7,200
next-boot.js sha256 4b801dc5… d5f40852…
next-sessions.js size 3,961 4,049
next-sessions.js sha256 6e2fd028… 32d5c413…
next-session.js size 11,338 11,469
next-session.js sha256 7d827a70… b444364d…
next styles.css size 24,200 24,551
next styles.css sha256 2ae8e188… 4a519ef9…
load_next_page() size 225,179 228,031
load_next_page() sha256 443c083e… d74f335e…

What moved them: the shared nextInstructionLine renderer and its label vocabulary in next-boot.js, the line rendered into the detail header in next-session.js and into the identity cell in next-sessions.js, and three rules in next styles.css. The assembled page moves because every part feeds it.

test_page.py's pins did not move, and that was verified rather than assumed. load_page() recomputes to 321,499 and app styles.css to 60,265, both identical to the pins on main. No APP_PARTS file is touched by this branch.

Two defects found and fixed while building

  • The continuation test counted raw markup as words. A slash command arrives as sixty characters of markup that splits into six tokens, so /burndown DRC-4266 and the board was classified a bare continuation and buried. Fixed by transcripts.states_work: render first, count second, with slash commands exempt because /release is two words rendered and a whole instruction meant. This lifted Claude line-2 coverage from 42.8% to 50.5%.
  • claude_data.session_title returned the raw aiTitle vendor string with no safe_text and no cap, the one untrusted vendor string in the runtime reaching the payload unbounded. DRC-4266 asks that any change editing session_title close it rather than preserve it. Measured harmless on this corpus (max 69 characters, no C0 or bidi), so it is hardening, not a live bug.

Decisions recorded

  • A new instruction key, not a wider last_prompt. That field is read at ten render sites and two of them are not the session card (next-projects.js puts it first in its own chain, calm.js renders it standalone), so a labelled line packed into it would leak onto both with no label to explain it. records.safe_text collapses newlines, so two lines cannot share one string field anyway.
  • No new config.py threshold. config.py is a documented conflict hotspot. The caps (PROMPT_TITLE_CAP_CHARS 80, LAST_PROMPT_CAP_CHARS 140, INSTRUCTION_CAP_CHARS 140) and the six-word continuation rule live in records.py, which is where the bounding happens. DRC-4264 asked that the 80 and the 140 stop living in two files; they now live in one.
  • Not in turns.scan_turns. test_contracts.py pins its imports and excludes transcripts, so prompt_title is unreachable from it. The readers use io.reverse_lines with a (mtime_ns, size) cache, the pattern claude_data.session_title already established. The two new cache slots are documented as never hitting for a live session; they pay for the idle and ?all=1 rows.
  • The slash-command carve-out is intact. <command-message>/<command-name>/<command-args> stay out of the rejection lists, and both readers route through transcripts.prompt_title rather than growing new tag-stripping code.

Tests

42 new test methods across five modules, three of them mutation-checked against deliberate regressions (removing the turn floor, failing to freeze the preamble at the floor, dropping the 0.149 preamble shape) and each caught by exactly the intended test.

test_records.py (continuation rule, unlabelled refusal, bounding, unusable stamps), test_transcripts.py (14 cases: all three user shapes, both preamble shapes, first-of-turn versus last, the floor, the publish-nothing branch, both compaction shapes, caching, malformed records), test_claude.py (anchor A, continuation fallback, six injected shapes, compaction in both directions, slash commands, a prompt behind the tail, aiTitle bounding), test_codex.py (four end-to-end row assertions including sanitization), test_next_session.py (7 render cases: label and age, the closed label vocabulary, publish-nothing, the no-duplicate rule and its deliberate non-duplicate counterpart, unstamped, escaping).

Docs

sync-docs reconciled: SKILL.md (the title note and the Codex data-source line), docs/design-runtime-architecture.md (four module rows), docs/design-next-ui.md (the header), and a new D-6 in docs/design-session-identity.md recording nine rejected alternatives with their measurements, including the character-length rule, the positional rules, the silent walk-back, and the single-shape read. The docs-synced-through marker is deliberately untouched per the parallel-work rule.

Follow-up commit

d19a917 corrects the duplicate-line rule. It was a plain prefix test in both directions, which suppressed a line whose title merely opens it ("Fix the tests" above "Fix the tests failing on Windows") — the case the feature exists for. It is now equality plus the one ellipsis case that the two caps genuinely produce, with a test pinning each side.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Coverage

Name                                                                  Stmts   Miss Branch BrPart  Cover
-------------------------------------------------------------------------------------------------------
cargento/skills/cargento/agy_hook.py                                     79     14     28      7  78.5%
cargento/skills/cargento/cargento_runtime/__init__.py                     0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/aggregate.py                  194      1     50      0  99.6%
cargento/skills/cargento/cargento_runtime/asks.py                       110      0     28      0 100.0%
cargento/skills/cargento/cargento_runtime/claude_data.py                305     33    142     16  89.0%
cargento/skills/cargento/cargento_runtime/cli.py                        126     14     26      3  87.5%
cargento/skills/cargento/cargento_runtime/collectors/__init__.py          0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/collectors/antigravity.py     410     42    166     26  86.8%
cargento/skills/cargento/cargento_runtime/collectors/claude.py          209     17     68      7  90.6%
cargento/skills/cargento/cargento_runtime/collectors/codex.py            98      7     38      7  89.7%
cargento/skills/cargento/cargento_runtime/collectors/copilot.py         148      6     50      2  96.0%
cargento/skills/cargento/cargento_runtime/collectors/cursor.py          277     20    106     17  89.8%
cargento/skills/cargento/cargento_runtime/collectors/droid.py            32      3      6      1  89.5%
cargento/skills/cargento/cargento_runtime/collectors/gemini.py           53      7     16      4  84.1%
cargento/skills/cargento/cargento_runtime/collectors/goose.py            89     11     28      4  87.2%
cargento/skills/cargento/cargento_runtime/collectors/opencode.py         78      6     26      2  92.3%
cargento/skills/cargento/cargento_runtime/collectors/pi.py              326     34    152     20  88.7%
cargento/skills/cargento/cargento_runtime/config.py                     187      1     20      1  99.0%
cargento/skills/cargento/cargento_runtime/diagnostics.py                 84      4     26      4  92.7%
cargento/skills/cargento/cargento_runtime/dismissals.py                 113      2     28      2  97.2%
cargento/skills/cargento/cargento_runtime/events.py                     162      0     62      0 100.0%
cargento/skills/cargento/cargento_runtime/http_api.py                   515     31    176      9  94.2%
cargento/skills/cargento/cargento_runtime/io.py                         126      2     28      0  98.7%
cargento/skills/cargento/cargento_runtime/lifecycle.py                  323     15    104      6  95.1%
cargento/skills/cargento/cargento_runtime/notifications.py              174     14     60      4  91.5%
cargento/skills/cargento/cargento_runtime/observation.py                235      2     64      0  99.3%
cargento/skills/cargento/cargento_runtime/observer.py                   217     28    100     13  84.5%
cargento/skills/cargento/cargento_runtime/probe.py                       44      0     18      1  98.4%
cargento/skills/cargento/cargento_runtime/quota.py                      333      2    112      1  99.3%
cargento/skills/cargento/cargento_runtime/records.py                    195      5     94      9  95.2%
cargento/skills/cargento/cargento_runtime/sessions.py                   101      0     44      0 100.0%
cargento/skills/cargento/cargento_runtime/snapshot.py                    36      0      4      0 100.0%
cargento/skills/cargento/cargento_runtime/spacedock.py                  382     46    190     24  87.4%
cargento/skills/cargento/cargento_runtime/state.py                       64      0      2      0 100.0%
cargento/skills/cargento/cargento_runtime/stream.py                      57      0      8      0 100.0%
cargento/skills/cargento/cargento_runtime/transcripts.py                354     16    190     15  94.3%
cargento/skills/cargento/cargento_runtime/turns.py                      197     14    104     12  90.7%
cargento/skills/cargento/cargento_runtime/web/__init__.py                 0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/web/page.py                    54      0     14      0 100.0%
cargento/skills/cargento/event_hook.py                                   86      4     28      3  93.9%
cargento/skills/cargento/mcp_server.py                                  377     22    112     14  92.2%
cargento/skills/cargento/notify_hook.py                                  49     15      6      1  67.3%
cargento/skills/cargento/server.py                                        3      0      2      1  80.0%
cargento/skills/cargento/statusline_hook.py                             131     13     46      8  87.0%
scripts/bench_collect.py                                                211      9     54      5  94.7%
scripts/bench_event_latency.py                                           67     67     14      0   0.0%
scripts/bump_version.py                                                  60     12     24      5  77.4%
scripts/capture_hook.py                                                 287     30     86     11  88.5%
scripts/lint_embedded.py                                                 92      3     28      2  95.8%
scripts/validate_plugins.py                                             663    186    386     58  69.3%
-------------------------------------------------------------------------------------------------------
TOTAL                                                                  8513    758   3164    325  89.6%

Threshold: fail_under in pyproject.toml · label coverage-exception to bypass (visible in PR timeline).

gcko and others added 4 commits August 27, 2026 14:39
…n title

A title answers "which session is this". "What is it working on now" is a
different question, and two harnesses were answering it wrongly in opposite
directions.

Codex published nothing. Its title came from a bounded tail read of the
`event_msg`/`user_message` record, and current builds put the prompt elsewhere:
of the 276 local rollouts holding a genuine prompt, 171 (62.0%) have the newest
one outside `tail_bytes`, because `reasoning` records flood the tail with
encrypted blobs. Claude published something stale and looked healthy doing it.
Its `ai-title` is generated once and never refreshed, so on a long session it
names work that finished hours ago.

So line 1 keeps its job and a second, labelled line carries the current
instruction:

  line 1  the session's identity, unchanged in meaning
  line 2  the newest genuine prompt where it states work  -> "asked, 4m: ..."
          a bare continuation instead                     -> the agent's own
                                                             turn-start intent,
                                                             above an explicit
                                                             task_started floor,
                                                             "agent, 4m: ..."
                                                          -> else the newest
                                                             older prompt that
                                                             states work, and
                                                             only with no
                                                             compaction between,
                                                             "earlier, 2h: ..."
          none of those                                   -> nothing at all

Nothing rather than a guess is load-bearing: the page resolves a title through a
`||` chain, so a wrong value masks the project name for the life of the row.

Both record shapes are read on both records, because the Codex turn-start
preamble moved at CLI 0.149: `event_msg`/`agent_message` with
`phase: "commentary"` covers 295 of the 306 local rollouts on 0.142.5-0.146.1 and
0 of the 88 on 0.149.1, while `event_msg`/`item_completed` with an `AgentMessage`
item covers 78 of those 88 and none of the older files.

Measured on the shipped readers: Codex publishes a title on 280 of 457 rollouts
(61.3%) and a second line on 58 (48 "agent", 10 "earlier"); Claude publishes a
second line on 1,906 of 3,771 transcripts (1,733 "asked", 173 "earlier"). The
walk costs 2.6 ms median and 93 ms worst case on Codex, 0.3 ms median on Claude.

Implements DRC-4264 and DRC-4266 (Linear).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@variable.team>
The duplicate-line rule was a plain prefix test in both directions, so a short
generated title that happens to be the first few words of a genuinely newer
instruction ("Fix the tests" above "Fix the tests failing on Windows") was
read as a repeat and dropped. That is exactly the case this feature was added
for.

The rule is now equality, plus the one case beyond it: line 1 clips at 80
characters and line 2 at 140, so a single prompt reaches them as two strings
with the shorter one ellipsed. Nothing else is a duplicate.

Byte pins recomputed from the assets: next-boot.js 6,930 -> 7,200, and the
assembled next page 227,761 -> 228,031. test_page.py's pins are unmoved,
verified rather than assumed: load_page() still recomputes to 321,499.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@variable.team>
…ntrols

Five findings from the adversarial review of #223, plus one the arbiter merged
from four reviewers.

`_codex_walk` published a previous turn's statement of intent as the current
one's. `reached_floor` was read as proof that THIS turn opened, and it only
proves some turn did: a turn that has not written `task_started` yet lets the
walk run past the newest prompt, overwrite the preamble with the previous turn's
commentary, and then set the floor on that turn. The preamble is now bounded
structurally instead, assigned only while no prompt has been seen, which makes
it strictly newer than the newest genuine prompt and so this turn's by
construction. Gating the RETURN on a "this turn wrote no floor" flag was tried
first and is worse: the two differ only when a turn has commentary but no floor
of its own, and there the flag suppresses a preamble that is genuinely current.
The frozen corpus measures 0 incidence, which is why this needs a fixture; the
mutation that drops the new clause republishes the previous turn's intent.

A bare harness-control slash command published as the instruction on 202 of
1,906 local Claude lines (`/login` 74, `/clear` 68, `/plugin` 23, `/mcp` 12),
indistinguishable in the labelled slot from something the operator asked for.
#222 fixed this class for the observer's goal slot two hours before this branch
rebased onto it, so the list is not written a second time: `_is_harness_control`
and its measured names move to `records.harness_control` and both surfaces call
it. Two primitives disagreeing about whether `/clear` is an objective is the
bug #222 was eliminating. The carve-out it measured survives, with a test: a
bare SKILL invocation (`/create-pr`, `/cargento:cargento`) IS the operator's
intent and keeps publishing, and so does an argument-carrying command.
After: 0 of 1,820.

`instruction_line` scrubbed at the cap while `clip` appends its ellipsis AFTER
cutting to it, so the marker was trimmed off 29 of 1,906 lines and a mid-token
cut read as a truncated sentence. The bound is now the cap plus one, the same
`+ 1` and the same reasoning line 1 already carries. After: 4, and all four are
texts naturally exactly at the cap, which `clip` never cut.

The two instruction rules in the next stylesheet set no overflow guard. 50 of
1,789 published lines carry an unbreakable token over 80 characters (longest
113), pasted URLs that `shorten_paths` deliberately leaves whole; under
`max-width:760px` the detail padding drops to 0 and one of those gives the whole
page a horizontal scrollbar. Every comparable surface in the file already guards
it. Byte pins recomputed from the assets, not resolved textually: next
`styles.css` 24,551 -> 24,597 bytes, the assembled next page 228,031 -> 228,077,
both digests replaced. All twelve JS part digests are unchanged, and the +46 is
exactly two copies of the declaration.

Finally, `states_work` counted words on `prompt_title`'s output, which is line 1
only. The rendering has to decide the SHAPE, because that is where a slash
command becomes recognisable, but the count belongs on the tag-stripped body:
counting the rendering called 97 of 2,066 local newest prompts a bare
continuation when the operator had written an instruction, and quoted an older
prompt in place of the one they had just sent. Two shapes, both measured: a
short opener over a multi-line body, and a one-line prompt whose first 140
characters are mostly a pasted URL, which counts as a single word. All 97 move
the same way and none moves the other.

The shipped skill body was wrong in three ways this branch introduced, and it
is a validated artifact. The Session-title bullet claimed the fallback is the
session's newest instruction: that is true only on Codex, and the fallback is
the OPENING prompt on Claude (`claude_data.session_title`) and on Pi
(`collectors/pi.py`), which is the great majority of titled rows. The new bullet
claimed Codex suppresses line 2 when its title is the newest instruction "word
for word", and 167 of 280 local Codex titles are ellipsed truncations, 144 of
which suppress the line while holding back a median 56 further characters. And
the line renders only on the opt-in `?next=true` UI, the convention the file
already uses twice, since the default bundle's APP_PARTS read no `instruction`
field at all.

Refs DRC-4264.

Signed-off-by: Jared Scott <jared.scott@variable.team>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@variable.team>
Four claims the review fixes invalidated.

The module map said `states_work` renders first and counts words second. It
reads two different things off one prompt now: the rendering decides the shape,
the tag-stripped body decides the count, and the row says why counting the
rendering was wrong on 97 of 2,066 local newest prompts.

The same row, and the rejected-alternatives list in the session-identity design,
both described the turn floor as the thing that bounds the preamble. It is not
sufficient on its own, and the design doc keeps the floor bullet while naming
what reaching one does not prove.

`records.harness_control` is new to the `records.py` row, with the reason it
lives there rather than in either caller, and so is `instruction_line`'s cap
plus one. The observer row no longer implies it owns the control list.

`COMPATIBILITY.md`'s sync marker is deliberately untouched: this is a feature
branch, and parallel branches cannot vouch for one another's work.

Refs DRC-4264.

Signed-off-by: Jared Scott <jared.scott@variable.team>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jared Scott <jared.scott@variable.team>
@gcko
gcko force-pushed the feat/session-instruction-line branch from d19a917 to cabbc5c Compare August 27, 2026 07:02
@gcko
gcko merged commit 2ddf897 into main Aug 27, 2026
11 checks passed
@gcko
gcko deleted the feat/session-instruction-line branch August 27, 2026 07:14
gcko added a commit that referenced this pull request Aug 27, 2026
AGENTS.md's Parallel Work section says to leave this marker alone per branch and
stamp it once from main after the merges, naming what the range actually covers.
Three parallel branches just landed, so this is that stamp.

11730b5..0f9d086 is eleven commits: the v0.16.0 release, the five PRs that built
the instruction line and the redaction filter (#220, #222, #223, #225, #226),
the review-calibration and CI-detector change (#227), and the three DRC-4269
follow-ups that cleared the deferred-findings backlog (#228, #229, #230).

Each of those PRs carried its own doc updates, and the three DRC-4269 branches
deliberately did not touch this line so they could not collide on it. What this
stamp vouches for is that range as a whole: the design docs, SECURITY.md and the
runtime architecture module map were reconciled against the code inside the PRs
that changed it, and several figures in them were re-derived rather than carried
forward.

Signed-off-by: Jared Scott <jared.scott@variable.team>
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