Skip to content

Make Codex Desktop capture explicit and acceptance-tested - #425

Merged
philcunliffe merged 2 commits into
masterfrom
fix/issue-421
Jul 28, 2026
Merged

Make Codex Desktop capture explicit and acceptance-tested#425
philcunliffe merged 2 commits into
masterfrom
fix/issue-421

Conversation

@philcunliffe

@philcunliffephilcunliffe commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Premise check (done first)

The issue asserts Codex Desktop is already covered. Confirmed in code before writing anything:

  • Live route.attach() in hypaware-core/plugins-workspace/codex/src/settings.js writes defaultConfigPath(), which is $CODEX_HOME/config.toml or ~/.codex/config.toml (src/config-file.js). That is the file Codex Desktop reads. Corroborating evidence in-tree: exchange-projector.js accepts x-codex-turn-metadata as a sufficient match signal with the comment "Codex Desktop tags requests with a x-codex-turn-metadata header", and LLP 0083 states the header is "Codex Desktop behavior" as distinct from codex-tui.
  • Backfill route.backfill.js scans <codexHome>/sessions/** and maps session_meta.originator onto the row's first-class entrypoint column plus attributes.codex.originator. entrypoint is a real column in the ai_gateway_messages schema (ai-gateway/src/message_projector.js).
  • The confusing flag.defaultUnsupportedLocations() did list ~/Library/Application Support/Codex as codex_desktop_app with no explanation.

Premise holds. One qualification I did not paper over: the literal string Codex Desktop for originator appears only in fixtures/tests in this repo, not in any captured artifact I could verify. So nothing I wrote pins that literal. The acceptance procedure lists what a machine actually recorded instead of asserting a value.

Per-item status

1. Picker, help, and reference docs state the Codex source covers CLI and Desktop - DONE.

  • codex/hypaware.plugin.json: picker label -> capture Codex conversations (CLI and Desktop); summary and the plugin description name both surfaces. Both label and summary are rendered by the walkthrough (src/core/cli/walkthrough.js lines 91-93), so this is a real product surface, not a comment.
  • hypaware-reference SKILL.md for bothclaude and codex (they are near-identical copies): the source list and the hyp attach entry.
  • README.md source list, plus a new Desktop apps subsection.
  • docs/PRIVACY.md source table.

2. Why Codex Desktop uses the ordinary adapter while Claude Desktop needs its own - DONE.
New llp/0141-codex-desktop-rides-the-codex-adapter.decision.md, with a table of the actual asymmetry (shared ~/.codex/config.toml + shared ~/.codex/sessions/** vs a root-owned /Library/Managed Preferences/...plist and an embedded-CLI route landing under entrypoint: "claude-desktop-3p"). Summarised in README and in both reference skills. @ref LLP 0141#one-adapter on settings.js's attach(), which is the code that makes it true.

3. The exact unsupported boundary - DONE, and made self-explanatory at runtime.
Documented in LLP 0141 #unsupported-boundary, README, and the reference skills. Also a behavioral change: the codex_desktop_appunsupported_location event and its log now carry a covered_by attribute naming the live gateway route and ~/.codex/sessions. The ChatGPT desktop-app flags stay bare on purpose - those conversations really are not captured by any route.

4. Real or opt-in acceptance flow - PARTIAL, and honestly so.
I have no Mac with Codex Desktop. I did not write a smoke that pretends otherwise. What landed:

  • docs/ACCEPTANCE.md: a written, runnable, opt-in / manualcodex_desktop_capture procedure - attach, quit and reopen Desktop, hold a conversation, then verify the live route by query and the backfill route by rollout independently, plus the covered_by boundary check and a clean detach. Each step has a stated pass condition and a "if it fails" branch that says which of them would falsify LLP 0141 rather than which workaround to apply.
  • It leads with what it proves and what it does not prove, and the doc's preamble says outright: do not mark one passed unless you ran it.
  • AGENTS.md (CLAUDE.md is a symlink to it) now splits "written acceptance procedures" from "candidates with no written procedure yet" and points at the doc.
  • hypaware-core/smoke/flows/gateway_codex_capture.js gained a "What this does NOT prove" paragraph: its originator / User-Agent / x-codex-turn-metadata request is synthetic, issued by the flow itself against a local fake upstream, and proves the projector handles that shape, not that a real Desktop routes through HypAware.

No acceptance run happened. No result is claimed. Nothing in this PR should be read as evidence that Codex Desktop was exercised end to end.

5. hyp status / verification surface identifies recent Codex Desktop traffic - NOT DONE. Needs a human design decision.
The analysis, recorded in LLP 0141's consequences:

  • hyp status boots with no plugins activated, by design: decideBootProfile in src/core/cli/dispatch.js returns { activate: [] } for status, and runStatus documents the reason ("intentionally avoids activating configured plugins so the command does not bind local listeners just to render a report"). So status has no dataset registry and no cache read - collectHypAwareStatus reads config, the daemon status file, and client settings files, and nothing else.
  • Teaching core about Codex originator values would put client-specific knowledge in core, which cuts against LLP 0130 ("rendering the picker needs no plugin code execution", and picker/client data is declarative) and LLP 0003's core/plugin split.

Four ways out, each a design decision I should not make unilaterally:

  1. A new declarative activity-probe key on the client descriptor (extends the LLP 0005 / LLP 0130 manifest contract).
  2. Change hyp status's boot profile or give it a cache read (contradicts a documented deliberate decision).
  3. A fifth entrypoint section in hyp query overview - the natural generic answer, covering Claude Desktop too, but it changes that block's calibrated window budget (OVERVIEW_SECTIONS.length feeds rowsAffordable), and every constant there has a paragraph of rationale behind it (LLP 0135#window).
  4. Have the gateway track last-seen entrypoints into status.json - a new runtime feature.

Until one is chosen, the supported check is the query itself, which docs/ACCEPTANCE.md step 4 spells out.

Related finding, left alone deliberately:hypaware-core/plugins-workspace/claude-desktop/src/verify.js line 77 tells users to "Confirm capture: rows land under entrypoint 'claude-desktop-3p' ... (check via 'hyp status' or 'hyp mcp')". The hyp status half of that is not achievable today for the same reason as item 5 - status shows no rows. It is Claude Desktop, not Codex, so I did not change it here; it is the same gap and should be fixed with item 5.

Tests: real pre-fix failure output

New file test/plugins/codex-desktop-coverage.test.js, three tests, run against unmodified source before any change:

$ node --test test/plugins/codex-desktop-coverage.test.js
not ok 1 - the codex picker names Codex Desktop, not just "Codex conversations"
error: 'picker copy names Desktop'
actual: |-
capture Codex conversations
Configures Codex to use the local gateway and records Codex request/response traffic.
operator: 'match'
not ok 2 - the codex client descriptor and manifest description name both Codex surfaces
error: 'plugin description names Desktop'
actual: "OpenAI Codex client adapter for HypAware. Registers the OpenAI-compatible upstream preset on the local AI gateway, configures Codex's config.toml to route through the gateway, and ships the hypaware-query, hypaware-graph, and hypaware-sensitive-scan helper skills plus the AI report skills and the report-to-html renderer."
operator: 'match'
not ok 3 - the Codex app-container unsupported_location says what IS still captured
error: |-
the event states which Codex Desktop capture routes DO work, so "unsupported" is not read as "Codex Desktop is unsupported"
+ actual - expected
+ 'undefined'
- 'string'
expected: 'string'
actual: 'undefined'
# tests 3
# pass 0
# fail 3

After the change: # tests 3 / # pass 3 / # fail 0.

Test 3 drives the real backfill provider against a path-faithful Library/Application Support/Codex directory in a temp home and reads the yielded BackfillEvent, so it covers the path hyp backfill codex runs.

Suite numbers (real)

beforeafter
npm test2761 tests, 2752 pass, 8 fail, 1 skipped2764 tests, 2755 pass, 8 fail, 1 skipped

The 8 failures are identical before and after and all pre-existing in test/core/leave-command.test.js (not ok 799, 800, 801, 803, 804, 805, 806, 807). The count did not grow.

  • npm run typecheck: clean, before and after.
  • npm run smoke -- gateway_codex_capture: ok. Also re-ran backfill_codex_fixture and walkthrough_picker_to_first_query (the two flows nearest this diff): both ok.

Review fixes applied (commit 320f3e7)

Renumbered the new LLP from 0139 to 0141. Open PR #422 (unmerged) claims
both 0139-desktop-picker-consent and 0140-transcript-entrypoint-ownership,
and master's highest is 0138, so whichever of the two PRs merged second would
have collided. The doc is now
llp/0141-codex-desktop-rides-the-codex-adapter.decision.md; all five @ref
annotations and all four prose links moved with it, and both anchors
(#one-adapter, #unsupported-boundary) still resolve. grep -rn "LLP 0139"
returns nothing.

Also from review:

  • docs/ACCEPTANCE.md step 5's pass condition was self-contradictory. It
    asked for rows written by a backfill of the session step 3 had already
    captured live, which the materializer's part_id dedupe suppresses. The
    condition now states outright that rows_written: 0 with
    rows_skipped >= 1 is a pass, and why.
  • The LLP's backfill bullet now carries its own caveat (reviewer point 5
    below): the shared-rollout half rests on the provider's assumption and on
    fixtures, not on a verified capture, and step 5 is the check that confirms
    it. The README/SKILL.md restatements stay assertive.
  • covered_by is now gateway_live,codex_sessions_rollout (reviewer
    point 2 below), not 211 characters of prose, so it stays key-shaped and
    queryable like every other attribute on the event. The prose lives in the
    LLP and README. Test 3 pins the new value on the event and on the
    structured log, which was previously unpinned.
  • The release checklist in AGENTS.md now points at docs/ACCEPTANCE.md
    when a release touched a client adapter.
  • Step 7 re-attaches, so the procedure no longer leaves an operator's own
    machine with Codex capture off.
  • Test 2 renamed to "the codex plugin description names both Codex
    surfaces": it only asserts on manifest.description, and the client
    descriptor carries no prose.
  • The picker summary lost its trailing clause (reviewer point 1 below):
    163 chars, down from 211. walkthrough.js writes it as one unwrapped line.

Re-verified after these fixes: npm test 2764 tests / 2755 pass / 8 fail (the
same pre-existing test/core/leave-command.test.js failures, not ok 799-807,
no growth), npm run typecheck clean,
node --test test/plugins/codex-desktop-coverage.test.js 3/3,
npm run smoke -- gateway_codex_capture ok.

What a reviewer should check closely

  1. The picker label wording.capture Codex conversations (CLI and Desktop) is the one user-visible string change in the walkthrough. If the house style prefers something shorter, this is the line to argue about.
  2. The covered_by attribute value. Now gateway_live,codex_sessions_rollout (shortened from prose during the review fixes, to match this repos preference for key-shaped attributes). It rides both the BackfillEventand the structured log, and does not reach--json`. The prose it replaced now lives in the LLP and the README.
  3. Item 5's four options. That is the real open decision in this PR, and the one thing a human needs to pick.
  4. docs/ACCEPTANCE.md command accuracy. I verified the flags exist (hyp backfill [provider] --since --dry-run --json, hyp query sql, hyp attach/detach codex) and that unsupported_location reaches the log stream rather than the command's JSON (hence the HYP_DEV_TELEMETRY=1 + dev-telemetry/logs-*.jsonl step). I could not execute any of it against Codex Desktop, so the shell is reviewed, not run.
  5. LLP 0141's claim that Desktop writes to ~/.codex/sessions. This is the load-bearing factual claim behind the backfill half. It is consistent with the provider's own long-standing JSDoc ("the rollout files the Codex CLI/Desktop writes") and with the fixtures, but the acceptance procedure's step 5 exists precisely to confirm it on real hardware.

Refs #421 (item 5 is deliberately not done, so this must not auto-close the issue)

The Codex adapter has always captured Codex Desktop, by two routes:
`hyp attach codex` writes the `~/.codex/config.toml` both the CLI and
Desktop read, and the backfill provider reads the `~/.codex/sessions`
rollout tree both write. Nothing in the product surface said so, and
three things implied the opposite: the picker read "capture Codex
conversations", Claude Desktop ships a dedicated setup (so desktop
clients look like they need their own adapter), and the backfill
provider flags `Application Support/Codex` as an unsupported location
with no explanation.
- Picker label, summary, plugin description, both hypaware-reference
skills, README, and PRIVACY.md now name Codex CLI and Codex Desktop.
- New LLP 0139 records why Codex Desktop rides the ordinary adapter
while Claude Desktop needs its own (shared config file and shared
rollout tree vs a root-owned managed plist), and where the unsupported
boundary actually is.
- The `codex_desktop_app` unsupported_location event and log now carry a
`covered_by` attribute naming the live gateway route and
`~/.codex/sessions`, so the flag reads as "this directory", not "this
client".
- New docs/ACCEPTANCE.md carries an opt-in, manual `codex_desktop_capture`
procedure (a human, a real Mac, a real Codex Desktop). AGENTS.md points
at it. `gateway_codex_capture` now states in its own header that its
Desktop-shaped request is synthetic and proves nothing about a real app.
Not done: making `hyp status` (or another product surface) report recent
Codex Desktop traffic from `entrypoint`. `hyp status` boots with no
plugins activated by design, so it has no dataset registry and no cache
read, and putting client-specific knowledge in core cuts against LLP 0130
and LLP 0003. Every route out of that needs a design decision; the
analysis is in LLP 0139's consequences.
Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Fan-in check. The reproduce gate holds independently: the three new tests were run against origin/master's source (3 fail) and against this branch (3 pass), so they pin real behaviour rather than restating it.

One thing to decide before merging, and it is bookkeeping rather than code. The body ends with Fixes #421, which will auto-close the issue on merge. But item 5 of that issue ("Make hyp status or the verification surface identify recent Codex Desktop traffic") is deliberately not done, and one of its acceptance criteria ("An acceptance procedure proves a real Codex Desktop request lands in ai_gateway_messages") is satisfied only as a written opt-in procedure that nobody has run. The body is honest about both, but the trailer is not: merging would close an issue with an open acceptance criterion.

Worth doing one of:

  • change the trailer to Refs #421 and let the issue stay open for item 5, or
  • keep Fixes #421 and open a follow-up issue for item 5 before merging, so the decision is not lost.

Item 5 is a genuine design question, not an oversight. hyp status boots with { activate: [] } by documented design, so it has no dataset registry and no cache read, and putting client-specific knowledge in core cuts against LLP 0130 and LLP 0003. The four routes out are recorded in LLP 0139's consequences: a declarative activity-probe descriptor, a status boot-profile change, an entrypoint section in hyp query overview, or gateway-tracked last-seen entrypoints in status.json. Each is a call a human should make.

An adjacent finding surfaced by that analysis, worth knowing because it is the same root cause on another client:hypaware-core/plugins-workspace/claude-desktop/src/verify.js:77 tells users to confirm Desktop rows "via hyp status", which hyp status cannot do for exactly the reason item 5 is blocked. Out of scope for this PR, but it should be fixed alongside whatever resolves item 5. That file is also touched by PR #422.

And one honest limit on the premise, from the worker's own report: the literal string "Codex Desktop" appears only in fixtures and tests in this repo, so nothing here pins it. LLP 0139's claim that Desktop writes to ~/.codex/sessions is the load-bearing factual claim, and the acceptance procedure's step 5 exists precisely to confirm it on real hardware. Until someone runs that, it rests on the projector comment and LLP 0083 rather than on an observed Desktop run.

…a pass
Renumber the new decision doc from 0139 to 0141: open PR #422 claims 0139
and 0140, so whichever landed second would have collided. Every @ref and
prose link moves with it; both anchors still resolve.
Also, from review:
- docs/ACCEPTANCE.md step 5 asked for rows written by a backfill of a
session step 3 already captured live. The materializer's part_id dedupe
suppresses that duplicate, so a healthy system read as a failure. The
pass condition now states outright that rows_written: 0 with
rows_skipped >= 1 is the expected result.
- The LLP's backfill bullet now marks its own confidence: the shared
rollout tree rests on the provider's assumption and on fixtures, and
the acceptance procedure is what confirms it on real hardware.
- covered_by drops 211 characters of prose for two tokens,
gateway_live,codex_sessions_rollout, so it stays queryable like every
other attribute on the event. The prose stays in the LLP and README.
- The release checklist points at docs/ACCEPTANCE.md when a release
touched a client adapter.
- Step 7 re-attaches, so the procedure does not leave an operator's own
machine with Codex capture off.
- Test 2 is renamed to what it asserts, and test 3 now pins covered_by on
the structured log as well as the event.
- The picker summary loses its trailing clause; walkthrough.js writes it
as one unwrapped line.
Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Review round 1 - 5111c79

Verdict: findings, two blockers. All eight fixed and pushed as 320f3e7.

The review executed docs/ACCEPTANCE.md against the real CLI in a sandboxed HOME/HYP_HOME rather than reading it, which is how both blockers surfaced.

1. BLOCKER - step 5's pass condition contradicted what the command prints

ACCEPTANCE.md:88-90 required "the backfill run reports rows for it", then immediately said re-running is safe because identity comes from the rollout. Both cannot hold. By step 5 the session was already captured live in step 3, and the materializer's part_id dedupe (ai-gateway/src/dataset.js:496-506, whose own comment says backfilled and live rows are byte-identical for the same projection) suppresses the duplicate. Confirmed by running it:

"items_seen": 1, "sessions_seen": 0, "rows_written": 0, "rows_skipped": 1

An operator following the written condition reads a healthy system as a failure, and the doc's next sentence tells them the opposite. Fixed: the condition is now items_seen >= 1 with rows_written + rows_skipped >= 1, and states outright that rows_written: 0 with rows_skipped >= 1 is a pass, with the reason.

2. BLOCKER - LLP number collision with open PR #422

This branch added llp/0139-codex-desktop-rides-the-codex-adapter.decision.md. Open PR #422 adds llp/0139-desktop-picker-consent.decision.mdandllp/0140-transcript-entrypoint-ownership.decision.md. Both are unmerged against a master whose highest is 0138, so whichever landed second would silently duplicate a number, and every @ref LLP 0139#... here would resolve ambiguously.

Neither PR can see this on its own; it is only visible from outside both. Fixed: renumbered to 0141, past #422's range, with all five @ref annotations and four prose links moved. Verified no dangling reference remains:

$ git grep -n 'LLP 0139' <branch> -- src/ hypaware-core/ test/ README.md AGENTS.md docs/
(none)

Both anchors still resolve, and no unrelated LLP number was touched.

3-8. NON-BLOCKING, all fixed

The doc of record now carries its own caveat. LLP's Backfill bullet asserted that $CODEX_HOME/sessions/** is written by both surfaces with no confidence marker, while hedging an adjacent lesser claim. In-repo support for that half is one pre-existing unsourced JSDoc line (codex/src/backfill.js:39) plus three smoke fixtures that synthesizeoriginator: 'Codex Desktop'; no captured artifact demonstrates it. The PR body flagged this, the doc did not. Now the bullet says so and points at the acceptance step as the real-hardware check.

covered_by was 211 characters of prose on an event attribute where every sibling is a short token, against this repo's own preference for key-shaped attributes. Now 'gateway_live,codex_sessions_rollout', with the prose kept in the LLP and README.

Also: the release checklist section (the list a releaser actually follows) never mentioned docs/ACCEPTANCE.md, now does; step 7 left the operator detached from their own Codex capture with no counterpart, now re-attaches; test 2's name promised an assertion on the client descriptor that carries no prose, renamed, and test 3 now pins covered_by on the structured log as well as the event; and the picker summary was 211 chars against a next-longest of 108, rendered unwrapped by walkthrough.js:92-94, now trimmed with the label kept.

Factual claims: all but one hold

Every doc claim was checked against code. Confirmed: attach() writes $CODEX_HOME/config.toml; the rollout backfill scans <codexHome>/sessions and maps session_meta.originator onto the first-class entrypoint column; the live route populates the same column from the request header; Claude Desktop's root-owned managed plist and embedded-CLI route are as described; covered_by reaches both event and log and does not leak into --json; ChatGPT flags stay bare; and hyp status really does boot with { activate: [] }, which is why item 5 is blocked.

The one that does not fully hold is the sessions claim in finding 3. The config half is strong: exchange-projector.js:67-70 and LLP 0083 both predate this PR and record that Codex Desktop tags requests with x-codex-turn-metadata, which only happens if Desktop read the attach-written config. The sessions half rests on assumption and fixtures. That distinction is now in the doc.

Scope honesty

The body states item 5 NOT DONE and item 4 PARTIAL in bold and adds "No acceptance run happened. No result is claimed." The trailer was Fixes #421, which would have auto-closed an issue with an open acceptance criterion; it is now Refs #421 with the reason inline.

Verification run

npm test: 2764 tests, 2755 pass, 8 fail, all the pre-existing leave-command.test.js set, still the only failing file, no growth. npm run typecheck clean. node --test test/plugins/codex-desktop-coverage.test.js: 3/3. npm run smoke -- gateway_codex_capture: ok. Conventions clean, no U+2014, and the new LLP's metadata matches the corpus.

The head has moved to 320f3e7, so the next tick reviews that head.

@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Review round 2 - 320f3e7

Verdict: clean. All eight round 1 fixes verified. One stale line in the PR body was corrected in the same act.

#ItemResult
1ACCEPTANCE step 5 pass conditionPASS - now asks items_seen >= 1 and rows_written + rows_skipped >= 1, and states outright that rows_written: 0 with rows_skipped >= 1 is a pass, with the reason. Internally consistent with the "re-running is safe" sentence, and --json really does emit those fields (commands/backfill.js:983-986).
2LLP renumber 0139 to 0141PASS, checked exhaustively - see below
3LLP backfill caveatPASS - names the two actual grounds (the provider's long-standing assumption, and smoke fixtures that synthesize a Desktop originator), says it is not a verified capture, points at step 5. Specific rather than vague, and the "If it fails" branch names this bullet as the thing falsified.
4covered_by shortenedPASS - 'gateway_live,codex_sessions_rollout' (backfill.js:143), reaches both the log (:317) and the event (:326), does not leak into --json, and no doc still quotes the old prose
5Release checklist wiringPASS - AGENTS.md:227-230, inside ## Release checklist itself, not just the test-model section
6Operator left detachedPASS - step 7 now re-attaches with a conditional note
7Test rename + log assertionPASS, and the log assertion is load-bearing: mutation-tested by deleting only the log-side covered_by spread, which flips test 3 to failing
8Picker summary trimmedPASS - 211 to 163 chars, label unchanged at capture Codex conversations (CLI and Desktop)

The renumber got the most scrutiny, since it is wide, mechanical, and the easiest thing to get subtly wrong. The file is llp/0141-..., its own title says 0141, both #one-adapter and #unsupported-boundary exist as real anchors, and all five @ref LLP 0141#... annotations resolve. A repo-wide grep for 0139 and 0140 across *.js, *.md, and *.json returns zero hits. The llp/ filename set differs from origin/master by exactly one added file, so no unrelated number was disturbed. And 0141 itself is free: master's highest is 0138, #422 claims 0139 and 0140, and none of the other eight open PRs touches 0141.

The trailer is Refs #421 with the parenthetical, so merging will not auto-close an issue whose item 5 is deliberately undone.

One cosmetic thing fixed in this act: the PR body's "what a reviewer should check closely" list still described covered_by as carrying "its long prose value" and asked for it to become a short key - work the review fixes had already done. A reviewer starting at the bottom of the body would have been sent after a resolved item. Rewritten to describe the current state.

Verification run

npm test: 2764 tests, 2755 pass, 8 fail, all the pre-existing leave-command.test.js set, still the only failing file, no growth. npm run typecheck clean. node --test test/plugins/codex-desktop-coverage.test.js: 3/3. npm run smoke -- gateway_codex_capture: ok.

@philcunliffephilcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Jul 28, 2026
@philcunliffe
philcunliffe merged commit ad88783 into masterJul 28, 2026
8 checks passed
@philcunliffe
philcunliffe deleted the fix/issue-421 branch July 28, 2026 19:12
philcunliffe added a commit that referenced this pull request Jul 31, 2026
…item 5) (#502)
* hyp status names recent clients from gateway-tracked entrypoints (#421 item 5)
Issue #421 items 1-4 shipped in #425. Item 5 - "make `hyp status` or the
verification surface identify recent Codex Desktop traffic when `entrypoint`
or `originator` proves it" - was left open because closing it needed an
architecture choice, recorded as four candidates in LLP 0141's consequences.
The maintainer picked **option 4** on the issue thread: the gateway tracks
last-seen entrypoints into `status.json` as traffic flows, and `hyp status`
reports them with no cache read.
- The AI-gateway source folds each committed batch of projected rows'
`entrypoint` / `client_name` into an in-memory, bounded, LRU-evicting map
(new `entrypoint_activity.js`), and exposes it as `recent_entrypoints` in
its `status()` details. It counts and timestamps; it interprets nothing, so
no client-specific string enters core (LLP 0130, LLP 0003).
- The daemon re-reads started sources' `status()` details on every sink tick
and once at shutdown. Boot captured them exactly once, which was enough for
details fixed at bind time (host, port, LLP 0114 fallback marker) and is not
enough for a detail that accrues.
- `hyp status` renders a `recent clients:` block and `--json` a
`recent_entrypoints` array, read from `status.json`. It is deliberately not
liveness-gated: unlike a bound port, "last seen at T" stays true after the
daemon exits, and the rendered age carries the staleness.
- `claude-desktop verify` step 3 told users to "confirm capture via
`hyp status`", which was not achievable. It now is, and the wording points
at the line that answers it.
Docs: new LLP 0164; LLP 0141's open consequence resolved; LLP 0086's
status.json note corrected; `docs/ACCEPTANCE.md`'s `codex_desktop_capture`
step 4 gains the query-free check plus its two expected-negative cases.
Co-Authored-By: Claude <noreply@anthropic.com>
* Bound and sanitize recent-client labels before they reach disk or a terminal
`entrypoint` reaches the tracker by three routes, not the two the design
assumed. Codex's `originator` header and the User-Agent product are both
bounded by Node's HTTP parser (no C0 control bytes, ~16KB). The third is
not: for Claude the *live* projector copies `entrypoint` off a transcript
`.jsonl` line on disk (`applyTranscriptMatch` -> `assignTranscriptIdentity`,
transcripts.js:517), and that is an ordinary JSON string of any length
holding any byte, which then wins over the exchange-level value at
message_projector.js:715.
That value was stored verbatim, written to `status.json` on every tick, and
printed verbatim by `hyp status`. Executed against the real binary: a 30KB
transcript entrypoint carrying ESC and newline produced a 30375-byte
status.json and terminal output containing a raw escape sequence and a
forged `daemon: FORGED ALL GOOD` line. The 32-entry cap bounds the number
of entries, not their size, so it did not prevent this.
Adds `sanitizeLabel` to core util (strip C0/C1 and the Unicode line
separators, clamp to 120 chars) and applies it in two places:
- at the gateway tracker's `record`, so the map key itself is clean and the
eviction cap cannot be diluted by values differing only in invisible bytes
- at core's `recentEntrypointsFromSources`, because `status.json` is a file
and core must not assume the daemon that wrote it was this build; this is
the last point before render
Same input after the fix: status.json 444 bytes, output 1139 bytes, no
escape byte, no forged line, surface still named.
Also corrects LLP 0164's "Bounded" rationale, which named only the header
route while its own motivating example (`local-agent`) is the transcript
one, and pins the default cap in a test (raising it to 100000 previously
went uncaught, since every eviction test passes an explicit `max`).
Co-Authored-By: Claude <noreply@anthropic.com>
* Finish the label bound, and stop a plugin probe from freezing the tick loop
Round-2 review of #502. Three gaps, all executed against the real tracker,
the real core reader, and a real daemon.
1. `sanitizeLabel` stripped control bytes but not the two other classes that
make a label render as something other than what it stores. Bidi overrides
and isolates survived to the terminal, and an unterminated one reorders
past the end of the label into the rest of the status line. Zero-width and
default-ignorable characters survived to the map *key*, which is exactly
what the tracker's own comment claimed could not happen: 500 variants of
`codex-tui` that differ only in zero-width spaces filled all 32 slots and
evicted every real surface. Both classes are now stripped, so those 500
variants collapse to one entry. Confusables stay out of scope on purpose.
Also: the clamp appended its marker past the ceiling (120 chars in, 123
out) and could cut between the halves of an astral character, putting a
lone surrogate into `status.json`. The marker is now inside the ceiling
and a stranded high surrogate is dropped.
2. Core sanitized each label it read back but capped only their length, not
their number, though the comment justifying the read-side sanitize is that
this build may not have written the file. A status file with 5000 entries
printed 5000 lines. Capped at 32 after the sort, matching the writer.
3. The per-tick refresh put plugin code on the tick loop's critical path with
no bound. A source whose `status()` boots fine and then hangs froze
`persist()`, so *every* field in `status.json` stopped updating while the
daemon went on reporting itself healthy, and it hung `hyp daemon stop`
outright (shutdown refreshes too). Probes now run under a timeout, a
source with an outstanding probe is skipped rather than piling up an
unclosed span per tick, and failures are logged once per transition
(`daemon.source_status_failed` / `_recovered`) rather than never, which
was the old `safeStatus` behaviour, or every tick forever.
Verified by execution before and after: status.json keeps being written and
the daemon still stops in ms with a permanently hung probe, and ~500 ticks
produce at most 3 log lines, not 500.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: test <test@test.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: neutral <neutral@hyparam.dev>
Co-authored-by: test <test@example.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approvedneutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@philcunliffe