@junghanacs/entwurf 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -104,7 +104,9 @@ A `--entwurf-control` pi session is a citizen for the same reason a native bridg
104
104
 
105
105
  ## Issue queue — a manual sweeper, not a backlog
106
106
 
107
- - OPEN issues are live contracts only, capped at five. A sixth means something closes first; the cap does not bend.
107
+ - **Two caps, and the inner one is what disciplines the work.** OPEN issues are capped at **ten** in total; of those, **implementation issues — the ones that will go out on a branch — are capped at five**. A sixth implementation issue means one closes first; that inner cap does not bend.
108
+ - **What sits outside the implementation five:** research issues, and issues GLG keeps open to look at. These are not slots to be earned or swept — a research lane may stay open across releases, and a north-star issue other repos read may never close. They still count toward the ten, so the queue cannot grow without limit.
109
+ - Classify by destination, not by size: if closing it produces a diff, it is an implementation issue. If closing it produces a decision, a document, or nothing, it is not.
108
110
  - A slot is earned by a current defect or executable contract, not by importance, age, or possible future value.
109
111
  - Direction, philosophy, frozen invariants and observations without a current action live in `ROADMAP.md`, durable docs, or closed history.
110
112
  - Nothing stays open "in case." If a closed problem recurs, it returns as a new issue carrying the new evidence.
