OTEL telemetry attach replaces proxy capture for Claude Code (LLP 0262) - #818
Conversation
The deliberation record for the change set tracked by #798. Committed unchanged as the branch's design record; status stays Draft because acceptance is the author's call, not a pipeline step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One narrow decision per settled choice, so implementation code can cite the choice it realizes instead of restating the reasoning: the settings env block as the injection surface, the events-first plus bodies-for-gaps split, the spool's cap and sweep duties, ingest-time settlement in place of the flush-time pass, the claude_telemetry_events dataset, and the session-ignore control route on the listener. All six are Draft. RFC 0245 gains a Spawns line naming them; nothing here flips a status, because acceptance is the author's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The requirements the listener implementation and its tests answer to: what the source registers, what its endpoint accepts, how events and body files combine at ingest, the two datasets it writes, its status and capture-health duties, its failure modes, and the seams its tests use. Composes decisions 0251-0256 and cites them rather than repeating why. Draft, like the RFC that spawns it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanical forward refs only, on the parts the claude client's mode switch displaces: the RFC (0231), the proxy attach keys (0232), the CA lifecycle and trust (0235, 0237, 0238), the launchd environment delivery (0239), and the fresh-install and migration path (0242, 0243, 0244). Each is phrased "on acceptance of 0245" so it stays honest while 0245 is Draft, and each says what survives for the clients still routed through the proxy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The header Spawns line already listed LLP 0251 to 0256 plus 0257, but the prose "On acceptance" section still enumerated only five choices and left out session-ignore transport (LLP 0256), the choice open question 4 in the same file resolves. Name each decision next to its number so the two lists agree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The seven new docs each carried a Spawned-by header field that appears nowhere
else in the corpus. The corpus already has a way to say this: LLP 0168, spawned
by RFC 0167, glosses its parent inside Related ("LLP 0167 (the accepted RFC
this decision realizes)"), and the parent RFC carries the one-way Spawns line.
Fold the same gloss into Related here and drop the field. LLP 0256 keeps the
"settled in #798" fact on a Tracker line, which LLP 0245 and LLP 0257 already
use.
No information is lost: every doc already listed LLP 0245 in Related, and the
Spawns line on 0245 records that all seven stay Draft until it is accepted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The number was taken on origin/integration/proxy-mode-default-attach (llp/0251-proxy-mode-default-attach.design.md, Status Active), so whichever branch merged second would have gone red on the uniqueness check. 0258 is the next number free across master and every remote branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A second plugin is about to host an OTLP listener of its own, so the transport half of @hypaware/otel's receiver moves to src/core/otlp: signal routing, json-only content-type enforcement, gzip and deflate decoding, the per-signal partialSuccess envelopes, and bind-and-resolve. The shared server never looks inside `data`, so payload interpretation stays with whichever plugin hosts the listener. Pure prefactor. @hypaware/otel keeps its banner text, its bind error prefix, its config keys, its wire responses and its self-telemetry loop guard, which never left the collector. The one new option, a signal subset, defaults to all three signals, so today's listener is unchanged. The transport contract is now pinned by test/core/otlp-json-server.test.js rather than only by the two otel smokes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…P 0252) `ai_gateway_messages` is about to have a third producer, so the gateway capability grows the one call such a producer needs: `recordProjectedExchange`. It takes a finished `AiGatewayProjectedExchange` and owns everything after it - the shared row expansion, `part_id` identity, the schema strip, the table path - so an OTEL-projected row cannot drift from the proxy's row for the same content. The write is preceded by a pre-write `part_id` dedupe seeded from both committed partitions and the spool, restricted to the batch in hand. That is what makes producer overlap harmless: a part the proxy or a backfill run already stored is skipped, not appended a second time. The spool scan is the same one backfill does and stays forbidden on the flush path, where the rows being tested ARE the spool. Purely additive on the capability surface, so every adapter's `^2.0.0` requirement still resolves and the proxy recorder's path is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…LP 0257) `@hypaware/claude` contributes a `claude-telemetry` source through the kernel source registry: an OTLP http/json listener on loopback, on its own port (default 4319, config `claude.telemetry.listen_port`, `0` for dynamic), built on the shared core server so the transport is not copied. Only the logs and metrics routes are served; metrics are accepted and dropped so the exporter is not left retrying a 404, and a non-json content type is refused exactly as the otel receiver refuses it. Payload interpretation is claude-owned. `user_prompt` and `assistant_response` each carry their own `message.uuid`, so they project once, with native identity, and no settlement enricher has anything to repair. `api_request` carries no content and no uuid: it is the usage record for the `request_id` an assistant response names, and is folded onto that message's `attributes.usage` in the same shape the proxy path writes. The index that holds it spans batches, because the exporter flushes on a timer, and evicts oldest-first at a cap. Rows go out through the gateway's `recordProjectedExchange`, so OTEL is a third producer of `ai_gateway_messages`, not a new table. cwd and git identity still come from the SessionStart hook record; `query_source` and `agent.name` carry attribution, and parent_uuid, logical_parent_uuid, user_type and permission_mode read null by design. The daemon's own exports are dropped by resource marker, and another exporter that finds the port is ignored by scope rather than half-parsed. Registration is feature-detected against the capability, so an older gateway degrades to "no listener" instead of failing boot, and the source is registered rather than auto-started, so a CLI activation never binds a port. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Content in at the HTTP endpoint, rows out of `hyp query sql`: the smoke activates the gateway and the claude plugin in a temp HypAware home, drives the SessionStart hook, starts the listener on a dynamic port, and POSTs one real-shaped Claude Code batch (the three content events plus two behavioral ones the listener does not model yet, so skipping them is proven rather than assumed). It then asserts the rows: native uuid identity, prompt and response text, model, the usage the `api_request` event carried, the cwd the hook recorded, the OTEL producer marker, and the transcript-only columns reading null. A replayed batch adds nothing. `hyp backfill claude` over a transcript carrying the same uuids also adds nothing, which is the proxy-to-OTEL overlap window in miniature. A `text/plain` POST is refused with 415. The capture spans and the batch log carry the counts that prove the intended path ran. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(LLP 0258)
A third attach mode beside base_url and proxy: `otel` merges the LLP 0258
#env-keys telemetry block into the settings env block and writes nothing
that routes traffic, so the endpoint stays first party and Remote Control
needs no override keys. The mode reuses the whole marker machinery
unchanged (managed env, prev_env backups, atomic mtime-gated writes, JSONC
refusal, releaseUnmanagedKeys for mode switches), records the spool
directory on the marker for detach and purge to sweep (LLP 0258
#marker-and-spool, LLP 0253), and refuses below Claude Code 2.1.193 with a
`claude update` hint, leaving any existing attach byte-identical (LLP 0258
#version-floor, no fallback to any other mode).
claude_version.js owns the floor: numeric triple parse and compare
("2.1.193" must not sort below "2.1.9"), unknown-is-not-old, a best-effort
`claude --version` probe, and the HYP_CLAUDE_CODE_VERSION override that
keeps hermetic smokes independent of whatever binary the machine carries.
telemetry/spool.js owns the spool path contract: <hyp-home>/spool/
claude-bodies, created 0700 (LLP 0253 #spool-location).
Adopted from the crash-debris-806 stash after review; the malformed mode
doc block in types.d.ts is repaired.
Part of #806 (spec #798, RFC LLP 0245).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The adapter's attach handler switches its non-proxy branch from base_url to otel: with no local CA (no proxy-mode gateway) it probes the Claude Code version, resolves the listener port, and hands the writer the otel mode. The spool directory is created owner-only right after the settings write. A CA-bearing install still attaches by proxy; migrating it to otel is ticket #807. The endpoint's port resolves in trust order: the live daemon's bound port from status.json (pid-gated, via the new core resolveLiveSourceListenPortFromStatus, the generic sibling of the gateway endpoint resolver), then a configured fixed telemetry.listen_port, then the well-known default. That is the promise that makes the listener's default-port bind fallback safe. The marker keeps recording the gateway port, so the attach-drift check is unchanged. Smokes pin the new surface: claude_attach_detach golden-compares the exact nine-key env block, the absence of every routing key (the Remote Control predicate as absences), the marker's mode and spool_dir, and the 0700 spool; client_attach_idempotent adds a below-floor refusal leg (exit 1, `claude update` hint, settings byte-identical) beside the attach/detach cycles; client_attach_on_join proves the daemon's org-driven attach delivers the same block and that drift now shows in the marker port; the walkthrough asserts init's real attach lands otel. All four pin HYP_CLAUDE_CODE_VERSION so no flow depends on the machine's own claude. Closes #806 (spec #798, RFC LLP 0245). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…252) Body events (api_request_body / api_response_body) join the projection through their body_ref: the request body supplies system_text, the tools list, and canonical message ordering; gap blocks (tool_use with untruncated args, tool_result, thinking with its signature) become their own projected messages with a minimized pointer-only raw_frame, matching the proxy path's excerpt policy. Text blocks are never re-projected: the events already delivered them once under native uuid identity. A ref outside the spool is refused (the listener deletes what it reads, so an uncontained ref would be a read-and-delete primitive over the whole filesystem), a missing body counts as evicted, and an unparseable one is deleted rather than retried forever. Session-level body facts carry across exporter batches, bounded oldest-first. Ticket: #804 (spec #798, RFC 0245). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The listener repairs the spool to owner-only on every start, enforces the byte cap (telemetry.spool_max_bytes, default 512 MB) at start and on a timer, and evicts strictly oldest-first (mtime, then name) when the cap is exceeded: the sweep exists exactly for the window where Claude Code writes bodies and nothing consumes them. At ingest the referenced bodies are read, projected, and deleted only after the dataset write succeeded, so a failed write becomes an HTTP error the exporter retries against the same files. Status details gain the spool byte size and the projected / evicted / missing body counts, and eviction is logged with a count so a machine routinely losing detail to backfill is visible. Ticket: #804 (spec #798, RFC 0245). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on (LLP 0257) The hermetic flow now drops real-shaped request and response body files into the spool and proves the join end to end: system_text and the tools list on every row, the full 600-char tool args the event-side clip would have truncated, the tool result, the thinking signature, a pointer-only raw_frame, and both files DELETED after projection. A pre-staged over-cap body is evicted by the startup sweep (config-driven cap, oldest-first, visible in status and logs) and its session still completes: the events land at ingest and transcript backfill recovers exactly the tool rows the evicted body held, under native identity. The spool directory a client created loose is tightened to 0700. Ticket: #804 (spec #798, RFC 0245). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…LLP 0255) The first dataset @hypaware/claude owns: a manifest contributes.datasets entry plus a registration at activation, with the claude_telemetry source signal so central forwarding never falls back to the dataset name. One row per event, hot fields typed (event name, timestamp, session id, tool name, decision, source, cost), every remaining attribute preserved in a JSON column; a hot key whose value does not fit its typed column stays in the JSON rather than vanishing, and an event name we do not model still lands (LLP 0257 S20). The split is behavioral-vs-conversation: user_prompt and assistant_response stay in ai_gateway_messages, the body-pointer events are transport, and everything else becomes a row - including the metrics half of the exporter config, which the listener previously dropped: claude-scope metric data points (cost, lines of code, active time) flatten into the same event shape, one row per data point. The listener writes the behavioral rows only after the message write succeeded, so an exporter retry after a failure re-attempts a write that never happened; there is deliberately no pre-write dedupe (single producer, one POST per batch), and the lost-success-response window produces byte-identical rows compaction's content-hash layer collapses. No localOnlyContentColumns declaration: the LLP 0105 wrapper would null the attributes column for every ordinary caller (no row here carries a cwd to prove itself with); this dataset's privacy seam is the inline ingest drop of LLP 0254 #policy-inline, ticketed as #808. The activation-context test stubs gain the query registry the real kernel always wires in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y_events (LLP 0255) The batch grows tool_decision (reject, with its source) and the hook execution pair beside the existing permission_mode_changed and tool_result, then hyp query sql asserts the behavioral half: one row per event in timestamp order, content and body events absent, hot fields typed and lifted out of the attributes JSON, the unpromoted attributes (from_mode/to_mode, hook identity and outcome, token counts) readable through JSON_VALUE. A /v1/metrics POST lands its two data points as rows named by their metrics, value and unit joined. The registration surfaces are asserted too: hyp query status enumerates the dataset beside ai_gateway_messages, and the registration carries the claude_telemetry source signal. The replayed batch's behavioral rows double by design (no pre-write dedupe; compaction's content-hash layer owns the collapse), asserted explicitly so a future dedupe is a conscious change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 0256) A second recorder is about to host the same /_hypaware/ignore/session route the gateway proxy hosts, and LLP 0256 wants one route shape with one set of tests rather than a copy per plugin. So the handler (and the reserved-prefix recognizer) moves from ai-gateway/src/control.js to src/core/control/session_ignore.js, the same way LLP 0257 S2 moved the OTLP http/json server into core. The mutation log's event name and identity fields become caller-supplied so each host keeps its own signal shape; the defaults keep the gateway's exactly, and no behavior changes. The route test moves to test/core with it, per LLP 0256's consequence that the route's tests live at the shared shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…est (LLP 0256) The shared OTLP http/json server gains the same reserved-prefix seam the gateway proxy has: an optional control handler owns /_hypaware/* before any OTLP routing. The claude telemetry listener registers the shared session-ignore handler over its own in-memory set (nothing on disk, dies with the process), so /_hypaware/ignore/session answers identically on both recorders. Ingest enforces the set on both signals: events whose session.id is in the set are partitioned out before the spool is read, so nothing of the session reaches ai_gateway_messages or claude_telemetry_events, and the dropped session's spooled bodies are DELETED unread under the same spool-containment rule as the read path (LLP 0253 delete-on-drop). Each drop emits the usage_policy_drop signal with policy_source session_opt_out, and the source status reports the live set size plus the drop counters. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 0256) A recorder that hosts the session-ignore route now says so itself: the claude listener advertises control_routes in its status details, and a new core resolver reads every such advertisement out of a LIVE daemon snapshot. The mutations address the gateway (its own two-rung resolution, unchanged) plus every advertised recorder, so the client-agnostic verb never carries a list of client plugins, and a listener that is not running is simply not addressed - it records nothing, so its absence is not a failure. Each outcome is reported: the receipt keeps its legacy top-level fields (the gateway's answer) and gains a recorders array naming every write; the human output prints one line and one responder-trust disclosure per addressed endpoint. An addressed recorder that refuses makes the verb report partial and exit unknown - the refusing recorder is the one still recording, so the write must not read as done. Single-recorder installs keep byte-identical receipts apart from the new array. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…7 S25) claude_telemetry_session_ignore boots the real daemon, ignores a session through hyp session ignore (asserting the receipt names the gateway and the claude listener, and that each control route confirms membership directly, so the gateway's own route is proven undisturbed), then posts the ignored session's events, bodies, and metrics: zero rows land in either dataset, the spooled body is deleted unread, and the drop signal fires with policy_source session_opt_out. unignore restores capture for the same session, whose resumed body is projected then deleted normally. A clean session lands beside the drop throughout, isolating the effect. PRIVACY.md's pausing-a-session passage now names both recorders and the listener's delete-not-skip duty for spooled bodies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…LP 0257 S17) On the otel path capture is best-effort, so a stale endpoint, a down daemon, and upstream event drift all fail into the same silence while every other status line stays healthy. This is RFC 0245 open question 1's duty: the gap becomes a line, and past a threshold a diagnostic. The comparison has two sides. The listener source now publishes last_event_at unconditionally (null before the first event), so its presence marks the snapshot as the telemetry listener's, the same self-advertisement pattern as control_routes; status reads it from status.json without a liveness gate (the LLP 0164 argument: last seen at T survives its daemon, and the dead-daemon window is precisely the gap to surface). The client's own side is a new manifest-declared activity_probe (dir + file suffix, the settings_file home-relative contract) that core stats fresh for the newest transcript mtime, so a daemon that was down while the user worked cannot hide the loss. assessCaptureHealth is pure: the baseline is the newer of the last event and the marker's attached_at, so months of pre-attach transcripts prove nothing right after a migration, and a listener that never saw an event is measured from the attach. Fifteen minutes past baseline is a warning; two hours escalates to an error that degrades overall, because an attached machine silently losing sessions is an outage, not an unfinished setup. The attach probe also reads the marker's mode and attached_at, gating the section to otel-attached configured clients and carrying mode into --json. Part of #810 (spec #798, RFC LLP 0245). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…etached (LLP 0257 S17) Modeled on status_diagnostics: one otel-attached claude install (marker plus transcript trail under a fake $HOME, listener detail under status.json, no daemon alive - the comparison must survive its daemon) driven through three states. Lockstep renders the line and stays healthy; transcripts hours past the last event turn --json's capture_health entry to gap, fire the capture_gap diagnostic at error severity with the attach repair hint, and degrade overall while the text surface tags the line; removing the marker empties the array and silences the section. Span assertions pin the healthy and degraded status.render runs. Part of #810 (spec #798, RFC LLP 0245). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 0245) The adapter stops choosing proxy mode off the CA on disk: otel is the claude client's only attach mode now, so an attach on a proxy-attached machine IS the migration. The settings write releases the proxy keys through the ordinary mode-switch rule and reports the prior marker mode; on 'proxy' the adapter unwinds the launchd environment (darwin, best-effort, mirroring the detach undo's release) and prints the migration story, ending with the one residue that stays the user's call: the CA trust, offered as 'hyp detach claude --purge' and never run for them. Below the version floor the refusal still fires before any I/O, so a too-old client keeps its working proxy attach byte for byte. The proxy-only attach machinery this retires from the adapter (ensureDarwinProxyTrust, the CA preflight read, the Cmd-Q relaunch note) goes with it; detach's own launchd release and the purge path are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--json has carried client_attach[].mode since the marker grew one; the
text line a human reads still said only 'attached', so a machine the
migration just moved from proxy to otel looked unchanged from the surface
that matters. The mode now rides the attached state ('attached (otel)');
markers that predate modes keep the bare word, and the not-attached and
probe-error shapes are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sessions started before the mode flip keep proxying while new events arrive over OTEL, so for a while both producers capture the same session. These tests pin what makes that harmless: the proxy projector's native transcript identity and the telemetry projection's message.uuid identity yield the same part_ids for the same content, and the OTEL producer's pre-write dedupe therefore drops every part the proxy already stored, whole or half. Either arrival order collapses, because the proxy's flush-time dedupe asks the same committed-part_id membership question. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…P 0245) A proxy attach whose keychain dialog was refused still ran and still left the CA on disk, so the migration telling that user their CA "is still trusted in your login keychain" is the one false line in an otherwise honest story. The offer now names the CA and any trust it was granted, which is exactly what 'hyp detach claude --purge' removes either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tops printing a raw marker mode Three review findings on the OTEL attach path, all narrow: - The telemetry listener called `ensureClaudeBodySpool` on every source start, so a daemon created `<hyp-home>/spool/claude-bodies` on machines that never attached the claude client, against whatever HYP_HOME the activation context resolved. `activatePlugins` does not thread the daemon's env, so ctx.env is `process.env`: running `npm test` without `HYP_HOME` demonstrably created the directory in the developer's real `~/.hyp`. Attach is what mints the spool (the same write tells Claude Code where to put bodies), so the daemon now repairs a directory it finds (`tightenClaudeBodySpool`) and creates none. - `hyp attach claude` ran the spool mkdir inside the same try as the settings write, so an unwritable spool root reported a failed attach after the settings file had already been rewritten, and swallowed the migration notes with it - including the `hyp detach claude --purge` line a migrated machine needs. It is now its own try; the failure rides the existing warnings list, counted apart from `malformed_blocks_repaired` so that count keeps meaning one thing. - `hyp status` interpolated the attach marker's `mode` into the clients row raw. The mode is read back off the client's own settings file, so it is a captured label, not an in-process constant; it now goes through the same `printable` every other disk-derived label on that surface uses (LLP 0225). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 1 (neutral, adopted PR)Verdict: changes requested, but nothing here blocks the shape of the design. Baseline at The conflict resolution: both halves check out
Sanitizing: the resolution was right about LLP 0232's self-contradiction does not reach this PR. Findings1. The Claude picker row still composes proxy mode, contradicting this PR's own forward-refs - medium, left for a human
Two live consequences:
I did not fix this. 2.
|
…e the listener listens An `otel` attach writes one `OTEL_EXPORTER_OTLP_ENDPOINT` into the client's settings and nothing ever rewrites it. Two ordinary sequences move the listener out from under it: - attach ran with no live daemon, so `resolveAttachTelemetryPort` could only write the well-known default, and the daemon later found that port taken and fell back to an ephemeral bind (LLP 0114 #ephemeral-fallback); - `telemetry.listen_port: 0` plus a daemon-down attach, where the default is written for a port the config guarantees will never be bound. Either way Claude Code keeps POSTing at the port it was told about, with `OTEL_LOG_USER_PROMPTS` and `OTEL_LOG_ASSISTANT_RESPONSES` on, and every other line in `hyp status` stays healthy. `client_attach_stale` does not cover it: that comparison watches the gateway, which this mode never talks to. The capture-health gap line eventually notices the silence, but only after fifteen minutes of transcript activity and without naming the cause. Both ports are already on disk. The probe now lifts the marker's managed `OTEL_EXPORTER_OTLP_ENDPOINT` port (the live value the client is using, not a parallel field that could disagree with it), and the capture-health block compares it against the listener's live `listen_port`, raising a non-degrading `client_telemetry_stale` naming both ports and the repair. Liveness-gated, unlike `last_event_at` beside it: a dead daemon's snapshot cannot say where anything is bound now, and a restart is what moves the port back. A marker with no endpoint, or one that does not parse to an in-range port, makes no claim at all. This is LLP 0114 #fallback-is-visible applied to the new listener - the half of that decision the listener had adopted the fallback without - and LLP 0257 gains S17b to record it. Does not prevent the exposure, only makes it loud: whether attach should refuse to write an endpoint no daemon has confirmed is a design call, left for a human. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…re gap, and a failed listener start owns nothing Two defects in the new listener, both found by re-reading the start path. The capture-health baseline could not survive a daemon restart. `state.lastEventAt` lives only in the listener's process, so every restart republishes `last_event_at: null` however long capture has been healthy. `assessCaptureHealth` then fell back to the attach timestamp, which on a machine attached a month ago and used an hour ago yields a month-long gap: severity `error`, degrading `overall`, on an install that is capturing perfectly. The first repair the diagnostic prints is `hyp daemon restart`, so the advice reproduced the finding. The listener now publishes `listener_started_at`, and the gap is measured from the newest of the last event, the attach, and a LIVE listener's start - live only, because on a dead daemon the last one's start bounds nothing and the growing gap is exactly what the line exists to surface. The null-events message stops saying "after the attach", which is no longer always the baseline it used. `listener_started_at` rides the capture-health report and `--json` so the reason a gap is or is not claimed is readable. The spool sweep timer was armed before the bind. `stop()` is the only thing that clears it, and a `start()` that throws never returns a handle to call `stop()` on, so an explicit-port bind failure (LLP 0114 #explicit-listen-fails-loudly) left an interval scanning the spool every minute for the life of the daemon on behalf of a source that does not exist. `unref()` kept it from holding the process open, which is why it would never have been noticed. The one-shot sweep still runs before the bind - bodies already on disk are over the cap either way - and only the repeating one waits for a listener to be behind it. LLP 0257 gains S17c for the restart baseline. Tests: five in test/core/status-capture-health.test.js covering the restart, a listener up long enough for the gap to be real, an event outranking the listener start, the live-daemon and dead-daemon halves end to end; and a new test/plugins/claude-telemetry-start-failure.test.js that counts `setInterval` arming across a failed start, because an unref'd timer does not appear in process.getActiveResourcesInfo() - verified to fail against the old order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oint it wrote In the OTLP environment-variable contract a per-signal key beats the generic one, so `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` decides where log records go whatever `OTEL_EXPORTER_OTLP_ENDPOINT` says. Nothing in the tree mentioned those keys. That leaves one shape worth saying out loud: a machine already exporting to its own collector through a per-signal key gets `OTEL_LOG_USER_PROMPTS`, `OTEL_LOG_ASSISTANT_RESPONSES` and `OTEL_LOG_TOOL_DETAILS` switched on by this attach, and its prompts and assistant responses start flowing THERE, while `hyp status` reports `attached (otel)` and the listener sees nothing. `OTEL_EXPORTER_OTLP_HEADERS` is the same hazard from the other side: it carries a collector's credentials and would now ride requests aimed at our loopback listener. The managed set is untouched - LLP 0258 #env-keys is "exactly these keys, and only these", and it is Accepted. Attach now warns per offending key instead, on the same warnings list every other attach notice rides. A warning rather than a refusal because attach cannot see a key exported from the user's shell, so refusing on the half it can see would buy a false sense of completeness. Values are never echoed: an endpoint or a headers value is exactly where a collector token lives, and this string is printed, logged, and serialised into `--json`. Two tests in test/plugins/claude-settings-otel-attach.test.js: the warning fires per key, echoes neither the collector nor the credential, and leaves the keys themselves untouched and unmanaged; and an ordinary attach still warns about nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 2 (neutral, adopted PR) - last round of the two-round capVerdict: findings remain, none of them blocking the design. Round 1's three Baseline at 1. Round 1's fixes hold, and the home-directory leak is really goneI re-tested the leak rather than reading the diff for it, because that is the
The fixes themselves check out in the committed tree, and I found nothing new
2. Verdict on the port desync specificallyA real defect, medium severity, not a merge blocker. Detection is fixed Not a blocker: the wrong-listener case needs a foreign process holding Not a preference either: when it does fire, what crosses to the squatting What I did not do, deliberately: prevention. Refusing to write an endpoint 3. Fixed and pushedHead is now a.
|
Triage: one residual is a production blockerNeutral triaged the residual findings from review rounds 1 and 2 at head Blocker: the spool byte cap is enforced only while the daemon runs, so a down daemon accumulates raw prompt bodies without boundEvery enforcement of Meanwhile the attach writes Why this is a production defect and not a deferrable disk nit:
The decision that unsticks thisA one-line choice from the author, then a small change:
Neutral did not pick: options 1 and 2 change who is allowed to delete captured data, and option 3 weakens an accepted privacy guarantee. Both are design calls above a reviewer's station. Verified non-blockers, held for a follow-up once unstuckAll re-verified at head; none blocks on its own:
Also carried for whoever picks this back up: the real-machine acceptance gate Reply with a comment on this PR (or push to the branch); neutral monitors this thread and will re-engage with your guidance on its next tick. |
…bounded (LLP 0263) LLP 0253 #byte-cap named the daemon-down window as the reason the spool's byte cap exists, then shipped every enforcement inside the listener source, so the window it named was the one window nothing swept. Claude Code keeps writing raw request and response bodies whether or not the daemon reads them, at roughly 145 KB per request, and the daemon is legitimately absent for a crashed service, a machine where one was never started, an uninstall that skipped detach, and the attach-before-first-start path the port resolver deliberately supports. With OTEL_LOG_USER_PROMPTS and OTEL_LOG_ASSISTANT_RESPONSES on, that is unbounded retention of raw prompts, not just a disk nit. `hyp claude-hook session-context` now enforces the same cap on its way out. It is the right second enforcer because attach already installs it on SessionStart, CwdChanged, UserPromptSubmit and PostToolUse/Bash (LLP 0085), so it runs at exactly the cadence bodies are written and the spool cannot outrun it, and because it needs nothing the daemon owns. It deletes only what the daemon's sweep would have deleted: the same enforceClaudeBodySpoolCap, same directory, same oldest-first order, same operator cap read from the same telemetry.spool_max_bytes key. The hook never widens the rule, it runs the existing one while the daemon cannot. The sweep goes last so it can never delay the session-context records the projector waits on (LLP 0085), and runs on every invocation including the ones that record nothing, because a malformed event says nothing about whether the spool is filling. A sweep failure is swallowed, as is the recording half, so the hook still exits 0. LLP 0263 carries the decision and 0253 gains the Extended-by forward-ref. Nine tests drive the hook with no daemon anywhere: four of them fail if the sweep call is removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decision on the blocker: option 1, the hook enforces the capPushed as Why the hook and not attach/status: it is the only daemon-less touchpoint whose cadence is tied to the writing itself. Attach installs it on On the two sub-questions the triage flagged as author calls:
The cap read is the operator's This composes with round 1's Docs
Verification at
|
Triage at
|
The last Draft in this change set joins LLP 0252-0258 and 0262 at Accepted, so every doc PR #818 lands is approved for implementation and the code already on the branch cites settled decisions rather than a draft. Accepted, not Active: the lifecycle reserves Active for built AND merged, and #818 is still an unmerged draft. All nine flip together at merge. Status only. Nothing 0263 settles was edited, and it carries no [inferred] claims to retire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conflict was AGENTS.md's "Written acceptance procedures" list. #818 (LLP 0262, the OTEL telemetry attach change set) already added an `openclaw_capture` bullet there, plus a new `claude_otel_shape_check` bullet. This branch's Finding 1 was the same addition, so master's already covers it. Took master's two bullets and dropped this branch's duplicate rather than listing `openclaw_capture` twice. `hypaware-core/plugins-workspace/ai-gateway/src/source.js` merged textually but conflicted in meaning. The branch's Finding 5 fix argued the stale-CA warning must not say "re-attach", because attach read its mode off the CA and so re-selected proxy mode while the file was on disk. After #818 that reason is gone: `hyp attach claude` is OTEL-only and explicitly never re-attaches by proxy (LLP 0258 #version-floor, LLP 0262 #migration), and LLP 0232's proxy-attach preflight is superseded for this client. The conclusion still holds for a different reason, so the remedy text is kept and the reasoning is restated on the ground that survives: attach leaves the CA on disk on purpose, offering the trust back rather than taking it, so a re-attach cannot clear this state. `hyp detach claude --purge` is the remedy merged master's own migration notice prints. The `@ref` moves from LLP 0232#proxy-attach-preflight to LLP 0262#migration in both the source and the test that pins the message, since that is the decision now constraining the behavior.
… the usage a failed batch consumed Six of the ten findings deferred from PR #818, the ones that are a bug with a reproducing test rather than a design call. - A batch whose message write fails no longer loses the usage it claimed. The usage index outlives one POST because an `api_request` and the `assistant_response` that names its `request_id` routinely arrive in different flushes; projection claimed the entry, the write then failed, and the exporter's retry re-projected against a drained index and wrote the same rows with no `attributes.usage` and no `claude.cost_usd`. The catch puts back only what the batch consumed. - `client_attach_stale` no longer fires at an `otel`-attached client. The marker records the gateway port that mode never uses, so a gateway rebind printed a re-attach warning at a client the rebind did not touch; `client_telemetry_stale` already watches the port that decides whether anything is captured. - A refused `body_ref` is logged as a 12-hex digest, not as the raw wire-supplied path (LLP 0257 S23). - `deleteSpooledBodies` counts what it removed. `fs.rm(..., { force: true })` succeeds on a missing path, so `bodies_deleted` and `bodies_dropped` over-reported every already-evicted ref. - A policy drop brings `spool_bytes` down with the files it deleted, instead of leaving the gauge high until the next sweep restates it. - `last_event_at` is maxed by instant, not by string. Mixed `Z` / `.000Z` / offset shapes let the capture-gap baseline run backwards. The other six findings stay open on the issue: the picker's proxy-mode contradiction between two Accepted docs, the `dedupeStoredPartIds` scan cost, `hyp session status` reading only the gateway, the `lastEventAt` stamping order, the telemetry-endpoint prevention half, and the two shapes in finding 10 - each needs a decision this PR is not the place to make. Refs #843 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two conflicts, both from #818 (LLP 0262) moving Claude Code off proxy capture onto OTEL telemetry attach. hypaware-core/plugins-workspace/claude/src/index.js: resolved to master. This branch's only change there sanitized the permitted-host list in `ensureDarwinProxyTrust`'s keychain dialog, and #818 deleted both that function and its call site - `hyp attach claude` no longer mints or trusts a CA, so the dialog it hardened does not exist. The shared half of that work survives: `displayableCaHosts` still lives in src/core/tls/ca.js and `hyp status` still routes the hosts through it. README.md: kept master's client-generic rewrite of the proxy-mode section and folded this branch's two additions back into it. `hyp status` naming "every host the CA is permitted to vouch for" is still true and still this branch's change (LLP 0238 Consequences, extended but not superseded by 0262, and master's collectProxyTrust still returned no hosts). The launchd residue bullet is kept but re-scoped to `<client>` and to the attach that trusted the CA, since no attach path installs the variable now; the removal paths it documents (detach, --purge, uninstall, the #818 migration unwind) all still exist, as does the `launchd env:` status line. docs/PRIVACY.md, src/core/daemon/status.js and src/core/daemon/types.d.ts merged cleanly; the new proxy-trust `hosts` field does not collide with #818's `client_telemetry_stale` diagnostic or #777's status.json cleanup.
LLP 0262 (#818) updated hyp session ignore to post to every local recorder (claude listener + gateway proxy), not just the gateway, and updated the activate() registration summary accordingly, but left the hypaware.plugin.json manifest summary describing the old gateway-only behavior. The new manifest/activate() agreement test caught the drift; this brings the manifest summary in line with the runtime text.
…sage a failed batch consumed (#851) * Deferred #843 findings: the listener's counters, its refusal log, and the usage a failed batch consumed Six of the ten findings deferred from PR #818, the ones that are a bug with a reproducing test rather than a design call. - A batch whose message write fails no longer loses the usage it claimed. The usage index outlives one POST because an `api_request` and the `assistant_response` that names its `request_id` routinely arrive in different flushes; projection claimed the entry, the write then failed, and the exporter's retry re-projected against a drained index and wrote the same rows with no `attributes.usage` and no `claude.cost_usd`. The catch puts back only what the batch consumed. - `client_attach_stale` no longer fires at an `otel`-attached client. The marker records the gateway port that mode never uses, so a gateway rebind printed a re-attach warning at a client the rebind did not touch; `client_telemetry_stale` already watches the port that decides whether anything is captured. - A refused `body_ref` is logged as a 12-hex digest, not as the raw wire-supplied path (LLP 0257 S23). - `deleteSpooledBodies` counts what it removed. `fs.rm(..., { force: true })` succeeds on a missing path, so `bodies_deleted` and `bodies_dropped` over-reported every already-evicted ref. - A policy drop brings `spool_bytes` down with the files it deleted, instead of leaving the gauge high until the next sweep restates it. - `last_event_at` is maxed by instant, not by string. Mixed `Z` / `.000Z` / offset shapes let the capture-gap baseline run backwards. The other six findings stay open on the issue: the picker's proxy-mode contradiction between two Accepted docs, the `dedupeStoredPartIds` scan cost, `hyp session status` reading only the gateway, the `lastEventAt` stamping order, the telemetry-endpoint prevention half, and the two shapes in finding 10 - each needs a decision this PR is not the place to make. Refs #843 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Delete two committed tool transcripts holding this branch red x/npm-test.log and x/typecheck.log are tracked leftovers from the git add -A sweep in #785 (pre-dating the repo-scratch-hygiene test added by #786). master already carries this fix via #853; this branch forked before that landed, so it inherited the red test separately. Same fix, scoped to the same two paths. * Review fixes: a malformed stamp cannot pin last_event_at, and spool_bytes counts what left the disk Two follow-ups from the review of this branch, both in the code it added. `newerEventTimestamp` fell back to the string compare whenever EITHER side failed to parse. `event.timestamp` is read off the wire unvalidated (`telemetry/events.js` takes whatever string the attribute carried), so a producer that stamps a non-date wins that compare outright - nothing an ISO stamp can begin with sorts above `u` in `unknown` - and because the fallback is symmetric it then beats every genuinely newer event that follows. The baseline is pinned for the life of the daemon, `hyp status` parses it to undefined, falls back to `listener_started_at`, and raises `capture_gap` against a listener that is capturing fine. A value that names an instant now beats one that names none, whichever side it arrives on; the string compare survives only for the pair where neither parses. The projected-body arm still subtracted `spooled.consumedBytes`, the bytes READ, while the PR added `bytesRemoved` precisely so the gauge tracks what left the disk. A body whose unlink fails (EPERM, a read-only spool) is still occupying the cap, and deducting it under-reported `spool_bytes` until the next sweep restated it: the drop arm's bug in the other direction. The new listener case fails on 40e40f9 and passes here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Review fixes: a failed batch's usage restore respects the index cap, and a ref stops citing S18 for something S18 does not say The catch-block restore reinstated every key missing from the pre-projection snapshot, which includes the ones USAGE_INDEX_LIMIT evicted during the batch, not only the ones projection claimed. During a sustained dataset outage nothing is ever claimed and nothing ever shrinks the index, so each failed batch added its `api_request` entries on top of a map that could no longer trim itself and the 512 cap stopped bounding anything. The restore is now `restoreUnclaimedUsage`, which puts the snapshot back and re-applies the cap, with a unit test that drives 40 failed batches through the real projector and asserts the index settles at the cap instead of growing past it (it reaches 576 by round 8 without the trim). The `@ref LLP 0257#failure-modes` glosses on the source and on the accounting test cited "S18 - a retried batch is re-projected from the same inputs, so its inputs have to survive". S18 says delivery is best effort and that a down daemon's behavioral-event loss is accepted; it says nothing about retry inputs surviving. 0257 is Accepted, so the mis-citation is removed rather than the spec edited, and the prose above each already carries the reasoning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: neutral <neutral@hyperparam.app> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: test <test@test.com>
LLP 0262 (#818) updated hyp session ignore to post to every local recorder (claude listener + gateway proxy), not just the gateway, and updated the activate() registration summary accordingly, but left the hypaware.plugin.json manifest summary describing the old gateway-only behavior. The new manifest/activate() agreement test caught the drift; this brings the manifest summary in line with the runtime text.
* Enforce plugin manifest and runtime command agreement (#837) Plugin command help has two sources that nothing compared: `hyp --help` renders before boot and reads `contributes.commands` out of the manifest, while group and leaf help read the registry `activate()` filled. `@hypaware/context-graph-enrich` shipped two of its commands with different summaries at the two levels. - `hyp plugin doctor` gains `command_help_drift`: a declared command whose manifest summary and registered summary differ, a declared command registered `hidden`, or a group description no declared command sits under. Verb-projected commands are covered for free, since a verb registers its CLI command into the same registry. - A new bundled contract test runs that diff over every plugin in the workspace, so the bundled set is held to the check a plugin author runs. - A `hidden` command no longer trips `contribution_undeclared`: omitting it from the manifest is how it stays out of pre-boot help. - The doctor's dry run stops running sources (`@hypaware/otel` bound a real port from `activate()`) and stops handing back a capability stub that cannot be converted to a string. - The dispatch miss path distinguishes a plugin the config selects but the boot did not get, which previously fell through to "unknown command". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix ai-gateway manifest/activate() drift on session ignore summary LLP 0262 (#818) updated hyp session ignore to post to every local recorder (claude listener + gateway proxy), not just the gateway, and updated the activate() registration summary accordingly, but left the hypaware.plugin.json manifest summary describing the old gateway-only behavior. The new manifest/activate() agreement test caught the drift; this brings the manifest summary in line with the runtime text. * Read boot's own unavailable-plugins list for the selected-unavailable dispatch miss The fourth miss state (LLP 0267 #d5) derived "selected but this run did not get it" by subtracting `activePlugins` from the config-selected manifests. `dispatch` only fills `activePlugins` when it boots the kernel itself; a caller that injects one (`opts.kernel`, forwarded by the integration API's `run()`) leaves it empty, so every config-selected plugin looked like a failed activation and a plain typo answered with "your config selects but this run could not activate". `bootKernel` already publishes that set as `unavailablePlugins`: the same four routes D5 names, and empty when dispatch did not boot. Read it instead of re-deriving it, and cover the injected-kernel case with a test. Also fold the seam's duplicated head-token match into `declaresCommandHead`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Doctor: name the blank-summary case, match multi-word groups, harden the bundled gate Three follow-ups on the new command_help_drift check. - `summary` is optional on a manifest command entry, so an entry without one is the shape authors most often land on. It is still drift (top level help lists the command with no description), but reporting it as "two different summaries: the manifest says ''" sends the author looking for a second wording that does not exist. Name the blank and hand back the exact line to paste. - A group prefix is not always one token: `resolveGroupHelp` walks every leading prefix, so `ctx.commands.registerGroup({ name: 'query cache' })` is correct and renders for `hyp query cache --help`. Comparing only the head token of each declared command warned about it forever. Match the whole prefix. - The bundled contract test treated `activate_threw` as non-fatal, so a bundled plugin that throws before registering anything and declares no commands passed the gate vacuously - exactly what LLP 0267 #d2 says it must not do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Review fixes: keep the doctor dry run out of the real HYP_HOME, and off two false reports Three defects found reviewing #849. - `dryRunActivate` left `ctx.env` defaulting to `process.env`, so a plugin that reads `HYP_HOME` during `activate()` was pointed at the caller's real install. `@hypaware/local-fs` mkdirs `<HYP_HOME>/exports` from `activate()`, so merely diagnosing it wrote into the home directory the function's own contract promises not to touch, and the new bundled agreement test did it for the whole workspace on every `npm test`. `HYP_HOME` now points at the throwaway root the rest of the dry run already uses. - The inert source registry returned a `StartedSource` the underlying registry never recorded, so a plugin that starts one of its own sources from `activate()` and then reloads it got `source 'x' is not started` and the doctor reported `activate_threw` against a plugin that works. The no-op is now swapped in at `register()` time and routed through the real lifecycle, which keeps the bookkeeping intact and still runs nothing. A malformed contribution passes through untouched so `register()` still rejects it. - The `registerGroup` warning contradicted LLP 0267 #d3: a group whose registered commands are all hidden is correctly absent from the manifest, yet was warned as describing a group nothing lists, and the bundled gate counts warnings as failures. Such a group is now exempt; a group with a visible command under it still warns. Also drops the "its manifest would not load" route from the `findInactivePluginForCommand` doc: boot names an unloadable plugin by its rootDir, not its plugin name, and an unreadable manifest declares no command to match against, so that route is not (and cannot be) served here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Reconcile command agreement with hidden manifests --------- Co-authored-by: test <test@example.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: test <test@test.com> Co-authored-by: neutral <neutral@hyperparam.app> Co-authored-by: Phillip Cunliffe <filco@Macmini.localdomain>
Claude Code now reports to HypAware through its own OpenTelemetry export instead of through a man-in-the-middle proxy:
hyp attach claudewrites a telemetry env block into the client's settings file (behind a version floor), the@hypaware/claudeplugin hosts an OTLP http/json listener that ingests those events, a capped and swept body spool fills the projection gaps the events themselves do not carry, and behavioral events land in a newclaude_telemetry_eventsdataset. The privacy seams follow the traffic: the folder usage policy and session ignore both decide at ingest on the OTEL path, andhyp purgeandhyp detachempty the raw-body spool. An attached machine migrates from proxy to OTEL in one command,hyp statusnames the attach mode and renders a capture-health line, and the docs and a release-gate shape check describe the attach Claude Code actually gets. Design record:llp/0262-otel-attach-replaces-proxy.rfc.md(renumbered from 0245 on 2026-08-17; the number was ceded to PR #815's design doc) plus decisions LLP 0252-0256, LLP 0258, and spec LLP 0257 - all Accepted by the author on 2026-08-17.Tickets
84cfbcb2RFC 0245: OTEL telemetry replaces proxy attach for Claude Code4a339d58LLP 0251-0256: the decisions RFC 0245 promises on acceptance555c2b38LLP 0257: spec for the claude telemetry listener source8d798803Forward-ref LLP 0245 from the proxy-attach docs it displacese86b667eRFC 0245 names all six decisions it spawns76cdfba3Carry the 0245 back-reference in Related, not a new header field02250bc7LLP 0251 renumbered to 0258: open PR Proxy mode by default: design of record for LLP 0242, plus the missing --json non-interactive pin #817 claimed 02512221a698RFC 0245 renumbered to 0262: open PR Proxy-mode capture: design of record for RFC 0231, plus the missing claude_proxy_capture acceptance procedure #815 claimed 024597be41efRFC 0262 and the docs it spawns are Acceptedb8ea9813The 0245 renumber reaches the test tree's @refsllp/0262-otel-attach-replaces-proxy.rfc.mdand it plus LLP 0252-0258 carryStatus: Accepted(Phil, 2026-08-17). The branch name keeps0245; a branch name is not a doc reference and renaming it would break this PR.bf8b0c97The OTLP http/json listener becomes shared core machinery (LLP 0257)922ccbd2A live producer that is not the proxy records through the gateway (LLP 0252)7cbfca81The claude plugin hosts a listener for Claude Code's own telemetry (LLP 0257)ac545045A hermetic smoke drives the telemetry listener end to end (LLP 0257)2beae929Spooled body files fill the projection gaps events never carry (LLP 0252)9b00bb11The daemon caps the body spool and the listener consumes it (LLP 0253)4ea1bd1fThe telemetry smoke joins body fixtures and recovers an evicted session (LLP 0257)41392189Behavioral events land in their own claude_telemetry_events dataset (LLP 0255)1f7eb092The telemetry smoke reads behavioral events back from claude_telemetry_events (LLP 0255)2e2686fdThe claude settings writer gains an otel mode behind a version floor (LLP 0258)3282bf5dhyp attach claude writes the telemetry env block in otel mode (LLP 0258)6149abcdhyp attach claude migrates a proxy attach to otel in one command (LLP 0245)3fdb53a9hyp status names the attach mode on the text surface (LLP 0245)918d0f1aThe migration overlap window collapses to one row set (LLP 0245)48152ab7The migration's CA offer stops claiming a trust it never verified (LLP 0245)b13c9814hyp status proves the migrated mode through the real collector (LLP 0245)83d75c4dhyp purge and detach empty the raw-body spool (LLP 0253)854764b9The folder usage policy decides at ingest on the OTEL path (LLP 0254)06127359A privacy smoke drives .hypignore through the telemetry listener (LLP 0257 S25)a812d366The session-ignore control handler becomes shared core machinery (LLP 0256)154e134bThe claude listener hosts session ignore and drops the session at ingest (LLP 0256)f4dc24cahyp session ignore posts to every recorder that offers the route (LLP 0256)f574b3f4A privacy smoke drives session ignore through both recorders (LLP 0257 S25)57a63cb7hyp status renders a capture-health line for otel-attached clients (LLP 0257 S17)70e77f2bA capture-health smoke drives hyp status through lockstep, gap, and detached (LLP 0257 S17)0356d959The glossary stops saying attach rewrites a base URL (LLP 0258)512b176fA release gate checks the OTEL shapes against the installed Claude Code721bb7b8The README describes the attach Claude Code actually gets (LLP 0245)ba60e709The shape check waits out an export interval before calling it brokenVerification
Head is now
b8ea9813; the three commits afterba60e709touch only LLP docs and code comments (@reftargets), and the unit suite was re-run green atb8ea9813(4376 pass, 0 fail, 2 skipped). The battery below ran atba60e709:HYP_HOME="$(mktemp -d)" mise exec node@22.22.0 -- npm test).npm run typecheckclean.package_bin_boot,cli_bundled_plugins_activated,daemon_foreground_start_stop,daemon_install_render,walkthrough_picker_to_first_query,client_attach_idempotent,gateway_claude_capture,gateway_codex_capture,hypignore_capture_drop,local_only_export_withhold,source_optout_export_withhold,otel_loopback_capture,local_parquet_export,status_diagnostics.claude_telemetry_capture,claude_telemetry_hypignore_drop,claude_telemetry_session_ignore,status_capture_health.claude_attach_detach,client_attach_on_join,otel_listener_writes_rows,purge_removes_cached_rows,session_optout_capture_drop,cache_spool_batching.Known, not ours
local_only_query_withholdandwalkthrough_to_first_queryfail on this branch, and they fail identically onorigin/master(checked in a throwaway worktree at master: same two assertions,cli: hyp ignore --local-only confirms the added directoryandconfig: golden v2 shape matches the preset output). Neither flow nor the code under its failing assertion is touched here, and neither is in the release battery. Pre-existing red, worth its own bug.launchListenerresolves its state root from the real~/.hyp. On a machine with a local CA from a past proxy attach that reds 6 tests intest/core/status-gateway-idle.test.jsandtest/plugins/ai-gateway-source.test.jsthat have nothing to do with this change. Running with a hermeticHYP_HOMEmakes the suite green. Real defect, but not this change set's ticket, so it is untouched here and deserves its own bug.Outstanding human gates
Spec: #798
🤖 Generated with Claude Code