@junghanacs/entwurf 0.16.1 → 0.17.2

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.
Files changed (60) hide show
  1. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +333 -0
  3. package/VERIFY.md +8 -1
  4. package/docs/acp-backend-rail.md +25 -14
  5. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
  6. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +15 -0
  7. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +14 -2
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -2
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +18 -3
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +23 -7
  12. package/package.json +4 -4
  13. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +3 -1
  14. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +13 -8
  15. package/pi-extensions/lib/acp/acp-client.ts +57 -4
  16. package/pi-extensions/lib/acp/backend-adapter.ts +59 -0
  17. package/pi-extensions/lib/acp/backend.ts +453 -11
  18. package/pi-extensions/lib/acp/event-mapper.ts +43 -6
  19. package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
  20. package/pi-extensions/lib/entwurf-v2-runner.ts +23 -6
  21. package/pi-extensions/lib/entwurf-v2-send.ts +26 -3
  22. package/pi-extensions/lib/entwurf-v2-surface.ts +18 -3
  23. package/pi-extensions/lib/meta-session.ts +32 -7
  24. package/run.sh +21 -0
  25. package/scripts/check-acp-stop-reason.ts +8 -2
  26. package/scripts/check-acp-usage-accounting.ts +1074 -0
  27. package/scripts/check-entwurf-v2-mailbox.ts +33 -0
  28. package/scripts/check-entwurf-v2-runner.ts +18 -0
  29. package/scripts/check-entwurf-v2-send.ts +13 -1
  30. package/scripts/check-entwurf-v2-surface.ts +75 -3
  31. package/scripts/check-gate-qualification.ts +1 -0
  32. package/scripts/check-hook-launch-topology.ts +70 -1
  33. package/scripts/check-mailbox-receipt-state.ts +6 -0
  34. package/scripts/check-meta-doctor-oracle.sh +73 -0
  35. package/scripts/check-meta-mailbox-state-write.ts +9 -2
  36. package/scripts/check-meta-manifest-schema.py +19 -1
  37. package/scripts/check-meta-session.ts +10 -2
  38. package/scripts/lib/pi-record-discovery.ts +47 -0
  39. package/scripts/meta-bridge-doctor.sh +18 -3
  40. package/scripts/meta-bridge-state.py +23 -5
  41. package/scripts/meta-bridge-statusline.sh +65 -2
  42. package/scripts/mutants/acp-stream-hooks.json +4 -2
  43. package/scripts/mutants/acp-usage-accounting.json +181 -0
  44. package/scripts/raw-async-delivery/README.md +280 -9
  45. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +6 -2
  46. package/scripts/raw-async-delivery/lab-statusline.sh +63 -0
  47. package/scripts/raw-async-delivery/mailbox-watch.py +230 -0
  48. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +3 -1
  49. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +13 -4
  50. package/scripts/raw-async-delivery/probe-delivery-transparency.sh +387 -0
  51. package/scripts/smoke-acp-bundled-mcp-live.ts +2 -2
  52. package/scripts/smoke-acp-cortex-live.ts +2 -2
  53. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  54. package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
  55. package/scripts/smoke-acp-v2-send-live.ts +2 -2
  56. package/scripts/smoke-entwurf-v2-matrix-live.ts +60 -10
  57. package/scripts/smoke-meta-async-drift.sh +31 -8
  58. package/scripts/smoke-meta-install-state.sh +170 -11
  59. package/scripts/smoke-meta-keyset-guard.sh +4 -1
  60. package/scripts/smoke-mux-lifecycle-live.ts +46 -2
@@ -2077,9 +2077,17 @@ export function enqueueMetaMessage(opts) {
2077
2077
  * Drain a garden citizen's mailbox: read every unread message (a fresh `.msg`
2078
2078
  * read before its doorbell, or a doorbell-rung `.msg.delivered`), archive each to
2079
2079
  * `*.read` so a re-read never double-returns, and — only if at least one message