@@ -140,7 +142,7 @@ LIVE=1 AGY_CONVERSATION_ID=<id> ./run.sh smoke-agy-native-push-live
140
142
  - The deterministic floor is tiered (#70). `pnpm check` is the everyday core — toolchain (lint + typecheck), the vitest lanes, and the pure-unit / behavioral-contract / source-topology gates plus the cheap static coherence checks. It prints its own total wall time; acceptance is ≤60s on the reference host `oracle` (an operator measurement, never a hard wall-clock gate on arbitrary hosts). `pnpm run check:full` is the full deterministic floor — the core plus the hermetic-integration and package/install tiers — and is what the candidate protocol, push CI, release-gate, and `prepublishOnly` run. Exact membership is the named `check:*` group scripts in `package.json` (the executable SSOT); a gate changes tier by semantic-class decision, never because it happened to get faster or slower. Neither tier includes `check-gate-qualification`, which is scheduled separately (below).
141
143
  - **Kill-proof discipline (gate qualification).** A gate is a test only if re-planting a closed defect turns it red for the claimed reason. `check-gate-qualification` proves that automatically: committed mutants in `scripts/mutants/` must be KILLED at their `[QK:<claim>]` signature inside an isolated snapshot repo (control→mutant→restore→control; the real checkout is never written). Gates a release touches carry such manifests; assertion counts are never evidence — claim IDs + killed mutant IDs are. `check-agy-permission-matrix` holds the enumerated permission contract space; matrix cells change by axis/rule edits, never by appending cases.
142
144
  - **When changing a contract/gate:** name the production subject and an oracle independent of it; give the failing assertion a stable `[QK:<claim>]` label and add/update the exact-once mutant in `scripts/mutants/*.json`; if the contract is combinatorial, update the literal matrix axes/cells/exclusions together with their declared counts; then verify the focused gate, and let qualification and the full floor follow the scheduling contract below — once on the frozen candidate, not once per amendment. `MUTANT-STALE`/`SURVIVED`/`WRONG-REASON`/`CONTROL-RED`/`HANG`/`IMPURE` are red — never substitute an assertion count for a kill.
143
- - Run LIVE gates with `PWD` in scratch so session artifacts do not land in the repo.
145
+ - Run LIVE gates with `PWD` in scratch so session artifacts do not land in the repo. Strip `CLAUDE_CONFIG_DIR`, `PI_SESSION_ID`, and `PI_AGENT_ID` from the gate process — a live pi/ACP session exports them into children, where they strip Claude hooks or capture a fresh-call callback.
144
146
  - Release acceptance and evidence levels are defined in [VERIFY.md](./VERIFY.md); recorded host evidence is in [BASELINE.md](./BASELINE.md).
145
147
  - A failed gate or evidence downgrade blocks commit/release. Pipes can be connected and the water can still taste wrong.
146
148
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,218 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.17.1 - 2026-09-03
8
+
9
+ ### Fixed
10
+
11
+ - **The C1b red that blocked two of the three 0.17.0 `--cut` runs: a LIVE smoke was waiting exactly
12
+ as long as pi's lock-stale window, and losing by 148ms.** The cause is measured end to end, and
13
+ none of it is a product regression:
14
+ 1. pi guards `auth.json` AND `models-store.json` with `proper-lockfile` and reads through that
15
+ lock on every boot (`dist/core/auth-storage.js`), retrying a held lock for `staleMs = 30_000`
16
+ before taking it over.
17
+ 2. `terminateChild`'s SIGTERM ends a resident before `proper-lockfile`'s release ever runs, so a
18
+ kill that lands inside the lock window orphans the lock directory. Sweeping 24 kill offsets
19
+ across a boot reproduced it once, at +375ms.
20
+ 3. With `~/.pi/agent/models-store.json.lock` orphaned, the next boot → V3 record measured
21
+ **30_148ms** — and **1_114ms** immediately afterwards, once the stale takeover had cleared it.
22
+ 4. `smoke-entwurf-v2-matrix-live`'s `BOOT_TIMEOUT_MS` was `30_000`: it stopped looking 148ms
23
+ before C1b's record landed. Hence the exact signature the blocked cuts left — empty stderr, no
24
+ record in any store, and (confirmed in both runs from the host's own process-audit trail) a
25
+ child that was alive for the entire 30s and was killed by the smoke at +30s.
26
+ The control was in the same three runs: `smoke-entwurf-chain-live` does the same two-resident
27
+ dance with `BOOT_TIMEOUT_MS = 45_000` and passed 3/3 while matrix-live failed 2/3.
28
+ - **The bound is now shared and carries its receipt.** `PI_BOOT_TIMEOUT_MS = 45_000` lives in
29
+ `scripts/lib/pi-record-discovery.ts` next to the measurements above, and the five smokes that
30
+ sat on the 30s cliff (`smoke-entwurf-v2-matrix-live`, `smoke-acp-socket-citizen-live`,
31
+ `smoke-acp-bundled-mcp-live`, `smoke-acp-v2-send-live`, `smoke-acp-cortex-live`) now derive
32
+ from it. 30s was the single worst value available: it expires *inside* the takeover.
33
+ `smoke-resident-garden-guard` already used 90s and needed no change.
34
+ - **A boot overrun now names the lock.** `describePiLockResidue()` reports which pi locks are held
35
+ at failure time — read-only, because a live holder and an orphan look identical from outside and
36
+ only pi's own stale protocol may arbitrate them.
37
+ - **Both blocked smokes now say WHICH failure they hit**, which is what made the cause findable.
38
+ `smoke-entwurf-v2-matrix-live` watches each resident (pid, exit code/signal and how many ms in, a
39
+ signal-0 liveness probe taken in the catch before the reaper runs, and a **per-child** stderr tail
40
+ — the old single shared buffer could not say whether C1 or C1b spoke; an empty tail now prints as
41
+ `(empty)` rather than being skipped). `smoke-mux-lifecycle-live` attaches window forensics when a
42
+ nonce callback never comes — pane-pid liveness, `list-panes`, and the last 40 lines of
43
+ `capture-pane`, for both cells. Its launch receipt already tells a human "the window is visible
44
+ and can be read directly"; on a headless gate nobody is there and the window is torn down seconds
45
+ later. Every diagnostic step is best-effort so it can never become the failure.
46
+
47
+ The mux pi-native nonce timeout (the second blocked cut) is NOT this bug and stays open: 300s is
48
+ ten times the stale window, and the codex rail was healthy in that same run (`chain-live` and
49
+ `smoke-omp-fresh-live` both passed on it). The forensics above are what the next occurrence will
50
+ answer with.
51
+
52
+ ### Verification
53
+
54
+ - `pnpm run check:toolchain` (biome + `tsc` ×3) — green.
55
+ - **The repair measured against the exact failing condition.** With `models-store.json.lock`
56
+ planted as an orphan, `LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live` is **17/17 PASS in 34s** —
57
+ the stale wait absorbed. The same condition measured 30_148ms to birth, i.e. red under the old
58
+ 30_000 bound.
59
+ - **Boot cost, undisturbed**: 1008–1212ms across 80 consecutive boots in the smokes' spawn shape
60
+ (5.1–5.4s under 4× CPU oversubscription). The bound is not sized for boot cost; it is sized to
61
+ clear pi's stale window.
62
+ - **The new matrix-live diagnostic exercised on a real failure path**, at 0 model tokens, by
63
+ pointing the smoke at a bogus provider: `resident C1: pid=… EXITED code=1 signal=null at +1057ms
64
+ — it was gone before the wait ended`, with that child's own stderr beneath it.
65
+ - `LIVE=1 ./run.sh smoke-entwurf-v2-matrix-live` — **17/17 PASS** on the real
66
+ `openai-codex/gpt-5.6-luna` target (undisturbed run).
67
+ - `LIVE=1 ./run.sh smoke-mux-lifecycle-live` — **81 checks passed, exit 0** (real model turns on
68
+ both pi cells and the claude-code cell).
69
+ - 15 iterations of the gate's own C1b neighbourhood (`smoke-resident-garden-guard` → `check-bridge`
70
+ → `doctor-pi-provider` → `smoke-entwurf-v2-matrix-live`) reproduced nothing — recorded because it
71
+ is what ruled out host load and prior-smoke residue and sent the search to pi's lock.
72
+ - **`LIVE=1 ./run.sh release-gate /tmp/entwurf-release-gate-0.17.1.hb5Q5j --cut`** — **MUST
73
+ PASS=23 FAIL=0 SKIP=0**, **BEHAVIOR PASS=1 FAIL=0 SKIP=0**, exit 0, `cut: OK`. Run on `665191d`,
74
+ 2026-09-03 09:46:37 → 10:35:42 KST, with `env -u CLAUDE_CONFIG_DIR -u PI_SESSION_ID -u
75
+ PI_AGENT_ID`. Log: `/tmp/entwurf-release-gate-0.17.1.hb5Q5j/release-gate.log`. This carried
76
+ `pnpm run check:full` (451s) and `./run.sh check-gate-qualification` (**347/347 KILLED**) as MUST
77
+ steps. **This is the green `--cut` 0.17.0 never got**: both cells that blocked it — C1b in
78
+ `smoke-entwurf-v2-matrix-live` and the pi-native nonce in `smoke-mux-lifecycle-live` — passed
79
+ here, on the same host, in one run.
80
+ - **Exact-SHA CI on `665191d`** — `check` (34m20s, carrying `check:full` and
81
+ `check-gate-qualification`), `install-surface`, `artifact-consumer`, all `success`.
82
+ Run: https://github.com/junghan0611/entwurf/actions/runs/33697821117
83
+
84
+ ## 0.17.0 - 2026-09-02
85
+
86
+ ### Verification
87
+
88
+ Each receipt carries its own scope; none of them is transferable to another commit or host.
89
+
90
+ - **`LIVE=1 ./run.sh release-gate <scratch> --cut`** — **MUST PASS=23 FAIL=0 SKIP=0**,
91
+ BEHAVIOR PASS=1 FAIL=0 SKIP=0, exit 0, `cut: OK`. Run on commit `0379764`, tracked tree
92
+ `cb1a3dc69b521909f0fe6f956e5c0fa36e885d5955f91271dd8e63d30cd918fb`, 2026-09-02
93
+ 20:14:51 → 21:04:41 KST. Frozen log: `/tmp/entwurf-0.17.0-evidence/ACCEPTED-cut-release-gate.log`.
94
+ This run carried `pnpm run check:full` and `./run.sh check-gate-qualification`
95
+ (**346/346 KILLED**) as MUST steps. **It is not the 0.17.0 acceptance SHA:** `9479750`
96
+ changed source after it.
97
+ `CHANGELOG.md` is in `package.json` `files` and `check:full → check:package → check-pack`
98
+ enumerates it (`npm pack --dry-run`, required/forbidden names — not a cardinality lock).
99
+ `NEXT.md` is not in `files`. Working-tree cleanliness at cut start was an operator
100
+ observation, not a line in the gate log.
101
+ - **Four reds along the way, all real, none smoothed.** A formatter rejection of the new mutant
102
+ manifest; a doc-floor gate catching the ROADMAP ledger entry quoting the ADAPTER's
103
+ `engines.node ">=22"` as though it were entwurf's own; a qualification run that aborted RED on its
104
+ own self-fence because the worktree was edited mid-run — kept in the record because a
105
+ qualification predating the final bytes is not a receipt, and the fence is what says so; and
106
+ `check-bridge-delivery` failing "artifact is not stale" after the review amendment edited three
107
+ `.ts` files without rebuilding the MCP bridge. That last one is why the tree hash above is quoted
108
+ twice: `mcp/entwurf-bridge/dist/` is gitignored, so the rebuild moved no candidate byte, and the
109
+ two receipts really do cover the same tree. None of the four would have been caught by typecheck
110
+ plus the focused gate.
111
+ - **The first `--cut` attempt was RED — MUST PASS=18 FAIL=5 SKIP=0 — and every one of the five was
112
+ the operator's own environment, not this release.** Recorded because the failure wore a
113
+ convincing disguise: it read as a broken host. `static` was the stale bridge artifact above,
114
+ re-created when a `git checkout`/`merge` bumped every tracked `.ts` mtime past the built `dist`.
115
+ The other four came from running the gate **inside a live entwurf ACP Claude session**, which
116
+ exports two carriers into every child it spawns. `CLAUDE_CONFIG_DIR` points at the ACP overlay,
117
+ whose `settings.json` is `hooks: {}` by design — so the Claude Code children the live smokes
118
+ launch are born with no entwurf SessionStart hook and mint no meta-record, failing
119
+ `smoke-claude-native-resume-live`, `smoke-entwurf-chain-live` and `smoke-mux-lifecycle-live` on
120
+ citizen birth. The same variable made `doctor-meta-bridge` report `installed: absent` and
121
+ `claude mcp list` deny `entwurf-bridge`; with it unset the doctor reads
122
+ `installed: v3 (229fef123589)` matching source and assembled exactly. `PI_SESSION_ID` did the
123
+ analogous damage on the omp axis: `smoke-omp-fresh-live` reported a 240s nonce-callback timeout,
124
+ but the sibling was alive — it was born at 19:58:21 KST and called back at 19:58:28 to the
125
+ AMBIENT caller id rather than the caller the smoke had minted, so the smoke watched the right
126
+ mailbox and saw nothing. That smoke's own source anticipates this exact disguise
127
+ (`scripts/smoke-omp-fresh-live.ts:186`, "the failure would masquerade as a silent sibling").
128
+ Re-run with `env -u CLAUDE_CONFIG_DIR -u PI_SESSION_ID -u PI_AGENT_ID`, all five turned green and
129
+ nothing else changed. That gap — VERIFY.md named `PWD` in scratch but not the carriers — is what
130
+ cost the first cut its 52 minutes; VERIFY.md and AGENTS.md now name the strip.
131
+ - **Qualification lane size.** On the `0379764` cut the total was **335 → 346**, eleven new claims
132
+ in `scripts/mutants/acp-usage-accounting.json` (absent on the 0.16.1 base). `9479750` adds a
133
+ twelfth, `ACP-REBILL-MAIN-LOOP-SCOPE`. Standalone `./run.sh check-gate-qualification` on
134
+ `fee89d3` is **347/347 KILLED** (36m19s, log `/home/junghan/.pi/background/1788355846036-bg03.log`).
135
+ That is a qualification receipt, not a `--cut` receipt.
136
+ - **Three recuts on the D1 tree, all `cut: BLOCKED`.** Each ran
137
+ `env -u CLAUDE_CONFIG_DIR -u PI_SESSION_ID -u PI_AGENT_ID LIVE=1 ./run.sh release-gate <scratch> --cut`.
138
+ All three: MUST PASS=22 FAIL=1 SKIP=0, BEHAVIOR PASS=1. None of the FAILs is D1.
139
+ 1. `/tmp/entwurf-cut-0.17.0.WdTkH7` — `smoke-entwurf-v2-matrix-live` C1b: second
140
+ `pi --entwurf-control` wrote no hidden-store record in 30s, stderr empty. Isolated re-run:
141
+ 17/17 PASS.
142
+ 2. `/tmp/entwurf-cut2-0.17.0.XQcOuR` — `smoke-mux-lifecycle-live` pi-native nonce
143
+ `mux-fresh-call-4bd6391e9783fb24fb1ee74a` never arrived in 300s (claude-code cell passed;
144
+ v2-matrix passed this run). Same shape as the 0.16.1 mux-lifecycle retry.
145
+ 3. `/tmp/entwurf-cut3-0.17.0.sfZ8WC` — C1b 30s again. Isolated re-run: 17/17 PASS.
146
+ mux-lifecycle PASS this run.
147
+ GLG 2026-09-03: no fourth cut. Make proceeds with this gap named here, not rounded up to
148
+ `cut: OK`.
149
+ - **`smoke-acp-raw-turn-live`** — PASS on the moved pin, quoted from the accepted `--cut` run's own
150
+ output rather than from a session message: launch source
151
+ `package:@agentclientprotocol/claude-agent-acp` (not PATH fallback), `protocolVersion=1`, model
152
+ `claude-sonnet-5`, `stopReason=end_turn`, reply `"OK"`, 62,998 bytes NDJSON. This is the dep-bump
153
+ track's own named lock, not a substitute for it. An earlier standalone run of the same smoke
154
+ reported 58,189 bytes and another 58,178; the byte count is per-run and is not a fixed
155
+ fingerprint, which is precisely why the number cited here is the one the gate printed.
156
+ - **Recurrence corpus measurement** — cited from the in-source comment at
157
+ `pi-extensions/lib/acp/backend.ts` (`priorTurnInputOutputSum`): 2,398 of 2,410 adjacent pairs
158
+ (99.50%), 2026-05 → 2026-09. The raw pair inventory is not in the frozen receipts; treat it as
159
+ an operator observation recorded in source, not as a cut receipt.
160
+
161
+ ### Changed
162
+
163
+ - **ACTION REQUIRED — ACP accounting consumers must read `usage.acp`.** Claude ACP
164
+ `PromptResponse.usage` is the sum across a turn's API round trips, not one request's
165
+ prompt shape. The turn's accounting totals now travel at
166
+ `usage.acp.{input,output,cacheRead,cacheWrite}`; pi's four request-shaped fields remain
167
+ zero, while `usage.totalTokens` remains the vendor's context-occupancy reading. Consumers
168
+ that display ACP token/cache accounting must read the new key — `agent-config`'s
169
+ `pi-extensions/glg-footer.ts` does so at `47b9b95`. The numerator is taken from the
170
+ vendor's ACCOUNTING-GRADE `_meta.quota.model_usage` rows (summed) in preference to the
171
+ main-loop-only `PromptResponse.usage`, because the cost denominator is an adjacent diff of
172
+ the backend's running total and already has that wider scope. Deterministic receipts:
173
+ `scripts/check-acp-usage-accounting.ts` CELLs 1/1b drive pi's real overflow and context
174
+ readers with the incident-scale 4,185,084 cache-read aggregate on a 223,516-token context,
175
+ and CELL 1e drives both token carriers at once so a silent fallback to the narrow one
176
+ cannot pass for a preference.
177
+ - **ACTION REQUIRED — the certified Claude ACP dependency coordinates move together, and
178
+ this release REQUIRES them.** The bundled Claude adapter is
179
+ `@agentclientprotocol/claude-agent-acp` **0.70.0 → 0.73.0**, its wire SDK is
180
+ `@agentclientprotocol/sdk` **1.3.0 → 1.4.0**, and the resolved transitive
181
+ `@anthropic-ai/claude-agent-sdk` is **0.3.232 → 0.3.257**. This is not a refresh riding
182
+ along with the fix: `_meta.quota.model_usage` does not exist before adapter 0.71.0 (added
183
+ by upstream `fad4d10`, "report per-model token usage on prompt responses"), so the
184
+ accounting above has no accounting-grade numerator without this bump. Three adapter minors
185
+ are folded into one certification; the per-bump measurement — declared-dependency deltas
186
+ per tag, the zod floor narrowing at 0.71.0, lock peer-resolution, the reachable/unreachable
187
+ split of the new surface, and the re-measured MCP readiness boundary — is recorded in the
188
+ ROADMAP "Dep bump(별도 트랙)" ledger. Re-run the ACP support gates on any locally
189
+ overridden adapter command before treating that command as covered by these coordinates.
190
+
191
+ ### Fixed
192
+
193
+ - **Claude ACP no longer presents a turn aggregate as one request to pi.** That projection
194
+ caused pi's raw overflow reader to compact a live 223,516-token session under a
195
+ 1,000,000-token window, invented two phantom cache misses, and silenced the one real miss
196
+ after a 401-minute idle gap. The aggregate is retained as accounting evidence, context
197
+ occupancy remains separate and is carried forward across a turn that reports none, and a
198
+ material re-billed prefix is reported as a PROVEN LOWER BOUND rather than silently hidden.
199
+ Turn cost remains the adjacent difference of the SDK's cumulative estimate, never a local
200
+ reprice. A backend with no measured semantics (cortex) is still sealed not at all.
201
+ - **The re-billed-prefix bound no longer mixes main-context occupancy with wide accounting IO.**
202
+ `readTurnAccounting` prefers `_meta.quota.model_usage`, whose rows also count Task subagents,
203
+ sidechains, and internal compaction. Feeding that `cacheWrite` into a bound whose occupancy is
204
+ main-context inflates the lower bound through both remaining terms, so a warm main prefix can
205
+ announce a miss and attach this turn's dollar figure to it. The bound and its stored prior now
206
+ read `PromptResponse.usage` (main loop) only; `usage.acp` stays the wide totals. Deterministic
207
+ oracle: `scripts/check-acp-usage-accounting.ts` CELL 1f.
208
+ - **A backwards session total no longer asserts a cause it has not measured.** The operator
209
+ diagnostic for a decreasing cumulative cost previously named a conversation reset as "the
210
+ known cause". The adapter's `conversation_reset` handler only switches to a fresh
211
+ conversation and touches no cost (0.73.0 `dist/acp-agent.js:3675-3682`); the documented
212
+ mechanism lives in claude-agent-sdk instead, which states that a mid-session `/clear`
213
+ resets the running total (`sdk.d.ts:4884`). The notice now reports the observation, names
214
+ `/clear` in the vendor's own word rather than paraphrasing it into a different noun, states
215
+ explicitly that the adapter's `conversation_reset` event is NOT that mechanism, and says plainly
216
+ that the cause is not measured here. Naming the disproven event as the documented cause was the
217
+ release's own thesis being violated inside its own diagnostic; it was caught in review.
218
+
7
219
  ## 0.16.1 - 2026-09-01
8
220
 
9
221
  ### Verification
package/VERIFY.md CHANGED
@@ -165,8 +165,15 @@ cd "$REPO_DIR" && ./run.sh setup "$PROJECT_DIR"
165
165
 
166
166
  ### 1.2 Live acceptance (optional)
167
167
 
168
+ Put `PWD` in scratch so session artifacts do not land in the repo. LIVE gates inherit
169
+ the caller's environment: a pi or ACP session exports `PI_SESSION_ID` / `PI_AGENT_ID` /
170
+ `CLAUDE_CONFIG_DIR` into every child, which masquerades as the smoke's identity or
171
+ strips Claude hooks (`hooks: {}` overlay). Strip those carriers:
172
+
168
173
  ```bash
169
- LIVE=1 ./run.sh release-gate /path/to/consumer-project --cut
174
+ cd /path/to/scratch
175
+ env -u CLAUDE_CONFIG_DIR -u PI_SESSION_ID -u PI_AGENT_ID \
176
+ LIVE=1 /path/to/entwurf/run.sh release-gate /path/to/scratch --cut
170
177
  pi --provider entwurf --model claude-sonnet-5 -p "reply with ok only" # one-turn smoke
171
178
  ```
172
179
 
@@ -92,11 +92,11 @@ undifferentiated "supported" column is what let a Claude PASS read as if it also
92
92
 
93
93
  | Surface | Declaration | Class | What a green actually says |
94
94
  |---|---|---|---|
95
- | Entwurf package | `0.15.1` | shipped baseline | the package contract these rows belong to |
96
- | pi runtime | devDep exact `0.84.3`, peer `>=0.84.3 <0.85` | **exact** oracle + **closed range** | built and certified against 0.84.3; hosts inside the range are accepted, and the ceiling moves only on measurement |
97
- | ACP wire SDK | `@agentclientprotocol/sdk 1.3.0` | **exact** | the shared wire oracle both adapters speak |
98
- | Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.70.0` | **exact**, bundled | the adapter we ship and certify; resolved before any PATH fallback |
99
- | Claude Agent SDK | `0.3.232` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
95
+ | Entwurf package | `0.17.0` | shipped baseline | the package contract these rows belong to |
96
+ | pi runtime | devDep exact `0.84.4`, peer `>=0.84.4 <0.85` | **exact** oracle + **closed range** | built and certified against 0.84.4; hosts inside the range are accepted, and the ceiling moves only on measurement |
97
+ | ACP wire SDK | `@agentclientprotocol/sdk 1.4.0` | **exact** | the shared wire oracle both adapters speak |
98
+ | Claude ACP adapter | `@agentclientprotocol/claude-agent-acp 0.73.0` | **exact**, bundled | the adapter we ship and certify; resolved before any PATH fallback |
99
+ | Claude Agent SDK | `0.3.257` (transitive) | **exact** oracle | the runtime risk surface behind the adapter |
100
100
  | Anthropic SDK | `0.100.1` | **exact**, peer-resolution only | satisfies the Agent SDK peer floor (0.93.0+); never an API client here (gate L4) |
101
101
  | Claude Code runtime | `>=2.1.217` (`entwurf.claudeCodeFloor`) | **floor** | below it, hook args are silently dropped; entwurf enforces this itself |
102
102
  | Node | `>=24` (`engines.node`) | **floor** | single axis, derived everywhere else |
@@ -141,8 +141,10 @@ different reasons, and collapsing them would hide a real risk**:
141
141
  - **Capability-gated.** AIR typed session failures, the 0.69.0 AIR file-change report, terminal
142
142
  output widgets and nested subagent transcripts each test a client capability entwurf does not
143
143
  send, so the adapter itself keeps the legacy path.
144
- - **Advertised but never called.** Some surfaces carry no capability prerequisite at all — 0.70.0's
145
- `providers/list` / `providers/set` / `providers/disable` are advertised unconditionally. They are
144
+ - **Advertised but never called.** Some surfaces carry no capability prerequisite at all — the
145
+ `providers/list` / `providers/set` / `providers/disable` trio added in 0.70.0 is advertised
146
+ unconditionally, and 0.71.0–0.73.0 add native subagents, async tasks, message-specific session
147
+ forks, AI-generated session titles and permission-mode kinds on the same footing. They are
146
148
  unreachable only because the common loop never invokes them (nor `logout`). Nothing upstream
147
149
  enforces that; it is our own call-site discipline, and it stops holding the moment we use one.
148
150
 
@@ -255,13 +257,22 @@ caller-session `_meta`, and cross-machine certification.
255
257
 
256
258
  A backend can return `newSession` before its declared MCP server is callable. This was
257
259
  observed intermittently on the Claude rail and directly on Cortex's private `mcp.json`
258
- path. Neither `claude-agent-acp` 0.70.0 nor the Cortex landing adds a client-side
259
- readiness fence, and `mcpServerStatus()` is not called by the common loop.
260
- (Re-measured at the 0.68.0 → 0.70.0 bump: AIR typed failures and the 0.69.0 AIR
261
- file-change report are both capability-gated and unadvertised by entwurf; 0.70.0's
262
- `providers/set` / `providers/disable` are advertised unconditionally by the adapter but
263
- entwurf calls neither, so no session's provider state is transitioned; goal extension is
264
- still not a fence. This bump changes no readiness behavior and closes no part of #72.)
260
+ path. Neither `claude-agent-acp` 0.73.0 nor the Cortex landing adds a client-side
261
+ readiness fence over a session's declared MCP servers, and entwurf's common loop
262
+ calls `mcpServerStatus()` nowhere.
263
+ (Re-measured at the 0.70.0 0.73.0 bump, not inherited from the previous one.
264
+ `mcpServerStatus` call sites in `src/acp-agent.ts` went 0 2, new in 0.71.0 via
265
+ `0cbbaf3` (MCP OAuth, LLM-25012) — so the ADAPTER now calls it where it previously
266
+ did not. Both sites were read at `v0.73.0 src/acp-agent.ts:1618` and `:1711`: each is
267
+ gated behind `supportsMcpOAuth(query)`, and the polling one waits only on a server
268
+ that already reported `needs-auth`, never on every declared server before
269
+ `newSession` returns. That is an auth handshake, not a readiness fence, so the
270
+ boundary below is unchanged. The other reachable-surface findings also re-measured:
271
+ AIR typed failures and the AIR file-change report stay capability-gated and
272
+ unadvertised by entwurf; `providers/set` / `providers/disable` stay advertised
273
+ unconditionally and uncalled; native subagents, async tasks, session forks, session
274
+ titles, permission-mode kinds and clear-context planning are all new-but-uncalled.
275
+ This bump changes no readiness behavior and closes no part of #72.)
265
276
 
266
277
  ### 11-7-a/b. Instrument and first measurement
267
278
 
@@ -16,7 +16,7 @@
16
16
  // This module owns the `AcpConnectionLike` seam so the backend and the live
17
17
  // smokes both drive ONE adapter — the SDK method-name mapping lives here only,
18
18
  // and the backend's orchestration + the gate fakes stay untouched.
19
- import { AGENT_METHODS, CLIENT_METHODS, client } from "@agentclientprotocol/sdk";
19
+ import { AGENT_METHODS, CLIENT_METHODS, client, } from "@agentclientprotocol/sdk";
20
20
  /**
21
21
  * Production factory — wrap the SDK 1.1 fluent `client()` into the
22
22
  * `AcpConnectionLike` seam the backend (and the live smokes) drive.
@@ -121,6 +121,13 @@ export const claudeAdapter = {
121
121
  // `_adapterSettings` here.
122
122
  return {};
123
123
  },
124
+ // MEASURED (2026-09-02, #93): claude-agent-acp's `usage_update` carries a
125
+ // session-cumulative ESTIMATED cost (`cost.amount = message.total_cost_usd`)
126
+ // and a context-occupancy scalar (`used`; both read at 0.73.0
127
+ // `dist/acp-agent.js:2918-2924`). Both are measured, so backend.ts seals them.
128
+ // The token partition is NOT declared here: ACP's only token carrier is a
129
+ // per-turn round-trip aggregate, which is not what pi's four fields mean.
130
+ sealsTurnAccounting: true,
124
131
  };
125
132
  // ---------------------------------------------------------------------------
126
133
  // Registry — modelId → adapter
@@ -259,6 +266,14 @@ export const cortexAdapter = {
259
266
  const settings = adapterSettings;
260
267
  return { cortexConnection: settings?.cortexConnection ?? null };
261
268
  },
269
+ // NO sealsTurnAccounting — a DELIBERATE, permanent absence, not an omission to be
270
+ // filled in later by symmetry with claude. Nobody has measured what cortex's
271
+ // ACP `usage` (and its usage_update cost, if it sends one) MEAN: whether the
272
+ // token counts are a turn delta or a session total, and against which price
273
+ // table. Until that measurement exists, the honest report is no report:
274
+ // backend.ts seals nothing for a backend without this flag, so cortex's
275
+ // emitted usage is byte-identical to what it was before #93. Declaring this
276
+ // with a guess would mint exactly the silent misaccounting #93 exists to end.
262
277
  };
263
278
  const ADAPTERS = [claudeAdapter, cortexAdapter];
264
279
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
4
4
  "description": "Garden-citizen dispatch substrate and meta-bridge for Claude Code, Codex, Antigravity, and pi harnesses.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -82,8 +82,8 @@
82
82
  "claudeCodeFloor": ">=2.1.217"
83
83
  },
84
84
  "dependencies": {
85
- "@agentclientprotocol/claude-agent-acp": "0.70.0",
86
- "@agentclientprotocol/sdk": "1.3.0",
85
+ "@agentclientprotocol/claude-agent-acp": "0.73.0",
86
+ "@agentclientprotocol/sdk": "1.4.0",
87
87
  "@anthropic-ai/sdk": "0.100.1",
88
88
  "@modelcontextprotocol/sdk": "^1.12.1",
89
89
  "zod": "^3.25.0 || ^4.0.0"
@@ -165,7 +165,7 @@
165
165
  "check:toolchain": "pnpm lint && pnpm typecheck",
166
166
  "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface",
167
167
  "check:contracts": "./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-lock && ./run.sh check-entwurf-v2-decider && ./run.sh check-entwurf-v2-matrix && ./run.sh check-entwurf-v2-release && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-send-fallback && ./run.sh check-entwurf-v2-mailbox && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-runner && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-entwurf-resume-args && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-entwurf-facts && ./run.sh check-control-socket-path && ./run.sh check-socket-discovery && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment",
168
- "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex",
168
+ "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex",
169
169
  "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
170
170
  },
171
171
  "pi": {
@@ -17,14 +17,68 @@
17
17
  // smokes both drive ONE adapter — the SDK method-name mapping lives here only,
18
18
  // and the backend's orchestration + the gate fakes stay untouched.
19
19
 
20
- import { AGENT_METHODS, CLIENT_METHODS, client, type Stream } from "@agentclientprotocol/sdk";
20
+ import {
21
+ type Usage as AcpWireUsage,
22
+ AGENT_METHODS,
23
+ CLIENT_METHODS,
24
+ client,
25
+ type Stream,
26
+ } from "@agentclientprotocol/sdk";
21
27
  import type { AcpTextBlock } from "./context.js";
22
28
 
29
+ /**
30
+ * What `session/prompt` answers with — entwurf's own narrow view of the SDK's
31
+ * `PromptResponse`.
32
+ *
33
+ * `stopReason` stays `string | undefined` DELIBERATELY, not the SDK's closed
34
+ * `StopReason` union: backend.ts's verdict mapping owes an honest answer for an
35
+ * unknown or absent reason (it seals those as errors), and typing the field as
36
+ * the closed union would make that branch look unreachable.
37
+ *
38
+ * `usage` reuses the SDK's own `Usage` type rather than a hand-copy, so a field
39
+ * rename upstream is a typecheck failure here instead of a silent zero. It is
40
+ * marked `@experimental` upstream and its per-field comments say "across all
41
+ * turns/session" while the outer one says "for this turn" — that contradiction
42
+ * is why NO common code interprets this shape — and #93 then measured that the
43
+ * counts are a per-turn ROUND-TRIP AGGREGATE, which is not what any of pi's four
44
+ * `Usage` fields mean. It is read in exactly ONE place, `sealTurnUsage`, which
45
+ * relays it verbatim onto `usage.acp` (an accounting key, never a per-request
46
+ * shape) and never onto those four. A field rename upstream fails the typecheck
47
+ * here rather than silently zeroing the operator's cache-effect badge.
48
+ */
49
+ /** One `_meta.quota.token_count` row (claude-agent-acp 0.73.0
50
+ * `dist/acp-agent.js:5750-5765`). `cachedInputTokens` is cache READS — the name
51
+ * differs from `usage.cachedReadTokens` because the shape is shared with
52
+ * codex-acp; `cachedWriteTokens` is Claude's extra sibling. */
53
+ export type AcpQuotaTokenCount = {
54
+ totalTokens?: number | null;
55
+ inputTokens?: number | null;
56
+ cachedInputTokens?: number | null;
57
+ cachedWriteTokens?: number | null;
58
+ outputTokens?: number | null;
59
+ };
60
+
61
+ export type AcpPromptResponse = {
62
+ stopReason?: string;
63
+ usage?: AcpWireUsage | null;
64
+ /** Vendor-private, version-pinned. `_meta` is a standard ACP extension slot
65
+ * whose values a client may not assume, and `quota` is NOT in
66
+ * claude-agent-acp's exported types — it is produced by the private
67
+ * `turnQuotaMeta()` (read at 0.73.0 `dist/acp-agent.js:5738-5748`). Read
68
+ * defensively, never structurally required, and re-measure on a pin move. */
69
+ _meta?: {
70
+ quota?: {
71
+ token_count?: AcpQuotaTokenCount | null;
72
+ model_usage?: ReadonlyArray<{ model?: string; token_count?: AcpQuotaTokenCount | null }> | null;
73
+ } | null;
74
+ } | null;
75
+ };
76
+
23
77
  /** The subset of the ACP agent connection the backend drives (real or fake). */
24
78
  export interface AcpConnectionLike {
25
79
  initialize(params: unknown): Promise<unknown>;
26
80
  newSession(params: unknown): Promise<{ sessionId?: string }>;
27
- prompt(params: { sessionId: string; prompt: AcpTextBlock[] }): Promise<{ stopReason?: string }>;
81
+ prompt(params: { sessionId: string; prompt: AcpTextBlock[] }): Promise<AcpPromptResponse>;
28
82
  setSessionConfigOption?(params: unknown): Promise<unknown>;
29
83
  /**
30
84
  * ACP `session/cancel` — the PROTOCOL way to end an in-flight prompt turn.
@@ -84,8 +138,7 @@ export function connectAcpClient(stream: Stream, handlers: AcpClientHandlers): A
84
138
  initialize: (params) => agent.request(AGENT_METHODS.initialize, params as never),
85
139
  newSession: (params) =>
86
140
  agent.request(AGENT_METHODS.session_new, params as never) as Promise<{ sessionId?: string }>,
87
- prompt: (params) =>
88
- agent.request(AGENT_METHODS.session_prompt, params as never) as Promise<{ stopReason?: string }>,
141
+ prompt: (params) => agent.request(AGENT_METHODS.session_prompt, params as never) as Promise<AcpPromptResponse>,
89
142
  setSessionConfigOption: (params) => agent.request(AGENT_METHODS.session_set_config_option, params as never),
90
143
  cancel: (params) => {
91
144
  // Notification, not a request: nothing resolves it, and the connection
@@ -131,6 +131,30 @@ export interface AcpAdapterSettingsParams {
131
131
  projectPath: string;
132
132
  }
133
133
 
134
+ /**
135
+ * NO turn-usage evidence type lives here any more, and its absence is the #93
136
+ * finding, not an omission.
137
+ *
138
+ * ACP reports a turn's tokens ONLY as the SUM OVER THAT TURN'S API ROUND TRIPS
139
+ * (measured 2026-09-02: one 21-round-trip turn summed to cacheRead 4,185,084
140
+ * while the context it occupied was 223,516 — the overlay ledger's 21 rows add
141
+ * up to the four numbers `PromptResponse.usage` reported). pi's four `Usage`
142
+ * fields are not that quantity: `isContextOverflow` reads `input + cacheRead`
143
+ * as ONE REQUEST's prompt size (read at pi-ai `dist/utils/overflow.js:132-145`)
144
+ * and `cache-stats.detectMiss` reads input, cache-read, and cache-write as
145
+ * one request's prompt shape (read at pi-coding-agent
146
+ * `dist/core/cache-stats.js:14-37`). Projecting the turn
147
+ * aggregate into them fired a false overflow that compacted a live 223k session
148
+ * on a 1M window, invented two phantom cache misses, and SILENCED the one real
149
+ * 195,177-token miss the operator needed to see.
150
+ *
151
+ * During stream handling the vendor keeps a per-message snapshot in
152
+ * `lastAssistantUsage`, but its `usage_update` publishes only the scalar `used`
153
+ * value (read at claude-agent-acp 0.73.0 `dist/acp-agent.js:3273-3297`). Until
154
+ * that partition is carried on the wire, NOTHING honest can go in pi's four
155
+ * fields, so entwurf writes none of them.
156
+ */
157
+
134
158
  // ---------------------------------------------------------------------------
135
159
  // The adapter interface
136
160
  // ---------------------------------------------------------------------------
@@ -193,6 +217,24 @@ export interface AcpBackendAdapter {
193
217
  * sorted-stable primitive map (JSON.stringify determinism — no nested objects /
194
218
  * non-deterministic order). `backend` + `nativeModelId` are added by backend.ts. */
195
219
  configSignatureFields(adapterSettings: unknown): Record<string, unknown>;
220
+
221
+ /** Declares that THIS backend's `usage_update` cost semantics have been
222
+ * MEASURED, so backend.ts may seal its context occupancy and its per-turn
223
+ * cost from the session baseline.
224
+ *
225
+ * ABSENT means no measured semantics — permanent and static, not an omission
226
+ * to fill in by symmetry (cortex). backend.ts then seals NOTHING: no
227
+ * occupancy, no cost baseline, no diff. That is what keeps this lane's
228
+ * accounting out of a backend whose wire meaning nobody has measured.
229
+ *
230
+ * This is deliberately NOT a method returning token evidence. It was one
231
+ * until #93 measured that ACP's only token carrier is a turn aggregate that
232
+ * pi's four `Usage` fields cannot honestly hold (see the note above
233
+ * `AcpBackendAdapter`). With no token axis left to extract, presence alone
234
+ * carries everything the seal needs, and a flag that gates only the two axes
235
+ * entwurf actually reads from a NOTIFICATION cannot desync from a response
236
+ * shape it never inspects. */
237
+ sealsTurnAccounting?: true;
196
238
  }
197
239
 
198
240
  // ---------------------------------------------------------------------------
@@ -297,6 +339,14 @@ export const claudeAdapter: AcpBackendAdapter = {
297
339
  // `_adapterSettings` here.
298
340
  return {};
299
341
  },
342
+
343
+ // MEASURED (2026-09-02, #93): claude-agent-acp's `usage_update` carries a
344
+ // session-cumulative ESTIMATED cost (`cost.amount = message.total_cost_usd`)
345
+ // and a context-occupancy scalar (`used`; both read at 0.73.0
346
+ // `dist/acp-agent.js:2918-2924`). Both are measured, so backend.ts seals them.
347
+ // The token partition is NOT declared here: ACP's only token carrier is a
348
+ // per-turn round-trip aggregate, which is not what pi's four fields mean.
349
+ sealsTurnAccounting: true,
300
350
  };
301
351
 
302
352
  // ---------------------------------------------------------------------------
@@ -456,6 +506,15 @@ export const cortexAdapter: AcpBackendAdapter = {
456
506
  const settings = adapterSettings as CortexAdapterSettings | undefined;
457
507
  return { cortexConnection: settings?.cortexConnection ?? null };
458
508
  },
509
+
510
+ // NO sealsTurnAccounting — a DELIBERATE, permanent absence, not an omission to be
511
+ // filled in later by symmetry with claude. Nobody has measured what cortex's
512
+ // ACP `usage` (and its usage_update cost, if it sends one) MEAN: whether the
513
+ // token counts are a turn delta or a session total, and against which price
514
+ // table. Until that measurement exists, the honest report is no report:
515
+ // backend.ts seals nothing for a backend without this flag, so cortex's
516
+ // emitted usage is byte-identical to what it was before #93. Declaring this
517
+ // with a guess would mint exactly the silent misaccounting #93 exists to end.
459
518
  };
460
519
 
461
520
  const ADAPTERS: readonly AcpBackendAdapter[] = [claudeAdapter, cortexAdapter];