Skip to content

feat(worker): CCM-v0 lane B — economics-summary.json on every worker terminal - #49

Merged
thegeorgepu merged 15 commits into
mainfrom
economics/ccm-v0-emitter-v1
Sep 2, 2026
Merged

feat(worker): CCM-v0 lane B — economics-summary.json on every worker terminal#49
thegeorgepu merged 15 commits into
mainfrom
economics/ccm-v0-emitter-v1

Conversation

@thegeorgepu

@thegeorgepu thegeorgepu commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Wave 1 lane B of Cognitive Capital Measurement v0. Charter: docs/economics/CCM-V0-PROTOCOL.md on vinci-gpu-control main (#287). Ledger side: vinci-gpu-control #288.

What this does

  • vinci/worker/economics.mjs: buildEconomicsSummary, canonicalJson (sorted keys, integer micro-USD), economicsSha256. Never throws; missing inputs land in a closed incomplete[] vocabulary.
  • session-read.mjs: surfaces the persisted per-call vinci-task-usage entries, which of the three cost fallbacks answered, and whether crew helpers ran. Existing callers unchanged.
  • worker.mjs: a summary is built at every terminal — into the evidence bundle on the main path, and under <stateDir>/economics/<taskId>/ on early blockers, governor and branch-lease refusals, authority-lost, checkout-blocked and the catch-all FAILED. economics_sha256 travels as a structured field beside evidence_sha256, never spliced into blocker prose.
  • evidence.mjs: economics_summary and economics_sha256 added to the POST /v1/evidence body.

Known limitation, stated plainly

On the governed (CONTRACT-envelope) path the summary does not reach the ledger. evidence.mjs posts only when isLedgerRef(envelope.ref), and task.mjs sets ref: undefined for a contract envelope, so a governed attempt writes its summary to disk and never POSTs it. This PR does not change that gate: isLedgerRef governs all evidence posting, not just economics, and it deserves its own PR and its own adversarial review. Do not read this PR as "the ledger now receives worker economics" — it receives them on the prose/ledger-ref path only. The CCM Wave-1 integration gate stays open on this and on the packaged worker (#48).

Defects found and fixed during review

Two independent Claude reviews plus a live cross-lane review from projects-11 (bus msg_9438fe86):

  • Governed handoffs emitted work_order_id: null — the emitter read envelope.ref, which is undefined for contract envelopes while the real id sits in contract.work_order_id. That is exactly the field #288 keys acceptance on. Both PRs stayed green in isolation because every existing test drove a prose envelope. Fixed with contract-first precedence, a contract-envelope test whose fixture deliberately sets a different envelope.ref so the two sources can be told apart, and a mutation control.
  • A crashed emitter returned an identity-less stub, which the ledger records as boundary:missing_key → REFUSED against nothing, dropping that attempt's spend out of the denominator. The degraded summary now carries the resolved work order, lease, generation and a real digest.
  • The summary recorded zero tokens on every run (usage entries never read), a duplicated provider response double-counted tokens and cost, harness_stop carried raw tool output past the metadata-only boundary, session_id was a filename, and receipt-only cost silently became zero spend.
  • The digest broke the terminal-body contract: bodies are asserted byte-identical with reasons matched both ^- and $-anchored, so the digest moved into the structured field list.

Tests: 40 unit plus 7 worker integration files, including vinci/test/worker-economics-terminal.mjs, which drives worker.mjs start --once through the fixture bus to a real blocked terminal and checks the on-disk summary and the bus body. One skip with reason: the packaging test, blocked on #48.

Authorship: Vinci Code (module, wiring, first tests) and Claude (terminal coverage, receipt-keyed fields, dedup fix, governed-path fix, degraded path, reviews). Codex and OpenRouter lanes were unavailable.

🤖 Generated with Claude Code

thegeorgepu and others added 11 commits September 2, 2026 16:29
…Summary, economicsSha256

Add vinci/worker/economics.mjs exporting three functions for CCM-V0:
- canonicalJson(obj): deterministic sorted-key JSON, never throws
- buildEconomicsSummary(input): produces WorkOrderEconomicsSummary v1 with usage rollup/dedup, never throws
- economicsSha256(canonicalString): SHA-256 hex digest

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Expose which fallback provided the cost: 'outcome', 'usage_entries', or 'message_fallback'.
This is used by economics.mjs to fill cost_reconstruction field.
Existing callers unaffected (they ignore the new field).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… to ledger

- worker.mjs: Build economics summary before evidence upload, add to extraFiles
- evidence.mjs: Include economics_summary and economics_sha256 in POST /v1/evidence for ledger refs
- Terminal seam emits summary on all paths: DONE, BLOCKED, FAILED, UNVERIFIED, budget/deadline trip, harness stop

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add vinci/worker/test/economics.test.mjs with:
- 3 canonicalJson tests (determinism, sorting, robustness)
- 2 economicsSha256 tests (format, determinism)
- 2 summary structure tests (schema, error handling)
- 7 terminal state tests (DONE, BLOCKED, FAILED, limit_tripped, harness_stop, killed_before_outcome)
- 3 cost reconstruction tests (outcome, usage_entries, message_fallback)
- 2 lease tests (with_lease, no_lease)
- 3 usage rollup tests (dedup, accumulation, cost_basis)
- 1 malformed entry test
- 2 work field tests
- 1 complete scenario test
- 1 packaging test (skipped citing #48)
- 1 dedup mutation control

Uses Node's built-in test framework.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add test that verifies two entries with same responseId count as 1 model_call.
Mutation control: disable dedup logic, test fails with 2 != 1, restore via cp.
Proves the dedup mechanism works as intended.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add helper function to emit economics-summary.json into attempt dir.
Wiring to actual early terminal paths (contract error, deadline-in-past, clean-room)
needs to be completed with coordinator guidance on exact locations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cal_result, crew flag, digest in bus body

- session-read: usageEntries mapped from vinci-task-usage entries (was left
  uncommitted by the previous pass); crewRan when vinci-crew-helper/result
  entries exist
- economics: killed_before_outcome keyed to the vinci-task-outcome receipt,
  verification_state from the receipt, crew_unattributed when crew ran,
  responseId dedup counts a duplicated response once
- worker: emitEconomics on contract-error, invalid-bounds, past-deadline,
  provider-not-allowed, base_ref and clean-room terminals (written under
  <stateDir>/economics/<taskId>/ when no attempt dir exists);
  economics_sha256 token appended to every terminal bus body

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVK5S9ssoXtZYBhwcoWWT6
…roll-up to skip the whole entry

The new session test caught a real defect: a duplicated responseKey was
counted once for model_calls but its tokens and cost were summed twice.
The roll-up now skips the entire duplicate entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVK5S9ssoXtZYBhwcoWWT6
…t, checkout-blocked and catch-all FAILED terminals; session lookup by attempt session id

Review finding (Claude, self-review of lane B): six terminal sites still
returned without a summary, including the catch-all FAILED path where a
session may already have spent. emitEconomics also looked the session up by
task id; the session file is keyed by the attempt's session id.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVK5S9ssoXtZYBhwcoWWT6
…h, closed harness_stop token, caller-supplied session_id, receipt-only cost carried, no_session/none on pre-session terminals, per-response dedup, estimated cost_basis

Review (Claude, fresh agent) NO-GO findings 1-9 addressed:
- postFinal: BLOCKED/FAILED/harness-stop/UNVERIFIED branches now carry economics_sha256=
- harness_stop is instrument_stop:<count>, never the blocked tool call's text (R3)
- session_id is attempt.sessionId, not the session file name
- digest-path registry refusal emits a summary
- receipt-only or message-fallback cost becomes one estimated usage row plus
  usage_persistence_failed, never omitted as zero spend
- a terminal before any session ran reports no_session / cost_reconstruction none
  instead of a fallback that never ran or a kill that never happened
- main path also writes economics-summary.json into the attempt dir
- cost_basis/cost_confidence default to estimated; dedup is per response across rows;
  an over-long model name is malformed_entries
Tests updated to the new semantics with negative controls; 33+6 pass, 1 skip (#48).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVK5S9ssoXtZYBhwcoWWT6
…tegration test for a blocked terminal

- economics.mjs emits lineage (backlog_row_id from a bk_ ref), execution_world_ref,
  capacity_events, decision_refs, measurement_cost — nullable, each null carrying its
  closed incomplete[] code per charter §8.3
- early terminals read limit_tripped/exit_code from the lifecycle (run result is absent)
- vinci/test/worker-economics-terminal.mjs drives worker.mjs start --once through the
  fixture bus to a past-deadline BLOCKED terminal and asserts the on-disk summary
  (BLOCKED, limit deadline, no_session, no_lease, cost_reconstruction none, no usage)
  and that the bus body carries exactly its sha256 and no summary content

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVK5S9ssoXtZYBhwcoWWT6
@thegeorgepu

Copy link
Copy Markdown
Contributor Author

Independent review at exact head 066f2d5 (fresh Claude reviewer spawned by projects-11, neither asker nor builder): BLOCK. Full envelope on the bus thread under msg_72983480. Blocking: (1) usage[] rolls only the supplemental vinci-task-usage entries, so every main-loop model call and its cost is omitted (task-outcome.ts:576-579 places those in the assistant-message stream); (2) crew helper usage is summed into usage[] instead of being flagged; (3) local_result.harness_stop carries raw tool-result text across the R3 boundary instead of the closed instrument_stop: token; (4) the two failing offline CI jobs are caused by this PR — the appended economics_sha256= token breaks the end-anchored tail regex in vinci/test/worker-governor-fail-closed.mjs:107 (14 of 23 fail). Seven HIGHs (worker tests not wired into CI; main-path summary never written locally; cost_reconstruction mislabels its source; session_id is a filename; two terminal posts bypass the seam; packaging test is a skip, not red; no test reaches worker.mjs). The duplicate-responseKey fix is present and mutation-verified. §8.3 Revision-1 delta enumerated on the bus. Please hold un-draft; fixes are being prepared on a separate branch.

@thegeorgepu

Copy link
Copy Markdown
Contributor Author

Independent review (Claude, fresh agent): NO-GO at 066f2d5 — ten findings, addressed in f4149a2 and de30cc4.

# Finding Fix
1 economics_sha256= missing from BLOCKED/FAILED/harness-stop/UNVERIFIED bodies every postFinal branch carries it
2 harness_stop carried the blocked tool call's text (R3 breach; validator would refuse it) closed token instrument_stop:<count>
3 session_id was the session file name caller-supplied attempt.sessionId
4 digest-path registry refusal emitted nothing emits
5 receipt-only / message-fallback cost dropped → zero spend one estimated row + usage_persistence_failed
6 cost_reconstruction named a fallback that never ran; killed_before_outcome on never-spawned tasks none + no_session when no session exists
7 main path wrote no local file when evidence upload was unset written into the attempt dir too
8 cost_basis/cost_confidence null in production estimated
9 dedup per row, over-long model merged silently per response across rows; malformed_entries
10 no test reached worker.mjs vinci/test/worker-economics-terminal.mjs drives worker.mjs start --once through the fixture bus to a past-deadline BLOCKED terminal and checks the file and the bus digest

Also emits the charter §8.3 (Revision 1) fields with their codes. Tests: 34 + 6 unit, 7 worker integration files pass, 1 skip (#48). Fixes are builder-authored; the integration test is the discriminating control for the terminal-path claims.

…opy for standard runs

Found by the local end-to-end run against the lane A server (real worker,
real POST /v1/evidence): started_at was null on the main path (the run
result carries no timestamps; the lifecycle does), and the standard
(non-clean-room) run has no attempt dir so the local copy was never
written. Both fixed; the summary for job_e2e4 was ECONOMICS_ACCEPTED.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ules hold

CI caught it: worker-governor-fail-closed asserts BOTH that a terminal body
ends with ' worker_build=<x> vinci_binary=<y>' AND that several blocker
reasons match $-anchored against the body with that tail stripped. A token
appended after the reason broke the second; after the stamps it would break
the first. The digest now leads: economics_sha256=<64hex> <reason> <stamps>.
worker-economics-terminal additionally asserts the build tail still closes
the body. 23/23 fail-closed, 6 worker integration files, 40 unit tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a null ref

Found by projects-11 (bus msg_9438fe86), confirmed against this tree with
their reproduction and a mutation control. task.mjs:433 hard-codes
`ref: undefined` for a CONTRACT envelope and carries the real id in
contract.work_order_id; economics.mjs read only envelope.ref, so every
governed attempt emitted work_order_id: null plus incomplete ['missing'] —
on exactly the field vinci-gpu-control #288 keys acceptance on. Both PRs
stayed green because each is self-consistent alone and every existing test
drove a PROSE envelope; the failure exists only at the join.

The emitter now takes workOrderId (contract first, envelope ref second) on
the main path and in emitEconomics, and the degraded catch path uses the
same precedence. A contract-envelope test asserts it; the mutation (revert
to the ref-only read) fails that test and passes with the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thegeorgepu added a commit that referenced this pull request Sep 2, 2026
vinci/package.sh line 30 states that "tests, docs, infrastructure state, and
release tooling must never enter the public archive", but the only
test-shaped exclusion in the packaging path was a hardcoded
node_modules/ssh2/test, and package-entries.mjs tars first-party release
roots (vinci/worker, vinci/extensions, vinci/updater, vinci/themes,
vinci/assets, packages/*/dist) wholesale. Any first-party test directory
added under them shipped. Reported by projects-9f, routed via the
coordinator from projects-5a.

Latent, not live: no first-party test path ships today. `tar -tzf` of the
artifact built at this head matches no test-shaped first-party path, and no
such file exists on disk under any release root. PR #49 adds
vinci/worker/test/**, which would have been the first.

Fixed as a class rather than per path. A per-directory exclusion would leave
every future first-party test directory shipping while still reading as
closed — the same "enumeration wearing a policy's comment" shape as the
workspace gate repaired in the previous commit. vinci/scripts/
first-party-test-paths.mjs keys off the shape of the path instead: any
test/tests/__tests__/__mocks__/spec/specs segment, or a *.test.* / *.spec.*
basename. Scope is first-party only; paths inside node_modules stay governed
by the production dependency closure plus the ssh2 carve-out, because
pruning a dependency by name would change which bytes of a third-party
package the artifact carries.

The rule is applied on BOTH sides from that one predicate, which is not
optional: the checker requires every authority entry under vinci/worker, so
a producer-only exclusion would make every artifact fail as "required by the
trusted package layout is missing" (the pairwise failure this shared module
exists to prevent).
- package-entries.mjs excluded(): the entry list tar is given.
- packaged-artifact-check.mjs excludedFromReleaseAuthority(): the trusted
  release surface, so producer and verifier agree.
- packaged-artifact-check.mjs compareAuthorityDirectory(): a test path that
  did reach the archive is refused in its own words rather than as a generic
  authority mismatch, since that means the packaging rule regressed.

Controls (verbatim outputs in the PR body): with vinci/worker/test/
ws-c2-probe.test.mjs and vinci/updater/__tests__/ws-c2-probe.test.mjs
planted, `bash vinci/package.sh` before this change listed all four entries
in the tarball and after it lists none, while vinci/worker keeps its 21
shipped entries and the artifact still certifies. Both new tests fail under
the per-path mutation named in their docstring; they are pinned separately
because the producer and the artifact-direction refusal are different
guards and one mutation does not fail both.
projects-11 (bus msg_9438fe86): the emit helper's catch returned a stub with
no work_order_id, so #288's validator records boundary:missing_key ->
ECONOMICS_REFUSED against nothing, and the attempt's spend leaves the
denominator entirely (charter §8.1) — while looking like poster fault. The
degraded summary now carries the resolved work_order_id, lease_id,
fencing_generation and a real digest, and says malformed_entries.

Also de-aliases the contract-envelope fixture: it now sets envelope.ref to a
DIFFERENT value than workOrderId, so the test can actually tell the two
sources apart instead of passing because they coincide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thegeorgepu
thegeorgepu marked this pull request as ready for review September 2, 2026 22:56
@thegeorgepu
thegeorgepu merged commit 4af5f2b into main Sep 2, 2026
6 checks passed
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.

2 participants