2080
- * was read — stamp `lastReadAt` (NOT `lastDeliveredAt`: the doorbell owns
2081
- * delivery-time, see the stamp-site note below). An empty inbox mutates nothing:
2082
- * reading nothing is not a receipt.
2080
+ * was read — stamp `lastReadAt`. `lastDeliveredAt` stays untouched (see the
2081
+ * stamp-site note below). An empty inbox mutates nothing: reading nothing is not
2082
+ * a receipt.
2083
+ *
2084
+ * WHERE THE PER-MESSAGE TRUTH LIVES (#98 5a, corrected 2026-09-03). The FILE SUFFIX
2085
+ * is the per-message receipt: `.msg` = enqueued, `.msg.delivered` = the doorbell
2086
+ * rang for it, `.msg.delivered.read` = this function handed it to the reader.
2087
+ * `state.json` is NOT that — its three slots are GARDEN-WIDE and overwritten, so
2088
+ * `lastReadAt` says "this citizen last read something at T", never "message X was
2089
+ * read". A sender that quotes `lastReadAt` back as the fate of the letter it just
2090
+ * queued is quoting the PREVIOUS letter's read. That misreading is what opened #98.
2083
2091
  */
2084
2092
  export function readMetaInbox(opts) {
2085
2093
  const now = opts.now ?? new Date();
@@ -2101,10 +2109,18 @@ export function readMetaInbox(opts) {
2101
2109
  if (messages.length === 0) {
2102
2110
  return { gardenId: citizen.gardenId, messages, readAt: null, recordPath: recordFile };
2103
2111
  }
2104
- // 3D-4 the cut: the read receipt lives SOLELY in the mailbox state store now.
2105
- // Stamp lastReadAt — the one receipt this layer stamps honestly (it KNOWS the body
2106
- // reached the reader). lastDeliveredAt is the doorbell's to own; stamping it here
2107
- // would report read-time as delivery-time, so it is left as the doorbell left it.
2112
+ // 3D-4 the cut: the garden-wide read receipt lives SOLELY in the mailbox state store
2113
+ // now. Stamp lastReadAt — the one slot this layer stamps honestly (it KNOWS a body
2114
+ // reached the reader at this instant). lastDeliveredAt is left alone, and #98 5a
2115
+ // corrects WHY: the old comment said "the doorbell owns it", but the shipped
2116
+ // doorbell.sh (54 lines) writes NOTHING to state.json — measured, zero state writes.
2117
+ // So `lastDeliveredAt` is a RESERVED SLOT nobody stamps, permanently null on all
2118
+ // ~180 on-disk states, while 900+ files carry a `.delivered` suffix. Stamping it
2119
+ // here would still be wrong (it would report read-time as delivery-time), so it
2120
+ // stays null — but do not read "the doorbell will fill it in" into that. The
2121
+ // per-message delivery fact is the `.delivered` SUFFIX. The field is left in place
2122
+ // deliberately: removing it is a migration (the parser rejects unknown keys AND a
2123
+ // bumped schemaVersion, and every stamp re-parses first), tracked as #98 5b.
2108
2124
  // The state stamp returns the updated state, whose lastReadAt IS the D7 read-receipt.
2109
2125
  // Inside the messages.length>0 branch by construction — an empty inbox already
2110
2126
  // early-returned (no .read archive, state untouched), so "read nothing" is no
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.16.1",
3
+ "version": "0.17.2",
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": {
@@ -45,7 +45,9 @@
45
45
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/hook-launch.sh",
46
46
  "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/doorbell.sh"],
47
47
  "asyncRewake": true,
48
- "timeout": 20
48
+ "timeout": 20,
49
+ "rewakeSummary": "entwurf inbox: sibling mail arrived",
50
+ "rewakeMessage": "entwurf mailbox notice:"
49
51
  }
50
52
  ]
51
53
  }
@@ -6,15 +6,20 @@
6
6
  # with a doorbell notice. Free: a file write + continuation of an already-running
7
7
  # subscription session (no `claude -p` spawn).
8
8
  #
9
- # ADDRESSED by GARDEN ID: the changed path arrives on stdin as `file_path`; its
10
- # directory IS this session's garden mailbox (<meta-mailbox>/<garden-id>/). So
11
- # this hook touches ONLY its own mailbox — a sender that pokes one garden id's
12
- # signal wakes only that session. No node needed here; the dirname is the mailbox.
9
+ # ADDRESSED by GARDEN ID: the changed path arrives on stdin as `file_path` and this
10
+ # hook takes its DIRECTORY as the mailbox to process. No node needed here; the
11
+ # dirname is the mailbox.
13
12
  #
14
- # DOORBELL ONLY: announce "you have mail" + the body path on stderr (the sole
15
- # asyncRewake payload channel stdout is dropped). NEVER push imperatives; strong
16
- # models flag hook-injected commands as prompt injection. The agent self-fetches
17
- # the body with its own trusted tool, and that inbox-read is the real D7 receipt.
13
+ # Two statements, deliberately kept apart (#98, 2026-09-03 they used to be one
14
+ # sentence claiming this hook "touches ONLY its own mailbox"):
15
+ # - PREMISE: under a normal install the only watch this session armed is its own
16
+ # <garden-id>/inbox.signal, so that is the only file_path that arrives.
17
+ # - LIMIT: this script does not verify it. It trusts `file_path` and never compares
18
+ # the dirname against its garden id. Measured — a second FileChanged hook in the
19
+ # same session made this doorbell process the OTHER mailbox and race it to exit 2,
20
+ # which is why a second such hook cannot coexist with this one.
21
+ # Receipt: scripts/raw-async-delivery/README.md, "What the probe session actually
22
+ # touches". Not a live defect: nothing pokes a signal outside the garden mailbox.
18
23
  #
19
24
  # RUNTIME DEPS: bash + python3 (the FileChanged stdin JSON is parsed with python3
20
25
  # below — robust against escaping, unlike sed/grep). The meta-bridge doctor must
@@ -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];