Skip to content

M2+M3(M-MISSION-LOOP-UNIFIED-TELEMETRY): stage accounting + node-generic cloud dual-write - #697

Merged
sunholo-voight-kampff merged 4 commits into
devfrom
sprint/iter193-mission-telemetry-m2m3
Aug 13, 2026
Merged

M2+M3(M-MISSION-LOOP-UNIFIED-TELEMETRY): stage accounting + node-generic cloud dual-write#697
sunholo-voight-kampff merged 4 commits into
devfrom
sprint/iter193-mission-telemetry-m2m3

Conversation

@sunholo-voight-kampff

Copy link
Copy Markdown
Collaborator

M-MISSION-LOOP-UNIFIED-TELEMETRY — M2 + M3

Lands the implementation completed by mission iteration 193 (2026-08-13 13:58–15:18), which
finished the work, committed it to the main checkout's dev, and died before pushing or recording.
This PR is iteration 194's verify-and-land of that orphaned work — not a redo.

Milestone Commit
M2 mission stage accounting 088176104
mission-control skill: post tokens + real status, M3 switch e4c45eb4f
M3 node-generic cloud routing 769d920a0
Handover doc 2b5cfa976

M2 — two defects with different owners: PostIteration never called UpdateStageStatus
(writer), and the poster supplied zero tokens (caller). IterationStage gained Status; stage
cost/tokens now aggregate into the chain total. Measured on the iter-190 shape through the built
binary: 4 stages pending / $0.0000 / 0 tokens → completed+failed / $0.1077 / 37,414 tokens.

M3chains post-iteration dual-writes to the local store and, when a node names one via
AILANG_CHAINS_CLOUD / --cloud, a remote one, under the same chain and stage ids so spans
join either copy. Each target keeps its own bounded spool; an unreachable remote is a loud stderr
notice plus a buffered retry, never a blocked iteration.

Design freeze items 1–3 were ratified by Mark on 2026-08-13 and are recorded verbatim in the sprint
JSON's design_freeze block.

Co-Authored-By: codex gpt-5.6-sol

🤖 Generated with Claude Code

sunholo-voight-kampff and others added 4 commits August 13, 2026 14:55
…come, and the money adds up

mission:v1/iter-190 spanned Anthropic, OpenAI and OpenRouter and read back as four
`pending` stages, two of them recording $0.0570/$0.0507 at ZERO tokens, under a chain
total of $0.0000 against $0.1077 actually spent. Two defects with DIFFERENT owners —
fixing "mission accounting" as one thing patches one and misses the other.

WRITER-SIDE (status). PostIteration created each stage and never transitioned it, so
every stage kept CreateStage's `pending` default forever. IterationStage now carries
Status from the EXISTING ChainStageStatus vocabulary (no new enum), applied via
UpdateStageStatus. An unknown value is rejected in Validate rather than coerced:
coercion would report an outcome the caller never claimed.

CALLER-SIDE (tokens). The writer already forwarded tokens_in/tokens_out to
UpdateStageMetrics — the zeros came from the poster. The mission-control skill now
supplies per-stage token counts, and the CLI echoes the totals it recorded plus a
stderr notice naming how many stages posted no status, so a payload that forgot them
is visible at the call site instead of weeks later in a cost rollup.

Stage costs/tokens now aggregate into the chain total, and a chain whose stages ALL
reported terminal closes as completed — or failed if any stage failed. That derivation
is deliberately gated on every stage having reported: a legacy or mid-flight post
leaves the chain `active`, which is today's behaviour.

The criterion that blocks the shortcut: setting every stage to `completed` would
satisfy "nothing stays pending" while HIDING real failures. A failed stage reads back
`failed`, pinned by a test built from the real iter-190 shape (4 stages, 3 providers,
two carrying cost with zero tokens), and the skill instruction says so in as many words.

Version skew is a contract, not a nicety — the skill and the CLI ship independently, so
a payload that omits Status still works and behaves exactly as before (its money still
aggregates; only the outcome is unreported).

Verified end-to-end through the built binary against a throwaway HOME, not just in
tests: the iter-190 payload reads back 4 stages completed/failed, $0.1077, 37414 tokens,
chain `failed` — against all-pending/$0.0000/0-tokens before. 7 new tests.
make test PASS, lint clean, check-boundaries / file-sizes / check-skills / check-changelog green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…3 dual-write switch

Re-applies the M2 caller-side instruction lost to a concurrent write of this file
(the live mission-control iteration rewrites its own SKILL.md), and adds M3.

