@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
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
# garden id is looked up by scanning meta-record BODIES via the native Claude
|
|
8
8
|
# `session_id`. No cache, no filename authority, no DB.
|
|
9
9
|
#
|
|
10
|
+
# It also draws the #98 B unread badge: `✉N` when the garden's mailbox holds N
|
|
11
|
+
# messages the read tool would still return, nothing when it holds none, and `✉?` when
|
|
12
|
+
# the count could NOT be taken. That third state is the point — a statusline that silently
|
|
13
|
+
# renders 0 on a broken read would say "no mail" about a mailbox it never looked in.
|
|
14
|
+
#
|
|
10
15
|
# Runtime dependency: python3 (already gated by install-meta-bridge/doctor).
|
|
11
16
|
set -euo pipefail
|
|
12
17
|
|
|
@@ -18,7 +23,9 @@ input=$(cat)
|
|
|
18
23
|
# wrote. doctor/smoke are the fail-LOUD surfaces; this surface stays silent.
|
|
19
24
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
20
25
|
device="$(cat "$HOME/.current-device" 2>/dev/null || echo UNKNOWN)"
|
|
21
|
-
|
|
26
|
+
# ✉? not a missing badge: with no python3 the unread count was not TAKEN. Drawing
|
|
27
|
+
# nothing here would be indistinguishable from "no mail" (#98 B).
|
|
28
|
+
printf '%s ?\n🪛 ? cc ✉?' "$device"
|
|
22
29
|
exit 0
|
|
23
30
|
fi
|
|
24
31
|
STATUSLINE_INPUT="$input" python3 - <<'PY' || true
|
|
@@ -166,6 +173,60 @@ def garden_lookup(native_session_id: str) -> str:
|
|
|
166
173
|
return "?"
|
|
167
174
|
|
|
168
175
|
|
|
176
|
+
def meta_mailbox_dir() -> Path:
|
|
177
|
+
"""Mirror of `defaultMetaMailboxDir()` in meta-session.ts, including the env
|
|
178
|
+
precedence. Kept as a mirror rather than a shell-out: the statusline runs on every
|
|
179
|
+
render (measured 4-10 times per doorbell rewake turn, #98 Phase 1 P2a), so it must not
|
|
180
|
+
spawn a node process to count files."""
|
|
181
|
+
override = os.environ.get("ENTWURF_META_MAILBOX_DIR")
|
|
182
|
+
if override:
|
|
183
|
+
return Path(override).expanduser().resolve()
|
|
184
|
+
agent = os.environ.get("PI_CODING_AGENT_DIR")
|
|
185
|
+
if agent:
|
|
186
|
+
return Path(agent).expanduser().resolve() / "meta-mailbox"
|
|
187
|
+
return Path.home() / ".pi" / "agent" / "meta-mailbox"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def unread_badge(garden: str) -> str:
|
|
191
|
+
"""`✉N` / `""` / `✉?` — the #98 B badge.
|
|
192
|
+
|
|
193
|
+
The counted set is EXACTLY what `entwurf_inbox_read` would return: `*.msg` (arrived,
|
|
194
|
+
doorbell has not rung yet) PLUS `*.msg.delivered` (doorbell rang, model has not read).
|
|
195
|
+
`readMetaInbox` uses that same union (meta-session.ts), and `.read` is excluded by
|
|
196
|
+
both — so the badge clears on the read, on the statusline's next execution.
|
|
197
|
+
|
|
198
|
+
Counting only `.msg.delivered` would under-report a letter that landed between two
|
|
199
|
+
doorbells; counting `.read` too would never clear.
|
|
200
|
+
|
|
201
|
+
Three distinct returns, deliberately:
|
|
202
|
+
- `""` the mailbox was read and is empty. Silence is the quiet common case.
|
|
203
|
+
- `✉N` N letters the read tool will hand back.
|
|
204
|
+
- `✉?` the count could not be taken (unusable garden id, unreadable dir).
|
|
205
|
+
NEVER collapsed into `""` — a false zero is the failure this whole issue
|
|
206
|
+
is about.
|
|
207
|
+
|
|
208
|
+
A garden id that is `?`/`!`/`ready` means the lookup itself did not land, so there is
|
|
209
|
+
no mailbox to count and no claim to make: `✉?`, not silence.
|
|
210
|
+
"""
|
|
211
|
+
if not garden or garden in ("?", "!"):
|
|
212
|
+
return "✉?"
|
|
213
|
+
if garden == "ready":
|
|
214
|
+
# No session_id yet (a fresh render before the hook minted a record). Nothing has
|
|
215
|
+
# been addressed to this session, so an empty badge is TRUE, not a guess.
|
|
216
|
+
return ""
|
|
217
|
+
if "/" in garden or garden.startswith("."):
|
|
218
|
+
return "✉?"
|
|
219
|
+
try:
|
|
220
|
+
box = meta_mailbox_dir() / garden
|
|
221
|
+
if not box.is_dir():
|
|
222
|
+
# A citizen with no mailbox dir has received nothing. Honest empty.
|
|
223
|
+
return ""
|
|
224
|
+
n = sum(1 for f in box.iterdir() if f.name.endswith(".msg") or f.name.endswith(".msg.delivered"))
|
|
225
|
+
except Exception:
|
|
226
|
+
return "✉?"
|
|
227
|
+
return f"✉{n}" if n > 0 else ""
|
|
228
|
+
|
|
229
|
+
|
|
169
230
|
def main() -> None:
|
|
170
231
|
data = load_input()
|
|
171
232
|
raw_cwd = dig(data, "workspace", "current_dir") or data.get("cwd") or "?"
|
|
@@ -183,7 +244,9 @@ def main() -> None:
|
|
|
183
244
|
f"{CYAN_BOLD}{cwd_tail}{RESET}"
|
|
184
245
|
f"{DIM}{git_branch(str(raw_cwd))}{RESET}"
|
|
185
246
|
)
|
|
186
|
-
|
|
247
|
+
badge = unread_badge(garden)
|
|
248
|
+
badge_text = f" {badge}" if badge else ""
|
|
249
|
+
line2 = f"{DIM}🪛 {garden} cc{badge_text} | {model}{vterm}{context_info(data)}{RESET}"
|
|
187
250
|
sys.stdout.write(f"{line1}\n{line2}")
|
|
188
251
|
|
|
189
252
|
|
|
@@ -144,9 +144,11 @@
|
|
|
144
144
|
"claim": "ACPHOOK-ONRESPONSE-NEVER-CALLED",
|
|
145
145
|
"title": "someone 'completes' the hook contract by fabricating an HTTP 200 response on turn success — exactly the false evidence the exemption exists to forbid",
|
|
146
146
|
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
147
|
-
"find": [
|
|
147
|
+
"find": [
|
|
148
|
+
"\tfunction finishSuccess(adapter: AcpBackendAdapter, session: BridgeSession, promptResult: AcpPromptResponse): void {"
|
|
149
|
+
],
|
|
148
150
|
"replace": [
|
|
149
|
-
"\tfunction finishSuccess(
|
|
151
|
+
"\tfunction finishSuccess(adapter: AcpBackendAdapter, session: BridgeSession, promptResult: AcpPromptResponse): void {",
|
|
150
152
|
"\t\tvoid options?.onResponse?.({ status: 200, headers: {} }, model);"
|
|
151
153
|
],
|
|
152
154
|
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "acp-usage-accounting",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "ACP-TURN-AGGREGATE-NOT-PROJECTED",
|
|
7
|
+
"title": "the turn's ROUND-TRIP AGGREGATE is projected back onto pi's four per-request usage fields — re-plants the exact 2026-09-02 defect: pi's own isContextOverflow then read 42 + 4,185,084 against a 1,000,000 window and compacted a live session whose context was 223,516",
|
|
8
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
9
|
+
"find": ["\t\tsealTurnUsage(adapter, session, promptResult);"],
|
|
10
|
+
"replace": [
|
|
11
|
+
"\t\tsealTurnUsage(adapter, session, promptResult);",
|
|
12
|
+
"\t\tconst reprojected = promptResult?.usage;",
|
|
13
|
+
"\t\tif (adapter.sealsTurnAccounting && reprojected) {",
|
|
14
|
+
"\t\t\tstate.output.usage.input = reprojected.inputTokens ?? 0;",
|
|
15
|
+
"\t\t\tstate.output.usage.output = reprojected.outputTokens ?? 0;",
|
|
16
|
+
"\t\t\tstate.output.usage.cacheRead = reprojected.cachedReadTokens ?? 0;",
|
|
17
|
+
"\t\t\tstate.output.usage.cacheWrite = reprojected.cachedWriteTokens ?? 0;",
|
|
18
|
+
"\t\t}"
|
|
19
|
+
],
|
|
20
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
21
|
+
"timeoutSeconds": 240,
|
|
22
|
+
"signature": "[QK:ACP-TURN-AGGREGATE-NOT-PROJECTED]",
|
|
23
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"claim": "ACP-CONTEXT-OCCUPANCY-PRESERVED",
|
|
27
|
+
"title": "the seal stops writing the session's context occupancy into totalTokens — pi's calculateContextTokens then falls through to summing the four (all zero) usage fields, so the status-line percentage and auto-compaction read a long session as empty",
|
|
28
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
29
|
+
"find": [
|
|
30
|
+
"\t\tif (typeof session.contextOccupancyTokens === \"number\") {",
|
|
31
|
+
"\t\t\tstate.output.usage.totalTokens = session.contextOccupancyTokens;",
|
|
32
|
+
"\t\t}"
|
|
33
|
+
],
|
|
34
|
+
"replace": [
|
|
35
|
+
"\t\tif (typeof session.contextOccupancyTokens === \"number\") {",
|
|
36
|
+
"\t\t\tstate.output.usage.totalTokens = 0;",
|
|
37
|
+
"\t\t}"
|
|
38
|
+
],
|
|
39
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
40
|
+
"timeoutSeconds": 240,
|
|
41
|
+
"signature": "[QK:ACP-CONTEXT-OCCUPANCY-PRESERVED]",
|
|
42
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"claim": "ACP-CONTEXT-OCCUPANCY-CARRIED",
|
|
46
|
+
"title": "the seal stops carrying the session's last known context occupancy — a turn whose usage_update never arrived now emits a token partition with a zero totalTokens, so pi falls through to that partition and reads a long session as nearly empty",
|
|
47
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
48
|
+
"find": [
|
|
49
|
+
"\t\tif (typeof occupancy === \"number\") session.contextOccupancyTokens = occupancy;",
|
|
50
|
+
"\t\tif (typeof session.contextOccupancyTokens === \"number\") {",
|
|
51
|
+
"\t\t\tstate.output.usage.totalTokens = session.contextOccupancyTokens;",
|
|
52
|
+
"\t\t}"
|
|
53
|
+
],
|
|
54
|
+
"replace": ["\t\tif (typeof occupancy === \"number\") session.contextOccupancyTokens = occupancy;"],
|
|
55
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
56
|
+
"timeoutSeconds": 240,
|
|
57
|
+
"signature": "[QK:ACP-CONTEXT-OCCUPANCY-CARRIED]",
|
|
58
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"claim": "ACP-TURN-COST-SUM-MATCHES-SDK",
|
|
62
|
+
"title": "the backend's RUNNING SESSION TOTAL is assigned to the turn cost again instead of the adjacent diff — the arithmetic that displayed a $24.261 session as $444.370",
|
|
63
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
64
|
+
"find": [
|
|
65
|
+
"\t\t\t\tsession.sdkCumulativeCostUsd = observed;",
|
|
66
|
+
"\t\t\t\tstate.output.usage.cost.total = diff;",
|
|
67
|
+
"\t\t\t\tturnCostUsd = diff;"
|
|
68
|
+
],
|
|
69
|
+
"replace": [
|
|
70
|
+
"\t\t\t\tsession.sdkCumulativeCostUsd = observed;",
|
|
71
|
+
"\t\t\t\tstate.output.usage.cost.total = observed;",
|
|
72
|
+
"\t\t\t\tturnCostUsd = diff;"
|
|
73
|
+
],
|
|
74
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
75
|
+
"timeoutSeconds": 240,
|
|
76
|
+
"signature": "[QK:ACP-TURN-COST-SUM-MATCHES-SDK]",
|
|
77
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"claim": "ACP-COST-BASELINE-HELD-WHEN-MISSING",
|
|
81
|
+
"title": "a turn with no cost notification zeroes the baseline instead of holding it — the next diff then re-attributes every dollar the session had already spent",
|
|
82
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
83
|
+
"find": [
|
|
84
|
+
"\t\t\tstate.output.usage.cost.total = 0;",
|
|
85
|
+
"\t\t} else {",
|
|
86
|
+
"\t\t\tconst diff = observed - (session.sdkCumulativeCostUsd ?? 0);"
|
|
87
|
+
],
|
|
88
|
+
"replace": [
|
|
89
|
+
"\t\t\tsession.sdkCumulativeCostUsd = 0;",
|
|
90
|
+
"\t\t\tstate.output.usage.cost.total = 0;",
|
|
91
|
+
"\t\t} else {",
|
|
92
|
+
"\t\t\tconst diff = observed - (session.sdkCumulativeCostUsd ?? 0);"
|
|
93
|
+
],
|
|
94
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
95
|
+
"timeoutSeconds": 240,
|
|
96
|
+
"signature": "[QK:ACP-COST-BASELINE-HELD-WHEN-MISSING]",
|
|
97
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"claim": "ACP-COST-RESET-NOT-SILENT",
|
|
101
|
+
"title": "a running total that goes backwards is absorbed silently — the turn reports a negative cost, nobody is told, and the only observation that could settle what a conversation reset does to the total is destroyed",
|
|
102
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
103
|
+
"find": ["\t\tif (diff < 0) {"],
|
|
104
|
+
"replace": ["\t\tif (diff < -1_000_000) {"],
|
|
105
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
106
|
+
"timeoutSeconds": 240,
|
|
107
|
+
"signature": "[QK:ACP-COST-RESET-NOT-SILENT]",
|
|
108
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"claim": "ACP-CORTEX-USAGE-UNTOUCHED",
|
|
112
|
+
"title": "the seal stops being gated on the adapter DECLARING measured semantics — claude's accounting is sealed onto a backend nobody measured, minting the same unmeasured accounting this lane exists to end",
|
|
113
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
114
|
+
"find": ["\t\tif (!adapter.sealsTurnAccounting) return;"],
|
|
115
|
+
"replace": ["\t\tvoid adapter.sealsTurnAccounting;"],
|
|
116
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
117
|
+
"timeoutSeconds": 240,
|
|
118
|
+
"signature": "[QK:ACP-CORTEX-USAGE-UNTOUCHED]",
|
|
119
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"claim": "ACP-TURN-ACCOUNTING-ATTACHED",
|
|
123
|
+
"title": "the vendor's four turn totals stop riding their own key — pi's four fields are 0 and nothing else carries the numbers, so the cache-effect badge has no inputs and a paid-for prefix rewrite reaches the operator as silence",
|
|
124
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
125
|
+
"find": ["\t\t\t(state.output.usage as unknown as { acp?: AcpTurnAccounting }).acp = aggregate;"],
|
|
126
|
+
"replace": ["\t\t\tvoid aggregate;"],
|
|
127
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
128
|
+
"timeoutSeconds": 240,
|
|
129
|
+
"signature": "[QK:ACP-TURN-ACCOUNTING-ATTACHED]",
|
|
130
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"claim": "ACP-CACHE-REBILL-REPORTED",
|
|
134
|
+
"title": "the re-billed-prefix bound drops its ΣcacheWrite term, so a full prefix rewrite after an idle gap no longer clears the notice floor — the operator runs on a cache-effect badge while having already paid to rewrite the whole prefix",
|
|
135
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
136
|
+
"find": [
|
|
137
|
+
"\t\t\t? (priorOccupancy as number) - (priorIoSum as number) - Math.max(0, (occupancy as number) - cacheWriteForBound)"
|
|
138
|
+
],
|
|
139
|
+
"replace": ["\t\t\t? (priorOccupancy as number) - (priorIoSum as number) - Math.max(0, occupancy as number)"],
|
|
140
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
141
|
+
"timeoutSeconds": 240,
|
|
142
|
+
"signature": "[QK:ACP-CACHE-REBILL-REPORTED]",
|
|
143
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"claim": "ACP-REBILL-NEVER-EXCEEDS-WRITE",
|
|
147
|
+
"title": "the re-billed claim loses its physical cap, so a context shrink (organic compaction) announces a six-figure cache miss over a turn that wrote a thousand tokens — a fact the operator has no way to disbelieve",
|
|
148
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
149
|
+
"find": [
|
|
150
|
+
"\t\tconst missLowerBound = rawBound === undefined ? undefined : Math.min(rawBound, cacheWriteForBound);"
|
|
151
|
+
],
|
|
152
|
+
"replace": ["\t\tconst missLowerBound = rawBound;"],
|
|
153
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
154
|
+
"timeoutSeconds": 240,
|
|
155
|
+
"signature": "[QK:ACP-REBILL-NEVER-EXCEEDS-WRITE]",
|
|
156
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"claim": "ACP-ACCOUNTING-PREFERS-WIDEST",
|
|
160
|
+
"title": "the accounting-grade model_usage rows stop being preferred, so the turn silently reports MAIN-LOOP-ONLY tokens against an all-inclusive cost denominator — the cache-effect badge understates itself exactly when compaction or a subagent ran, and nothing says so",
|
|
161
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
162
|
+
"find": ["\tif (Array.isArray(rows) && rows.length > 0) {"],
|
|
163
|
+
"replace": ["\tif (Array.isArray(rows) && rows.length > 99) {"],
|
|
164
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
165
|
+
"timeoutSeconds": 240,
|
|
166
|
+
"signature": "[QK:ACP-ACCOUNTING-PREFERS-WIDEST]",
|
|
167
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"claim": "ACP-REBILL-MAIN-LOOP-SCOPE",
|
|
171
|
+
"title": "the re-billed bound takes cacheWrite from the WIDE accounting rows instead of the main loop, so a warm main prefix announces a six-figure miss (and this turn's dollar figure) because an internal/compaction call wrote extra cache — mixing occupancy's scope with model_usage's",
|
|
172
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
173
|
+
"find": ["\t\tconst cacheWriteForBound = mainLoop !== undefined ? mainLoop.cacheWrite : 0;"],
|
|
174
|
+
"replace": ["\t\tconst cacheWriteForBound = aggregate !== undefined ? aggregate.cacheWrite : 0;"],
|
|
175
|
+
"gate": ["bash", "run.sh", "check-acp-usage-accounting"],
|
|
176
|
+
"timeoutSeconds": 240,
|
|
177
|
+
"signature": "[QK:ACP-REBILL-MAIN-LOOP-SCOPE]",
|
|
178
|
+
"signatureSource": "scripts/check-acp-usage-accounting.ts"
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
@@ -112,6 +112,28 @@ do not ship that. Proven: deliver to A's sessionId → A wakes (FileChanged), B'
|
|
|
112
112
|
- `live` — one session idle → addressed external write wakes it, zero typing.
|
|
113
113
|
- `repro-addressed-routing.sh` — **two sessions** A,B → deliver to A only → assert A wakes, B undisturbed.
|
|
114
114
|
|
|
115
|
+
### Delivery-transparency probes (issue #98 Phase 1)
|
|
116
|
+
None of these is a product surface, and none edits a product **file** —
|
|
117
|
+
`pi/meta-bridge/**`, `pi-extensions/**`, `mcp/**`, `scripts/meta-bridge-*.sh`.
|
|
118
|
+
They are **not** isolated from operator **state**, though: see
|
|
119
|
+
[What the probe session actually touches](#what-the-probe-session-actually-touches).
|
|
120
|
+
Receipts in [Inherited facts corrected](#inherited-facts-corrected).
|
|
121
|
+
- `probe-delivery-transparency.sh [keep]` — P1 + P2a + P2b in one live session:
|
|
122
|
+
does a configured `rewakeSummary`/`rewakeMessage` reach the operator's row and
|
|
123
|
+
the model's prefix, and does the statusline re-execute both when mail lands and
|
|
124
|
+
after a mid-turn drain. Builds its own `/tmp` mailbox, cwd and settings.
|
|
125
|
+
- `lab-statusline.sh` — the P2a/P2b instrument. Logs one line per invocation with
|
|
126
|
+
the unread count it observed, so the question is answerable without shipping a
|
|
127
|
+
badge first. **Never install this as the product statusline**
|
|
128
|
+
(`scripts/meta-bridge-statusline.sh`).
|
|
129
|
+
- `mailbox-watch.py [root]` — P4 prototype of the out-of-harness observation
|
|
130
|
+
window: one line per message transition (`ARRIVED`/`RUNG`/`READ`), read from the
|
|
131
|
+
mailbox files alone. Uses `inotify(7)` via ctypes because `inotifywait` is not on
|
|
132
|
+
`PATH` here. **Mailbox-rail only** — self-fetch backends (claude-code / copilot /
|
|
133
|
+
omp). pi goes over its control socket and antigravity over native-push, so
|
|
134
|
+
neither writes here; silence is not "no sibling traffic". See the script's SCOPE
|
|
135
|
+
block.
|
|
136
|
+
|
|
115
137
|
## Quick start (plugin reception)
|
|
116
138
|
|
|
117
139
|
```bash
|
|
@@ -131,9 +153,11 @@ CC_MAILBOX_ROOT=/tmp/cc-mbx ./cc-enqueue-addressed.sh <session_id> "your async m
|
|
|
131
153
|
|
|
132
154
|
## Design notes / invariants
|
|
133
155
|
|
|
134
|
-
- **Doorbell only.**
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
- **Doorbell only.** Write the payload to **stderr** — not because stdout is
|
|
157
|
+
ignored (it is not; see the correction to gotcha #3), but because stderr is the
|
|
158
|
+
channel that is used unconditionally and is never parsed as JSON. Announce "you
|
|
159
|
+
have mail" + the body path; never push imperatives — strong models flag
|
|
160
|
+
hook-injected commands as prompt injection. The agent self-fetches.
|
|
137
161
|
- **Body path in the doorbell.** The hook `mv`s `*.msg` → `*.msg.delivered`
|
|
138
162
|
before announcing and reports the `.delivered` path, so the agent reads it in
|
|
139
163
|
one step (measured: removes a filesystem-hunt round-trip).
|
|
@@ -161,12 +185,23 @@ that a second pass reversed.
|
|
|
161
185
|
at skill *invocation* (mid-session, after `SessionStart`); plugin
|
|
162
186
|
`hooks/hooks.json` loads at *startup*. (Wrong conclusion #2 was "scoped is
|
|
163
187
|
impossible → global settings only". See top of this file.)
|
|
164
|
-
3.
|
|
165
|
-
dropped and the model sees "No stderr output". The body must go to stderr
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
188
|
+
3. ~~**`asyncRewake` payload channel is `stderr` ONLY.** Anything on `stdout` is
|
|
189
|
+
dropped and the model sees "No stderr output". The body must go to stderr.~~
|
|
190
|
+
**FALSE — retired 2026-09-03, receipt in [Inherited facts corrected](#inherited-facts-corrected).**
|
|
191
|
+
The model-facing body is `${prefix} ${stderr || stdout}`: stdout is used
|
|
192
|
+
whenever stderr is empty, and is additionally scanned line-by-line for a JSON
|
|
193
|
+
hook-output object. Keep writing to stderr — that is still the right choice,
|
|
194
|
+
because a doorbell that also emits JSON on stdout invites the parser — but do
|
|
195
|
+
not repeat the reason. It is not "stdout is dropped".
|
|
196
|
+
4. ~~**`asyncRewake` force-prepends `Stop hook feedback:\n[<script>]:`** and
|
|
197
|
+
**ignores any configured `rewakeMessage`**.~~
|
|
198
|
+
**FALSE — retired 2026-09-03, receipt in [Inherited facts corrected](#inherited-facts-corrected).**
|
|
199
|
+
Both strings are configurable from `hooks.json`, ungated for a local plugin:
|
|
200
|
+
`rewakeSummary` replaces the operator-visible row (default `Stop hook
|
|
201
|
+
feedback`), `rewakeMessage` replaces the model-visible prefix (default `Stop
|
|
202
|
+
hook blocking error from command "…":`). Only the *stdout-JSON* form of
|
|
203
|
+
`rewakeSummary` is first-party gated. Doorbell framing is still right — never
|
|
204
|
+
push imperatives (lesson #7) — but that is a *policy* choice, not a limit.
|
|
170
205
|
5. **Infinite-loop guard is mandatory.** Honor `stop_hook_active` (if `true`,
|
|
171
206
|
`exit 0` — already continuing, let it stop) or you get a wake loop. The engine
|
|
172
207
|
also caps re-wakes via `CLAUDE_CODE_STOP_HOOK_BLOCK_CAP`.
|
|
@@ -186,6 +221,242 @@ that a second pass reversed.
|
|
|
186
221
|
submit a `tmux send-keys` prompt (the first keystroke only fills the input
|
|
187
222
|
box). The repro drivers send `Enter` twice on purpose — not a bug.
|
|
188
223
|
|
|
224
|
+
## What the probe session actually touches
|
|
225
|
+
|
|
226
|
+
A correction to a claim this lab made about *itself* — read it before the
|
|
227
|
+
receipts, because for two runs it made them **wrong**.
|
|
228
|
+
|
|
229
|
+
`probe-delivery-transparency.sh` used to launch a plain `claude --plugin-dir …`,
|
|
230
|
+
which inherits the operator's `~/.claude/settings.json`. That is where the
|
|
231
|
+
**product** meta-bridge plugin is enabled (`extraKnownMarketplaces` +
|
|
232
|
+
`enabledPlugins`), so it loaded next to the lab plugin. Two consequences, both
|
|
233
|
+
measured:
|
|
234
|
+
|
|
235
|
+
**1. The probe minted real garden citizens.** The product `SessionStart` hook
|
|
236
|
+
created a record in `~/.pi/agent/meta-sessions/` and a mailbox in
|
|
237
|
+
`~/.pi/agent/meta-mailbox/`. Three runs left three behind
|
|
238
|
+
(`20260903T161532-113180`, `…161709-137c49`, `…161922-45df8a`, each with
|
|
239
|
+
`cwd=/tmp/cc-p98-probe/cwd`). They have been returned.
|
|
240
|
+
|
|
241
|
+
**2. The two doorbells cross-fired, and P1 was non-deterministic.** An earlier
|
|
242
|
+
version of this section asserted they could not — that each fires only on the
|
|
243
|
+
signal path its own `SessionStart` armed. **That was false, and asserting it
|
|
244
|
+
without measuring is how it survived.** `doorbell.sh` takes
|
|
245
|
+
`dirname(file_path)` as "its" mailbox *unconditionally*, so the product doorbell
|
|
246
|
+
processed the **lab** mailbox and raced the lab hook to `exit 2`. When the
|
|
247
|
+
product hook won, the operator saw the default `Stop hook feedback` — from a hook
|
|
248
|
+
that carries no `rewakeSummary` — while the `hooks.json` under test was perfectly
|
|
249
|
+
correct. Two consecutive runs failed P1 that way.
|
|
250
|
+
|
|
251
|
+
The transcript is unambiguous about who fired: product wording, lab path.
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
<summary>Stop hook feedback</summary>
|
|
255
|
+
Stop hook blocking error from command "FileChanged": [entwurf inbox] 1 unread
|
|
256
|
+
mailbox message available for garden 598c57c0-… Read them by calling the
|
|
257
|
+
entwurf_inbox_read tool … bodies are at
|
|
258
|
+
/tmp/cc-p98-probe/mailbox/598c57c0-…/*.msg.delivered
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`[entwurf inbox]` / `entwurf_inbox_read` / `lastReadAt` is the **product**
|
|
262
|
+
doorbell's text; `/tmp/cc-p98-probe/mailbox/` is the **lab** mailbox. The first
|
|
263
|
+
three (green) runs were the lab hook winning the same race, not isolation.
|
|
264
|
+
|
|
265
|
+
**Fix: `--setting-sources project,local`.** Dropping user settings removes the
|
|
266
|
+
product plugin, so there is no citizen and no race, while the project settings
|
|
267
|
+
under the throwaway cwd still load and the lab statusline still applies. Two
|
|
268
|
+
consecutive runs after the change: **10 pass / 0 fail, `no probe-minted citizens
|
|
269
|
+
to clean`.**
|
|
270
|
+
|
|
271
|
+
`cleanup_citizens` stays as a sweep for anything an older-flag run left behind.
|
|
272
|
+
It deletes only records whose recorded `cwd` matches the probe's own `/tmp` path,
|
|
273
|
+
and a mailbox holding anything but `inbox.signal` is **reported and left alone** —
|
|
274
|
+
a probe must not destroy evidence.
|
|
275
|
+
|
|
276
|
+
Claude still writes `~/.claude/projects/<cwd>/<sid>.jsonl` and
|
|
277
|
+
`~/.claude/sessions/<pid>.json` on its own; receipt (ii) below **is** one of those
|
|
278
|
+
files. That is Claude's bookkeeping and is left alone.
|
|
279
|
+
|
|
280
|
+
> The older drivers (`repro-plugin-idle-wake.sh`, `repro-addressed-routing.sh`)
|
|
281
|
+
> launch without `--setting-sources` and carry both problems. Pre-existing, not
|
|
282
|
+
> introduced by Phase 1 — named here so the next person does not rediscover it as
|
|
283
|
+
> a mystery flake.
|
|
284
|
+
|
|
285
|
+
> **Carried out of the lab:** the product `doorbell.sh` trusting
|
|
286
|
+
> `dirname(file_path)` for any watched path it is handed is a real property of the
|
|
287
|
+
> shipped hook, not a lab artifact. Nothing today pokes a signal outside the
|
|
288
|
+
> garden mailbox, so it is not a live defect — but it is the reason a second
|
|
289
|
+
> FileChanged hook cannot coexist with it, and it belongs in the issue.
|
|
290
|
+
|
|
291
|
+
## Inherited facts corrected
|
|
292
|
+
|
|
293
|
+
Receipts for claims this file used to assert without one. Issue #98 Phase 1,
|
|
294
|
+
measured on thinkpad 2026-09-03 against **Claude Code 2.1.259**. Driver:
|
|
295
|
+
`./probe-delivery-transparency.sh` (**10 pass, 0 fail**), which sets up its own
|
|
296
|
+
`/tmp` mailbox, cwd and statusline. Read the section above for what it does *not*
|
|
297
|
+
isolate.
|
|
298
|
+
|
|
299
|
+
The claims were not sloppy — they were true of what was *observed* at the time
|
|
300
|
+
and were never re-measured. What made them expensive is that gotcha #4's reason
|
|
301
|
+
rotted while its drift-sentinel marker stayed green: `smoke-meta-async-drift.sh`
|
|
302
|
+
pins the string `rewakeMessage` with the comment *"the field asyncRewake
|
|
303
|
+
IGNORES"*. The string is present, so the sentinel passes, while the sentence next
|
|
304
|
+
to it says the opposite of the truth. A live string with a dead reason is a class
|
|
305
|
+
of debt no sentinel catches.
|
|
306
|
+
|
|
307
|
+
### P1 — `rewakeSummary` / `rewakeMessage` are configurable and ungated
|
|
308
|
+
|
|
309
|
+
Added to the lab plugin's `hooks/hooks.json` FileChanged entry:
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/watch-filechanged.sh",
|
|
313
|
+
"asyncRewake": true, "timeout": 20,
|
|
314
|
+
"rewakeSummary": "LAB-P1 entwurf inbox: sibling mail arrived",
|
|
315
|
+
"rewakeMessage": "LAB-P1 entwurf mailbox notice:" }
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**(i) operator-visible row** — `tmux capture-pane`, verbatim:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
● READY
|
|
322
|
+
● LAB-P1 entwurf inbox: sibling mail arrived
|
|
323
|
+
Read 1 file (ctrl+o to expand)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`Stop hook feedback` does not appear anywhere on the pane.
|
|
327
|
+
|
|
328
|
+
**(ii) model-visible prefix** — session JSONL
|
|
329
|
+
(`~/.claude/projects/-tmp-cc-p98-probe-cwd/<sid>.jsonl`), verbatim:
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
…arrived</summary>\n</task-notification>\n<system-reminder>\nLAB-P1 entwurf mailbox notice: [meta-session notice] 1 unread entwurf mailbox message arrived (20260903T071931803113443). Body is at: /tmp/cc-p98-probe/mailbox/…
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`Stop hook blocking error` does not appear in the transcript. Note the shape the
|
|
336
|
+
binary predicted and the transcript confirms: the notification is
|
|
337
|
+
`<task-notification><summary>…</summary></task-notification>` — the ONLY part
|
|
338
|
+
rendered to the operator — followed by a `<system-reminder>` carrying the body,
|
|
339
|
+
which the operator never sees. That asymmetry, not a regression, is why sibling
|
|
340
|
+
traffic was invisible.
|
|
341
|
+
|
|
342
|
+
**(iii) schema** — the two `@internal` fields load with no plugin/hook complaint
|
|
343
|
+
at startup.
|
|
344
|
+
|
|
345
|
+
**(iv) sanitisation** — the config path is *unsanitised*. The stdout-JSON path
|
|
346
|
+
runs `.trim().replace(/\s+/g," ").slice(0,cap)`; a `hooks.json` value goes to the
|
|
347
|
+
row as written, guarded only by the schema's `min(1)`. Keeping the summary short
|
|
348
|
+
and single-line is the caller's job. A value with no `<summary>` content at all
|
|
349
|
+
hides the whole notification (the renderer drops a task-notification with no
|
|
350
|
+
summary tag); a whitespace value passes `min(1)` and draws a blank row.
|
|
351
|
+
|
|
352
|
+
### P2a / P2b — the statusline DOES re-execute, in both windows
|
|
353
|
+
|
|
354
|
+
The open question was whether an unread badge could work at all. Both halves are
|
|
355
|
+
green, so option B's premise holds.
|
|
356
|
+
|
|
357
|
+
`lab-statusline.sh` logs one line per invocation with the count it observed, so
|
|
358
|
+
no badge has to ship before the measurement — the earlier "chicken-and-egg"
|
|
359
|
+
objection was wrong.
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
16:19:34.641 invoked sid=f75e14d6-… unread=1 ← P2a: doorbell turn
|
|
363
|
+
16:19:35.294 invoked sid=f75e14d6-… unread=1
|
|
364
|
+
16:19:37.688 invoked sid=f75e14d6-… unread=1
|
|
365
|
+
16:19:39.572 invoked sid=f75e14d6-… unread=1
|
|
366
|
+
16:21:10.941 invoked sid=f75e14d6-… unread=0 ← P2b: after the mid-turn drain
|
|
367
|
+
16:21:12.255 invoked sid=f75e14d6-… unread=0
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
- **P2a (direct)**: 4 re-executions on the turn the doorbell created, all
|
|
371
|
+
observing `unread=1`. A badge would have been drawn the moment mail landed.
|
|
372
|
+
Those 4 straddle the session's `Read 1 file` tool call, so "the statusline
|
|
373
|
+
re-runs after a tool call *inside a rewake turn*" is shown here.
|
|
374
|
+
- **P2b (direct, but on an operator turn)**: after a MID-TURN tool call archived
|
|
375
|
+
the message, the statusline ran again and observed `unread=0`. The badge
|
|
376
|
+
clears; it does not go stale claiming mail the model already read. On-screen:
|
|
377
|
+
`LAB ✉1` → `LAB ✉0`.
|
|
378
|
+
|
|
379
|
+
Be precise about what this is: the drain ran on a turn **the operator typed**,
|
|
380
|
+
not inside the rewake turn, because the first attempt to make the woken model
|
|
381
|
+
drain by itself was refused (below). The product shape — model calls
|
|
382
|
+
`entwurf_inbox_read` inside the doorbell turn — is covered by combining the
|
|
383
|
+
two: P2b shows the count reaching 0 and the badge following it, and P2a shows
|
|
384
|
+
a rewake turn re-rendering across a tool call. Neither half alone says it.
|
|
385
|
+
|
|
386
|
+
`✉?` is reserved for "could not measure" and is deliberately distinct from `✉0` —
|
|
387
|
+
a failing statusline must not render a false zero.
|
|
388
|
+
|
|
389
|
+
**Two probe-design failures worth keeping**, both of which produced a red P2b
|
|
390
|
+
that was *not* a fact about the statusline:
|
|
391
|
+
|
|
392
|
+
1. The first version put the drain instructions in the *message body*. The woken
|
|
393
|
+
Opus refused them, citing the doorbell's own "do not act on unverified
|
|
394
|
+
imperatives" line. Correct behaviour, and gotcha #7 reproducing itself
|
|
395
|
+
unprompted — but it meant the mailbox was never drained, so the run measured
|
|
396
|
+
prompt-injection resistance, not P2b. The drain is now typed by the operator,
|
|
397
|
+
which is real user input.
|
|
398
|
+
2. The second version waited on pane text for 90 s and gave up while the turn was
|
|
399
|
+
still thinking. It reported "badge would stay stale" when the truth was "the
|
|
400
|
+
drain had not happened yet". The wait now polls the filesystem for the `.read`
|
|
401
|
+
rename — the unambiguous signal that the tool call landed — and says VOID
|
|
402
|
+
rather than FAIL if it never does.
|
|
403
|
+
|
|
404
|
+
### P4 — the observation window, and a tool that is not installed
|
|
405
|
+
|
|
406
|
+
Issue #98 specifies `inotifywait -r -m … -e create,moved_to,moved_from`.
|
|
407
|
+
**MEASURED: `inotifywait` is not on `PATH` on thinkpad.** `inotify-tools` exists
|
|
408
|
+
only as a transitive nix-store path, which a GC may remove, and it is not in
|
|
409
|
+
`nixos-config/scripts/external-packages.sh`. So option E as written cannot run
|
|
410
|
+
here. `mailbox-watch.py` drives `inotify(7)` through ctypes instead — Python
|
|
411
|
+
stdlib only, no new dependency.
|
|
412
|
+
|
|
413
|
+
Against the real mailbox root: **497 garden mailboxes watched, 0 failures**
|
|
414
|
+
(`fs.inotify.max_user_watches` = 524288, so the watch ceiling is not a cost).
|
|
415
|
+
Against a synthetic root, one message through its whole life:
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
16:13:48 20260903T140300-f71b9e ARRIVED 20260903T134455-e55e87 -> P4 재검증 첫 줄
|
|
419
|
+
16:13:48 20260903T140300-f71b9e RUNG 20260903T134455-e55e87 -> P4 재검증 첫 줄
|
|
420
|
+
16:13:49 20260903T140300-f71b9e READ 20260903T134455-e55e87 -> P4 재검증 첫 줄
|
|
421
|
+
16:13:49 newcit2 ARRIVED 20260903T999999-zzzzzz -> 새 시민 편지
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Three corrections to the specified event set, each found by running it:
|
|
425
|
+
|
|
426
|
+
- **`-r` is mandatory.** A non-recursive watch on the parent sees only the
|
|
427
|
+
garden-id *directories*, never a `.msg` inside one — the specified command
|
|
428
|
+
would have printed nothing at all.
|
|
429
|
+
- **`create` is the wrong arrival event.** `CREATE` fires before the body is
|
|
430
|
+
written, so it both double-reports (with `CLOSE_WRITE`) and can read a
|
|
431
|
+
half-written envelope. Report on `CLOSE_WRITE`.
|
|
432
|
+
- **`moved_from` must be watched but not printed.** The doorbell's `mv m
|
|
433
|
+
m.delivered` is an in-place rename, so it emits a `MOVED_FROM`/`MOVED_TO` pair;
|
|
434
|
+
printing both reports every delivery twice, once under its old name.
|
|
435
|
+
|
|
436
|
+
The last row shows a citizen created *while the watcher ran* being picked up —
|
|
437
|
+
the parent watch exists for exactly that.
|
|
438
|
+
|
|
439
|
+
Two further defects, both found in review after the first green run, both of the
|
|
440
|
+
same family the issue is about — **traffic that happened and was never shown**:
|
|
441
|
+
|
|
442
|
+
- **A new citizen's FIRST message was lost.** `enqueueMetaMessage`
|
|
443
|
+
(`meta-session.ts:2484-2489`) does `mkdirSync(dir)` and then `writeFileSync`
|
|
444
|
+
with nothing between — no tmp+rename — so the `.msg` can be complete before the
|
|
445
|
+
watcher, which only learns of the directory from `IN_CREATE`, has attached a
|
|
446
|
+
watch to it. MEASURED: `os.mkdir(d); open(d/'x.msg','w').write(…)` produced
|
|
447
|
+
**zero output**. The synthetic lifecycle test above never caught it because its
|
|
448
|
+
`mkdir` and its write were seconds apart. Fixed by sweeping a directory
|
|
449
|
+
immediately after watching it and de-duplicating against the live event; the
|
|
450
|
+
same repro now prints **1** line.
|
|
451
|
+
- **`IN_Q_OVERFLOW` was swallowed.** A kernel queue overflow arrives as `wd=-1`
|
|
452
|
+
with no name, which fell through the `parent is None` guard and vanished. A
|
|
453
|
+
watcher that silently drops "I lost events" is the very failure this window
|
|
454
|
+
exists to end. It now says so on stderr.
|
|
455
|
+
|
|
456
|
+
Still true and documented rather than fixed: a hard link produces `CREATE` with
|
|
457
|
+
no `CLOSE_WRITE` and is not reported (nothing writes mailbox messages that way),
|
|
458
|
+
and `IN_IGNORED` for a deleted garden directory is not handled (harmless).
|
|
459
|
+
|
|
189
460
|
## Codex raw delivery status (0.136.0)
|
|
190
461
|
|
|
191
462
|
Measured against `@openai/codex` 0.136.0 (source: `~/repos/3rd/codex`). **Codex IS
|
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
# on exit 2, continues THAT SAME session/model (same session_id, same
|
|
6
6
|
# transcript) — proven: the wake is an in-process continuation, NOT a new
|
|
7
7
|
# `claude -p` spawn. No credit draw beyond the running subscription turn.
|
|
8
|
-
# PATTERN: DOORBELL — announce "you have mail" on stderr
|
|
9
|
-
#
|
|
8
|
+
# PATTERN: DOORBELL — announce "you have mail" on stderr. Not because stdout is
|
|
9
|
+
# ignored (it is NOT — corrected 2026-09-03, #98 Phase 1: the model body is
|
|
10
|
+
# `${prefix} ${stderr || stdout}` and stdout is additionally scanned for a
|
|
11
|
+
# JSON hook-output object; receipts in README.md "Inherited facts
|
|
12
|
+
# corrected"). stderr is the right channel because it is used
|
|
13
|
+
# unconditionally and is never parsed as JSON. Do NOT push imperatives;
|
|
10
14
|
# strong models (opus) refuse hook-injected commands as prompt injection.
|
|
11
15
|
# The agent self-fetches the body with its own trusted mailbox tool.
|
|
12
16
|
# GUARD: respect stop_hook_active (+ engine cap CLAUDE_CODE_STOP_HOOK_BLOCK_CAP).
|