Repository navigation
feat(officers): forward an officer instance's live session transcript to HQ - #392
Conversation
The fake HQ (hqtest) gains POST /officers/{id}/items/{item}/transcript to
the contract: <=100 events, text <=8 KiB, seq strictly increasing,
idempotent on seq, 409 officer_disabled / not_claimed, {stored, last_seq}.
Tests for the forwarder (internal/officers/transcript): kinds and strict
seq order, batches of <=100, 8 KiB truncation with a marker, redaction of
a secret in a tool_result (with a positive control line), stop on
not_claimed (and no restart from a lagging pass), resume after a restart
without duplicates, a new session keeps seq increasing, disabled config
sends nothing, the buffer cap drops with a counted metric, capped
retries, and the wake-pass start/stop. Plus ccd's GET /v1/transcript
route, the `transcript` config switch, and the metric ledger entry.
Red: none of the code under test exists yet (compile failure).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… to HQ
Opt-in per officer (`"transcript": {"enabled": true}` in MUSTER_OFFICERS).
While an instance holds a claimed item, muster-server reads the instance's
own Claude Code transcript incrementally and POSTs new events to
POST {hq_url}/officers/{id}/items/{item}/transcript with the officer's token.
- ccd: GET /v1/transcript serves the primary CLI's transcript from a
line-boundary offset (cctranscript.ReadLines / Backfill), bearer-authed
like /v1/responses. The route ledger is updated.
- agentgateway.Gateway.Transcript reads it with the instance's turn bearer.
- internal/officers/transcript: the forwarder. Kinds user / assistant /
tool_use ("Tool: target") / tool_result / system; every text scrubbed of
the officer token, then redacted by officership.RedactText (the CI-log
filter's RedactLines, unchanged; a new exported entry point that keeps
blank lines), then cut to 8 KiB with "… [truncated N bytes]". Batches of
<=100 every 2 s; HQ outages back off (2 s doubling to 1 min) with capped
retries; the per-instance buffer is capped and drops the oldest, counted
on muster_officer_transcript_events_total{result}.
- seq = line end offset * 16 + index in line: a function of the transcript
position, so a restarted muster derives the same seq and HQ's
idempotency dedupes its resume re-read. A new conversation mid-item is
shifted above; a restart into one heals from HQ's last_seq.
- officer_claim starts forwarding (from the claim on), officer_complete /
officer_release stop it, each wake pass resumes a held item muster was
not forwarding and stops one no longer held; 409 not_claimed (and every
other HQ decision) stops it until the next claim.
- docs/officers.md: "Transcript to HQ".
Tests: one ccd assertion on a transcript base field was dropped with that
field (seq no longer uses it); the forwarder's test rig gained a 24 h lease
and claim-relative times after the forwarder started at the claim; a
heal test was added.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Round 0 (requirements & deletion) at b31eb9a — verdict: requirement questioned (the pull-from-ccd source design was the agent's; checked, it holds) + one deletion candidate; no open premises. Requirements 11 (unattributed 5). Also found: the leak gate is red on a token-shaped literal fixture in internal/officers/transcript/forward_test.go (build it at runtime like cilogs_test.go); the rollback order (remove the config key before rolling back) is undocumented; a transcript line with more than 16 events is dropped by the ×16 seq scheme. Disposition: D1 kept — whether auto-compaction rotates the session id is unestablished, and the cost of being wrong is lost live events. |
… 15-bit line index
- forward_test: build the redaction fixture's GitHub-token-shaped secret at
runtime ("ghp_" + strings.Repeat("A1b2", 9)), as cilogs_test does, so the
source carries no token-shaped literal.
- docs/officers.md: the rollback order — remove `transcript` from
MUSTER_OFFICERS before rolling muster-server back to an image without it.
- seq: the per-line event index is 15 bits (was 4), so a user record with
many parallel tool results is never dropped as unsequenced. ccd serves no
line over 1 MiB, so a line carries < 32768 events; seq stays below 2^53
for any transcript under 256 GiB. Test: a line with 40 tool results posts
all 40 in strictly increasing seq (red at 4 bits: 17 of 41 stored).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Round 1 (full checklist) at 2543157: 🔴 0 · 🟡 4 · 🟢 3 — ladder ends; all being fixed un-re-audited.
Contract vs civitai/civ-hq#306 checked: field names, kinds, limits, error codes all match. |
- a lost acknowledgement (HQ commits, then 5xx) is stored once, counted skipped_by_hq, not re-sequenced (Y1); - a resume with nothing at HQ forwards only new lines; resuming A after B completes starts live; an untimed line before a fresh claim is not sent (Y2); - a re-claim by another instance lands above HQ's last_seq (Y3); - a 413 drops the batch and halves the next (G2); - a token cut by a cap leaves no prefix (G1); - more credential shapes withheld by the CI-log filter, with ordinary lines as positive controls; every fixture built at run time. The fake HQ gains FailTranscript (incl. commit-then-fail). The resume test now expects HQ's last_seq to be learned (an empty post) instead of a blind re-send; the retries test lets that probe succeed first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, 413, more secret shapes - Probe: before an item's first events, one empty post learns HQ's last_seq (the floor). A resume sends only what is above it; with nothing at HQ it starts live (the backfill window is earlier work). A resume of an instance this muster already forwarded for starts live. An untimed line in a fresh claim's first read is not sent. (Y2) - A batch not above the floor (a re-claim by another instance: its own file, its own offsets; HQ's last_seq is per item) is shifted above it, including a batch that straddles it. (Y3) - The heal re-sequences only a batch's FIRST send: a re-send answered stored 0 is a lost acknowledgement and is stored once. (Y1) - posted counts what HQ stored; the rest is skipped_by_hq (logged); skipped_resume, dropped_too_large added. (Y3) - A 413 drops that batch and halves the batch size for the item. (G2) - The CI-log filter knows Stripe sk_/rk_ live/test, Google AIza, GitLab glpat-, Hugging Face hf_, AWS ASIA and aws_secret_access_key, npm_, pypi-, kubeconfig client-key-data / client-certificate-data; and RedactCutTail withholds a line whose cap-cut word starts like a token. Client.Scrub replaces a cut prefix of the officer token. No muster-side cut happens before the filters (the tool_use 200-byte cut is gone). (G1) - docs: the probe, resume scope, the new results, and HQ's route must be live before `transcript` is enabled (else 404 stops each item). (G3) Tests (the round-1 test commit, plus here: the re-claim test now straddles HQ's last_seq, the 413 test sends 250 so halving is visible — both after mutants survived the first versions). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
What
Forward an officer instance's live session transcript to HQ while it holds a claimed item, so HQ's officer pages can show what the officer is doing. Opt-in per officer:
{"id": "release:example-web", "hq_url": "https://hq.example.com/api/v1", "transcript": {"enabled": true}}Sink, to HQ's contract:
POST {hq_url}/officers/{id}/items/{item_id}/transcriptwith the officer's token, body{"instance", "events": [{"seq", "at", "kind", "text"}]}. Events go in batches of 100 or fewer, each text is 8 KiB or less, and HQ answers{"stored", "last_seq"}.How
GET /v1/transcript?after=<offset>&session=<id>(after=-1&backfill=<bytes>for the tail). It serves the primary CLI's own transcript JSONL. Decoding goes throughinternal/cctranscript(ReadLines/Backfill), the same code the rendered view uses: only complete lines, and a partial last line waits for the next read. The route is bearer-authed like/v1/responses, and the ccd route ledger is updated. muster-server reads it throughagentgateway.Gateway.Transcriptwith the instance's turn bearer.internal/officers/transcript):user, assistant text →assistant, a tool call →tool_use("Tool: <target>"), a tool result →tool_result, an API error or harness notice →system.officers.Client.Scrub, because a shape filter cannot recognise an opaque token).officership.RedactTextruns: the CI-log filter'sRedactLines, unchanged, behind a new exported entry point that keeps blank lines.… [truncated N bytes].muster_officer_transcript_events_total{result}. The officer tools and the wake loop only record state, so nothing blocks the officer.officer_claimsucceeding starts forwarding from the claim (5 s clock slack).officer_completeorofficer_releasestops it.not_claimed, 409officer_disabled, 404 and 401/403 stop forwarding for that item until the next claim. A lagging pass does not restart it.seq design
seq = (end offset of the event's line in the transcript file) × 32768 + (index of the event within that line, 15 bits; ccd serves no line over 1 MiB, so no line's events are dropped)./clear; ccd resumes the primary's own conversation by id on restart) is shifted above all earlier seqs.stored: 0with alast_seqabove a batch read live (one past the resume's window, so not a duplicate). The forwarder re-sequences that batch and every later one abovelast_seq.Alternatives considered: a seq derived from timestamps (records can share a millisecond), and resuming only from HQ's
last_seq(the contract has no read for it, and it cannot be mapped back to a transcript position).Deploy-order hazard
MUSTER_OFFICERSrejects unknown fields. Thetranscriptkey must reach a deployment only together with, or after, an image that knows it. If the order is reversed, muster-server refuses to boot. Rollback is the reverse: remove thetranscriptkey before rolling back to an image without it. Instance pods need the new ccd forGET /v1/transcript. An older pod answers 404, and the forwarder logs it and retries every tick, sending nothing until the pod runs the new image. An officer that enablestranscripton a server whose gateway cannot read transcripts is a boot refusal.Tests
hqtest) implementing the contract: limits, 409s, idempotency.tool_result, with clean lines as a positive control.not_claimed, and no restart from a lagging pass.git archivecopies: the test commit is red (build failure inofficers,officers/transcript,metrics,cmd/ccd). HEAD is green: 12 forwarder tests, 2 ccd route tests and 3 api tests pass.RedactTextcall → the redaction test fails.not_claimedas an outage, or removing the stop → the stop test fails.go.modand the nixgoVendorHash, since no new module imports were added.🤖 Generated with Claude Code