@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.
- package/AGENTS.md +4 -2
- package/CHANGELOG.md +333 -0
- package/VERIFY.md +8 -1
- package/docs/acp-backend-rail.md +25 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +15 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +14 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +5 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +18 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +23 -7
- package/package.json +4 -4
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +3 -1
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +13 -8
- package/pi-extensions/lib/acp/acp-client.ts +57 -4
- package/pi-extensions/lib/acp/backend-adapter.ts +59 -0
- package/pi-extensions/lib/acp/backend.ts +453 -11
- package/pi-extensions/lib/acp/event-mapper.ts +43 -6
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -6
- package/pi-extensions/lib/entwurf-v2-send.ts +26 -3
- package/pi-extensions/lib/entwurf-v2-surface.ts +18 -3
- package/pi-extensions/lib/meta-session.ts +32 -7
- package/run.sh +21 -0
- package/scripts/check-acp-stop-reason.ts +8 -2
- package/scripts/check-acp-usage-accounting.ts +1074 -0
- package/scripts/check-entwurf-v2-mailbox.ts +33 -0
- package/scripts/check-entwurf-v2-runner.ts +18 -0
- package/scripts/check-entwurf-v2-send.ts +13 -1
- package/scripts/check-entwurf-v2-surface.ts +75 -3
- package/scripts/check-gate-qualification.ts +1 -0
- package/scripts/check-hook-launch-topology.ts +70 -1
- package/scripts/check-mailbox-receipt-state.ts +6 -0
- package/scripts/check-meta-doctor-oracle.sh +73 -0
- package/scripts/check-meta-mailbox-state-write.ts +9 -2
- package/scripts/check-meta-manifest-schema.py +19 -1
- package/scripts/check-meta-session.ts +10 -2
- package/scripts/lib/pi-record-discovery.ts +47 -0
- package/scripts/meta-bridge-doctor.sh +18 -3
- package/scripts/meta-bridge-state.py +23 -5
- package/scripts/meta-bridge-statusline.sh +65 -2
- package/scripts/mutants/acp-stream-hooks.json +4 -2
- package/scripts/mutants/acp-usage-accounting.json +181 -0
- package/scripts/raw-async-delivery/README.md +280 -9
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +6 -2
- package/scripts/raw-async-delivery/lab-statusline.sh +63 -0
- package/scripts/raw-async-delivery/mailbox-watch.py +230 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +3 -1
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +13 -4
- package/scripts/raw-async-delivery/probe-delivery-transparency.sh +387 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
- package/scripts/smoke-acp-v2-send-live.ts +2 -2
- package/scripts/smoke-entwurf-v2-matrix-live.ts +60 -10
- package/scripts/smoke-meta-async-drift.sh +31 -8
- package/scripts/smoke-meta-install-state.sh +170 -11
- package/scripts/smoke-meta-keyset-guard.sh +4 -1
- 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
|
|
2081
|
-
*
|
|
2082
|
-
*
|
|
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
|
|
2105
|
-
// Stamp lastReadAt — the one
|
|
2106
|
-
// reached the reader). lastDeliveredAt is
|
|
2107
|
-
//
|
|
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.
|
|
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.
|
|
86
|
-
"@agentclientprotocol/sdk": "1.
|
|
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
|
|
10
|
-
#
|
|
11
|
-
#
|
|
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
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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 {
|
|
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<
|
|
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];
|