The skill is the poster: iter-190's $0.0570/$0.0507-at-zero-tokens and its four
`pending` stages came from this payload, not from the writer. So the template now
carries `status` on every stage and tokens on every metered one, with the
do-NOT-blanket-complete rule stated where the payload is built — a stage that
failed must be posted `failed`, or the record hides the failures it exists to
surface. Omitting `status` stays valid so an older loop never breaks.

Also documents AILANG_CHAINS_CLOUD: dual-write to a remote observatory under the
same ids, own bounded spool, never blocks, and unset = unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mote observatory, node-generic

`chains post-iteration` wrote to ONE hardcoded local SQLite path. A node that names
a remote target — AILANG_CHAINS_CLOUD=gcp or --cloud gcp — now writes each iteration
to its local store AND to that one. Unset, the behaviour is what it was, which the
tests assert rather than assume (openPostTargets returns exactly one target).

SELECTION reuses the AILANG_STORAGE resolver: NewBackends now delegates to
NewBackendsForMode(ctx, mode), and the command passes an explicit mode. One switch
with the mode as a parameter, not a second local/gcp/hybrid selector — and this
node's own AILANG_STORAGE is untouched, so its coordinator and messaging stores stay
put. A remote target that resolves to this node's OWN store is rejected with an
explanation rather than quietly writing to itself.

IDENTITY is the part that makes the remote copy useful instead of an island.
ChainCreateRequest/StageCreateRequest take an optional ID, and PostIteration
backfills the post with what it wrote, so the second target reproduces the SAME
chain and stage ids and a span carrying them joins either copy. A PINNED id is never
regenerated by CreateStage's UNIQUE-constraint retry: that retry exists for
stage_number contention, and silently swapping a pinned id would look like success
while breaking the identity it was pinned for.

PostIteration now takes the Backend INTERFACE — one writer, two targets; a second
implementation would be a second set of bugs. That required UpdateStageEvalAssessment
on the interface, implemented across all five backends including Firestore (which is
what both deployed observatories actually run). A remotely-posted stage without its
model cannot be rate-classified cloud-side, so this is load-bearing, not tidiness.

NEVER-BLOCK, the ratified requirement, EXTENDS the existing bounded+loud spool rather
than inventing a policy — but each target gets its OWN. Sharing one would let a long
cloud outage evict local posts that were only waiting on a locked database; separate
spools keep both caps (100 entries / 1 MiB) intact per target.

Verified end-to-end through the built binary, not only in tests: the same chain id
lands in two distinct stores; a genuinely broken cloud config (gcp with no project)
produces a loud stderr notice, a buffered post, rc=0 and a local write that still
succeeded; and the buffered posts flush on the next run keeping their pinned ids.
8 new tests (6 cmd + 2 observatory).

make test PASS, lint clean, check-boundaries / file-sizes / check-skills /
check-changelog / check-golden-drift / verify-no-shim / verify-examples /
test-nightly-classifier / test-regression-guards all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n is what remains

M1/M2/M3 all landed; sprint JSON reads implementation_complete_pending_evaluation.
Records what was verified end-to-end (not only in tests), and four follow-ups in
priority order — the first being that M1's session->chain linkage reaches the EVAL
path only: internal/eval_harness/ai_agent.go:250 is the sole production site that
sets OpenRouter correlation, and the mission loop's quorum calls run outside it, so
no session.id arrives for a mission span. M3's ChainID pinning is the mechanism that
closes it.

Adds a fourth trap: staging an explicit path is NOT sufficient on this shared tree.
The live mission-control iteration rewrites its own SKILL.md while running, and
0881761 swept its in-flight edit while losing ours. Verify the file, then verify
git show HEAD:<path>.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@sunholo-voight-kampff
sunholo-voight-kampff merged commit 5a4dac7 into dev Aug 13, 2026
21 checks passed
@sunholo-voight-kampff
sunholo-voight-kampff deleted the sprint/iter193-mission-telemetry-m2m3 branch August 13, 2026 16:19
sunholo-voight-kampff added a commit that referenced this pull request Aug 13, 2026
…ied and landed (#697), #698 filed

Iteration 193 ran the full inner loop, committed 4 commits into the main checkout's dev, exited
rc=0, and left zero charter rows and zero log rows. Found by Gate 2's died-mid-flight sweep and
landed as inherited work rather than redone.

- charter: ITERATION 194 stamp; ITERATION 190 rotated to the archive (line-count invariant
  asserted, and the archive end asserted to have gained it)
- charter: a LANDED queue row for M-MISSION-LOOP-UNIFIED-TELEMETRY, which never had one
- log: entry 195, crediting the orphaned iteration 193
- dashboard: overwritten

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant