@junghanacs/entwurf 0.17.1 → 0.18.0
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 +2 -1
- package/CHANGELOG.md +248 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +76 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +9 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +11 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +66 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +10 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +8 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-mailbox.js +9 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +25 -13
- 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 +24 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +61 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +64 -4
- package/mcp/entwurf-bridge/src/index.ts +16 -8
- package/package.json +3 -2
- 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/entwurf-deliverability.ts +104 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +20 -6
- package/pi-extensions/lib/entwurf-facts.ts +52 -3
- package/pi-extensions/lib/entwurf-peer-observe.ts +77 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +12 -1
- package/pi-extensions/lib/entwurf-v2-decider.ts +26 -8
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +9 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +32 -12
- 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 +24 -3
- package/pi-extensions/lib/meta-session.ts +76 -7
- package/pi-extensions/meta-bridge-hook.ts +72 -3
- package/run.sh +21 -0
- package/scripts/check-bridge-delivery.ts +42 -1
- package/scripts/check-entwurf-fact-provider.ts +22 -1
- package/scripts/check-entwurf-facts.ts +17 -2
- package/scripts/check-entwurf-peers-surface.ts +20 -2
- package/scripts/check-entwurf-self-address.ts +20 -2
- package/scripts/check-entwurf-v2-decider.ts +6 -3
- package/scripts/check-entwurf-v2-mailbox.ts +33 -0
- package/scripts/check-entwurf-v2-matrix.ts +10 -5
- package/scripts/check-entwurf-v2-production.ts +72 -1
- 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-hook-session-switch.ts +560 -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/meta-bridge-doctor.sh +47 -7
- package/scripts/meta-bridge-state.py +23 -5
- package/scripts/meta-bridge-statusline.sh +65 -2
- package/scripts/mutants/meta-hook-session-switch.json +223 -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/raw-claude-session-switch/README.md +170 -0
- package/scripts/smoke-acp-cortex-live.ts +22 -2
- package/scripts/smoke-acp-v2-send-live.ts +23 -2
- package/scripts/smoke-entwurf-chain-live.ts +85 -13
- package/scripts/smoke-entwurf-v2-matrix-live.ts +23 -1
- 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-fresh-call-live.ts +13 -3
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# probe-delivery-transparency.sh — issue #98 Phase 1, probes P1 + P2a + P2b.
|
|
3
|
+
#
|
|
4
|
+
# MEASUREMENT ONLY -- but read the next paragraph before believing that phrase.
|
|
5
|
+
#
|
|
6
|
+
# It edits no product FILE: nothing under pi/meta-bridge/**, pi-extensions/**,
|
|
7
|
+
# mcp/** or scripts/meta-bridge-*.sh, and it installs nothing.
|
|
8
|
+
#
|
|
9
|
+
# Isolation from operator STATE took two tries to get right, and both failures are
|
|
10
|
+
# worth carrying:
|
|
11
|
+
#
|
|
12
|
+
# 1. Without `--setting-sources project,local` the session inherits
|
|
13
|
+
# ~/.claude/settings.json, so the PRODUCT meta-bridge plugin loads next to the
|
|
14
|
+
# lab one and its SessionStart hook mints a REAL garden citizen (a record in
|
|
15
|
+
# ~/.pi/agent/meta-sessions/, a mailbox in ~/.pi/agent/meta-mailbox/).
|
|
16
|
+
# MEASURED: three runs left three citizens behind.
|
|
17
|
+
# 2. Worse, the two doorbells DO cross-fire, and an earlier version of this
|
|
18
|
+
# header claimed they did not. `doorbell.sh` takes `dirname(file_path)` as
|
|
19
|
+
# "its" mailbox unconditionally, so the product doorbell happily processed the
|
|
20
|
+
# LAB mailbox and raced the lab hook to exit 2. When the product hook won, the
|
|
21
|
+
# operator got the DEFAULT "Stop hook feedback" -- from a hook that carries no
|
|
22
|
+
# rewakeSummary -- while hooks.json under test was perfectly correct.
|
|
23
|
+
# MEASURED: two consecutive runs failed P1 that way. The transcript proved it:
|
|
24
|
+
# product wording ("[entwurf inbox] ... entwurf_inbox_read ... lastReadAt")
|
|
25
|
+
# pointing at the lab path /tmp/cc-p98-probe/mailbox/<sid>/.
|
|
26
|
+
#
|
|
27
|
+
# So P1 was non-deterministic until the setting sources were cut. Dropping user
|
|
28
|
+
# settings fixes both: no product plugin, no minted citizen, no doorbell race.
|
|
29
|
+
# `cleanup_citizens` stays as a belt-and-braces sweep for anything a run under the
|
|
30
|
+
# old flags left behind; it deletes only records whose cwd matches this probe's
|
|
31
|
+
# throwaway /tmp path, and reports rather than removes a non-empty mailbox.
|
|
32
|
+
#
|
|
33
|
+
# Claude still writes ~/.claude/projects/<cwd>/<sid>.jsonl and
|
|
34
|
+
# ~/.claude/sessions/<pid>.json of its own accord -- receipt (ii) below IS one of
|
|
35
|
+
# those files. That is Claude's own bookkeeping and is left alone.
|
|
36
|
+
#
|
|
37
|
+
# WHAT EACH PROBE DECIDES
|
|
38
|
+
# P1 hooks.json `rewakeSummary` / `rewakeMessage` on an asyncRewake hook:
|
|
39
|
+
# does the operator's row stop saying "Stop hook feedback", and does the
|
|
40
|
+
# model stop being told "Stop hook blocking error"? Binary reads say both
|
|
41
|
+
# fields are ungated for a local plugin (only the stdout-JSON rewakeSummary
|
|
42
|
+
# is first-party gated). This is the live confirmation, and the receipt
|
|
43
|
+
# that retires README lesson #4.
|
|
44
|
+
# P2a Does Claude RE-EXECUTE the statusline command on the turn an asyncRewake
|
|
45
|
+
# doorbell creates? If not, a statusline unread badge cannot be the primary
|
|
46
|
+
# surface.
|
|
47
|
+
# P2b Does it re-execute again after a MID-TURN tool call drains the mailbox?
|
|
48
|
+
# If not, the badge would keep showing unread after the model already read
|
|
49
|
+
# -- the badge would lie in the direction that matters, so option B would
|
|
50
|
+
# WEAKEN the doorbell-rang-vs-model-read distinction instead of showing it.
|
|
51
|
+
#
|
|
52
|
+
# P2a/P2b need no badge on screen: lab-statusline.sh logs one line per
|
|
53
|
+
# invocation with the count it observed, so the log alone answers both.
|
|
54
|
+
#
|
|
55
|
+
# COST: one interactive subscription session; the wake itself is a continuation,
|
|
56
|
+
# not a `claude -p` spawn.
|
|
57
|
+
# USAGE: ./probe-delivery-transparency.sh [keep] (`keep` leaves tmux alive)
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
|
|
60
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
61
|
+
PLUGIN="$HERE/plugin-entwurf-receive"
|
|
62
|
+
T="${CC_PROBE_DIR:-/tmp/cc-p98-probe}"
|
|
63
|
+
ROOT="$T/mailbox"
|
|
64
|
+
CWD="$T/cwd"
|
|
65
|
+
SLOG="$T/statusline.log"
|
|
66
|
+
SESSION="ccp98"
|
|
67
|
+
KEEP="${1:-}"
|
|
68
|
+
|
|
69
|
+
pass=0 fail=0
|
|
70
|
+
ok() {
|
|
71
|
+
echo " PASS: $*"
|
|
72
|
+
pass=$((pass + 1))
|
|
73
|
+
}
|
|
74
|
+
bad() {
|
|
75
|
+
echo " FAIL: $*"
|
|
76
|
+
fail=$((fail + 1))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
rm -rf "$T"
|
|
80
|
+
mkdir -p "$ROOT" "$CWD/.claude"
|
|
81
|
+
: >"$SLOG"
|
|
82
|
+
|
|
83
|
+
# The lab statusline is wired through the PROJECT settings of a throwaway /tmp
|
|
84
|
+
# cwd, so it overrides the operator's statusline for THIS session only -- no file
|
|
85
|
+
# in ~/.claude is edited. (Claude still WRITES session/transcript files there of
|
|
86
|
+
# its own accord; see the header.)
|
|
87
|
+
cat >"$CWD/.claude/settings.json" <<JSON
|
|
88
|
+
{
|
|
89
|
+
"statusLine": {
|
|
90
|
+
"type": "command",
|
|
91
|
+
"command": "$HERE/lab-statusline.sh"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
JSON
|
|
95
|
+
|
|
96
|
+
# Remove the garden citizens this probe's session minted. Matching is on the
|
|
97
|
+
# record's own `cwd` field against this probe's throwaway /tmp cwd -- an exact
|
|
98
|
+
# string compare on a path no real citizen can have. A record that does not match
|
|
99
|
+
# is never touched, and anything unexpected is printed instead of deleted.
|
|
100
|
+
cleanup_citizens() {
|
|
101
|
+
local cwd="$1"
|
|
102
|
+
python3 - "$cwd" <<'PY'
|
|
103
|
+
import json, shutil, sys
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
|
|
106
|
+
cwd = sys.argv[1]
|
|
107
|
+
recs = Path.home() / ".pi" / "agent" / "meta-sessions"
|
|
108
|
+
boxes = Path.home() / ".pi" / "agent" / "meta-mailbox"
|
|
109
|
+
if not recs.is_dir():
|
|
110
|
+
sys.exit(0)
|
|
111
|
+
|
|
112
|
+
removed = []
|
|
113
|
+
for f in sorted(recs.glob("*.meta.json")):
|
|
114
|
+
try:
|
|
115
|
+
d = json.loads(f.read_text())
|
|
116
|
+
except Exception:
|
|
117
|
+
continue
|
|
118
|
+
if d.get("cwd") != cwd:
|
|
119
|
+
continue
|
|
120
|
+
gid = d.get("gardenId") or f.name.removesuffix(".meta.json")
|
|
121
|
+
box = boxes / gid
|
|
122
|
+
# Refuse to delete a mailbox that holds real traffic: only the SessionStart
|
|
123
|
+
# signal file is expected here. Anything else means this citizen was actually
|
|
124
|
+
# used, and a probe must not destroy evidence -- say so and leave it.
|
|
125
|
+
leftovers = sorted(p.name for p in box.iterdir()) if box.is_dir() else []
|
|
126
|
+
if [n for n in leftovers if n != "inbox.signal" and not n.endswith(".log")]:
|
|
127
|
+
print(f" ! left {gid}: mailbox is not empty ({', '.join(leftovers)})")
|
|
128
|
+
continue
|
|
129
|
+
if box.is_dir():
|
|
130
|
+
shutil.rmtree(box)
|
|
131
|
+
f.unlink()
|
|
132
|
+
removed.append(gid)
|
|
133
|
+
|
|
134
|
+
if removed:
|
|
135
|
+
print(f" cleaned {len(removed)} probe-minted citizen(s): {', '.join(removed)}")
|
|
136
|
+
else:
|
|
137
|
+
print(" no probe-minted citizens to clean")
|
|
138
|
+
PY
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
sid_for_cwd() {
|
|
142
|
+
python3 - "$1" <<'PY'
|
|
143
|
+
import json, glob, os, sys
|
|
144
|
+
cwd = sys.argv[1]
|
|
145
|
+
for f in glob.glob(os.path.expanduser("~/.claude/sessions/*.json")):
|
|
146
|
+
try: d = json.load(open(f))
|
|
147
|
+
except Exception: continue
|
|
148
|
+
if d.get("cwd") == cwd and d.get("kind") == "interactive":
|
|
149
|
+
print(d.get("sessionId")); break
|
|
150
|
+
PY
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
echo "== SETUP =="
|
|
154
|
+
echo " claude: $(claude --version 2>/dev/null)"
|
|
155
|
+
echo " plugin: $PLUGIN"
|
|
156
|
+
echo " mailbox: $ROOT"
|
|
157
|
+
echo " slog: $SLOG"
|
|
158
|
+
echo " hooks.json FileChanged entry under test:"
|
|
159
|
+
python3 -c 'import json,sys;print(" "+json.dumps(json.load(open(sys.argv[1]))["hooks"]["FileChanged"][0]["hooks"][0],ensure_ascii=False))' \
|
|
160
|
+
"$PLUGIN/hooks/hooks.json"
|
|
161
|
+
|
|
162
|
+
tmux kill-session -t "$SESSION" 2>/dev/null || true
|
|
163
|
+
tmux new-session -d -s "$SESSION" -x 220 -y 50
|
|
164
|
+
tmux send-keys -t "$SESSION" "export CC_MAILBOX_ROOT='$ROOT' CC_LAB_STATUSLINE_LOG='$SLOG'" Enter
|
|
165
|
+
# --setting-sources project,local DROPS ~/.claude/settings.json, which is where
|
|
166
|
+
# the product meta-bridge plugin is enabled (extraKnownMarketplaces +
|
|
167
|
+
# enabledPlugins). Without it the probe was NOT isolated in two ways, both
|
|
168
|
+
# measured: the product SessionStart hook minted a real garden citizen, and the
|
|
169
|
+
# product doorbell -- which takes dirname(file_path) as "its" mailbox
|
|
170
|
+
# unconditionally -- processed the LAB mailbox and won the race to exit 2, so the
|
|
171
|
+
# operator saw the DEFAULT "Stop hook feedback" from a hook that has no
|
|
172
|
+
# rewakeSummary. Two runs failed P1 that way while hooks.json was correct.
|
|
173
|
+
# The project settings under $CWD/.claude still load, so the lab statusline stays.
|
|
174
|
+
tmux send-keys -t "$SESSION" "cd '$CWD' && claude --plugin-dir '$PLUGIN' --setting-sources project,local --dangerously-skip-permissions" Enter
|
|
175
|
+
|
|
176
|
+
echo "== ARM =="
|
|
177
|
+
SID=""
|
|
178
|
+
for _ in $(seq 1 45); do
|
|
179
|
+
[ -z "$SID" ] && SID=$(sid_for_cwd "$CWD")
|
|
180
|
+
[ -n "$SID" ] && [ -f "$ROOT/$SID/hook.log" ] && break
|
|
181
|
+
sleep 1
|
|
182
|
+
done
|
|
183
|
+
[ -n "$SID" ] || {
|
|
184
|
+
echo "FAIL: session never started"
|
|
185
|
+
exit 1
|
|
186
|
+
}
|
|
187
|
+
[ -f "$ROOT/$SID/hook.log" ] || {
|
|
188
|
+
echo "FAIL: watch never armed"
|
|
189
|
+
exit 1
|
|
190
|
+
}
|
|
191
|
+
echo " sessionId: $SID"
|
|
192
|
+
echo " arm: $(cat "$ROOT/$SID/hook.log")"
|
|
193
|
+
|
|
194
|
+
# (iii) schema acceptance: two @internal fields must not make the plugin unloadable.
|
|
195
|
+
# A rejected hooks.json shows up as a hook/plugin error on the pane at startup.
|
|
196
|
+
STARTUP_PANE="$(tmux capture-pane -t "$SESSION" -p)"
|
|
197
|
+
if printf '%s' "$STARTUP_PANE" | grep -qiE 'hook.*(invalid|error|failed)|plugin.*(invalid|error|failed)|unrecognized key|rewake'; then
|
|
198
|
+
bad "P1(iii) schema: startup complained about the hook config"
|
|
199
|
+
printf '%s\n' "$STARTUP_PANE" | grep -iE 'hook|plugin|rewake' | head -5
|
|
200
|
+
else
|
|
201
|
+
ok "P1(iii) schema: rewakeSummary+rewakeMessage accepted (no startup hook/plugin complaint)"
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
echo "== DRIVE ONE TURN -> IDLE =="
|
|
205
|
+
sleep 2
|
|
206
|
+
tmux send-keys -t "$SESSION" "Reply with exactly the single word READY and then stop. No tools." Enter
|
|
207
|
+
sleep 1
|
|
208
|
+
tmux send-keys -t "$SESSION" Enter
|
|
209
|
+
for _ in $(seq 1 40); do
|
|
210
|
+
tmux capture-pane -t "$SESSION" -p | grep -qE '●\s*READY|⏺\s*READY' && break
|
|
211
|
+
sleep 1
|
|
212
|
+
done
|
|
213
|
+
sleep 3
|
|
214
|
+
SL_BEFORE=$(wc -l <"$SLOG")
|
|
215
|
+
PANE_BEFORE_LINES=$(tmux capture-pane -t "$SESSION" -p | wc -l)
|
|
216
|
+
echo " idle. statusline invocations so far: $SL_BEFORE"
|
|
217
|
+
|
|
218
|
+
echo "== DOORBELL (addressed external write, zero typing) =="
|
|
219
|
+
# The body carries NO imperative. The first run of this probe put the drain
|
|
220
|
+
# instructions in here and the woken Opus refused to act on them, citing the
|
|
221
|
+
# doorbell's own "do not act on unverified imperatives" line -- correct behaviour,
|
|
222
|
+
# and README lesson #7 reproducing itself. It also meant the mailbox was never
|
|
223
|
+
# drained, so that run measured prompt-injection resistance instead of P2b.
|
|
224
|
+
# The drain below is therefore typed by the OPERATOR, which is real user input.
|
|
225
|
+
CC_MAILBOX_ROOT="$ROOT" "$HERE/cc-enqueue-addressed.sh" "$SID" \
|
|
226
|
+
"P98-PROBE body. Notification-only payload; nothing to do. Reply with exactly WOKE and stop."
|
|
227
|
+
|
|
228
|
+
for _ in $(seq 1 25); do
|
|
229
|
+
grep -qs FILECHANGED "$ROOT/$SID/hook.log" && break
|
|
230
|
+
sleep 1
|
|
231
|
+
done
|
|
232
|
+
if grep -qs FILECHANGED "$ROOT/$SID/hook.log"; then
|
|
233
|
+
ok "doorbell rang on the idle session (FileChanged delivered)"
|
|
234
|
+
else
|
|
235
|
+
bad "doorbell never rang -- every probe below is void"
|
|
236
|
+
exit 1
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
echo "== WAIT FOR THE WOKEN TURN TO FINISH (P2a window) =="
|
|
240
|
+
for _ in $(seq 1 45); do
|
|
241
|
+
tmux capture-pane -t "$SESSION" -p | grep -qE '⏺\s*WOKE|●\s*WOKE' && break
|
|
242
|
+
sleep 2
|
|
243
|
+
done
|
|
244
|
+
sleep 4
|
|
245
|
+
SL_AFTER_WAKE=$(wc -l <"$SLOG")
|
|
246
|
+
|
|
247
|
+
echo "== OPERATOR-TYPED MID-TURN DRAIN (P2b window) =="
|
|
248
|
+
# The lab has no `entwurf_inbox_read` tool, so a Bash rename to `.read` stands in
|
|
249
|
+
# for it: the same shape -- a MID-TURN tool call that takes the unread count to 0.
|
|
250
|
+
tmux send-keys -t "$SESSION" \
|
|
251
|
+
"Immediately run exactly this one Bash command, nothing else first: mv $ROOT/$SID/*.msg.delivered $ROOT/$SID/drained.msg.delivered.read" Enter
|
|
252
|
+
sleep 1
|
|
253
|
+
tmux send-keys -t "$SESSION" Enter
|
|
254
|
+
# Poll the FILESYSTEM, not the pane. Pane text is a race: the first corrected run
|
|
255
|
+
# timed out at 90s while the turn was still thinking (high effort), and reported a
|
|
256
|
+
# P2b failure that was really "the drain had not happened yet". The rename itself
|
|
257
|
+
# is the unambiguous signal that the mid-turn tool call landed.
|
|
258
|
+
DRAINED=0
|
|
259
|
+
for _ in $(seq 1 90); do
|
|
260
|
+
if ls "$ROOT/$SID"/*.msg.delivered.read >/dev/null 2>&1; then
|
|
261
|
+
DRAINED=1
|
|
262
|
+
break
|
|
263
|
+
fi
|
|
264
|
+
sleep 2
|
|
265
|
+
done
|
|
266
|
+
if [ "$DRAINED" -eq 1 ]; then
|
|
267
|
+
ok "mid-turn drain landed (mailbox archived to .read)"
|
|
268
|
+
else
|
|
269
|
+
bad "mid-turn drain never landed -- P2b below is VOID, not a measurement"
|
|
270
|
+
fi
|
|
271
|
+
# Give the status area time to render at least once after the tool result.
|
|
272
|
+
sleep 6
|
|
273
|
+
|
|
274
|
+
PANE="$(tmux capture-pane -t "$SESSION" -p)"
|
|
275
|
+
JSONL="$HOME/.claude/projects/$(printf '%s' "$CWD" | sed 's#/#-#g')/$SID.jsonl"
|
|
276
|
+
|
|
277
|
+
echo
|
|
278
|
+
echo "===================== P1: OPERATOR-VISIBLE ROW ====================="
|
|
279
|
+
printf '%s\n' "$PANE" | grep -nE 'Stop hook feedback|LAB-P1' || echo " (neither string on the pane)"
|
|
280
|
+
if printf '%s' "$PANE" | grep -q 'LAB-P1 entwurf inbox: sibling mail arrived'; then
|
|
281
|
+
ok "P1(i) pane row shows our rewakeSummary"
|
|
282
|
+
else
|
|
283
|
+
bad "P1(i) pane row does NOT show our rewakeSummary"
|
|
284
|
+
fi
|
|
285
|
+
if printf '%s' "$PANE" | grep -q 'Stop hook feedback'; then
|
|
286
|
+
bad "P1(i) pane still shows the default 'Stop hook feedback'"
|
|
287
|
+
else
|
|
288
|
+
ok "P1(i) default 'Stop hook feedback' is gone from the pane"
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
echo
|
|
292
|
+
echo "===================== P1: MODEL-VISIBLE PREFIX ====================="
|
|
293
|
+
if [ -f "$JSONL" ]; then
|
|
294
|
+
echo " transcript: $JSONL"
|
|
295
|
+
python3 - "$JSONL" <<'PY'
|
|
296
|
+
import json, sys
|
|
297
|
+
hits = []
|
|
298
|
+
for line in open(sys.argv[1], encoding="utf-8", errors="replace"):
|
|
299
|
+
if "meta-session notice" not in line and "Stop hook" not in line and "LAB-P1" not in line:
|
|
300
|
+
continue
|
|
301
|
+
try: rec = json.loads(line)
|
|
302
|
+
except Exception: continue
|
|
303
|
+
txt = json.dumps(rec, ensure_ascii=False)
|
|
304
|
+
for needle in ("Stop hook blocking error", "LAB-P1 entwurf mailbox notice"):
|
|
305
|
+
if needle in txt:
|
|
306
|
+
i = txt.index(needle)
|
|
307
|
+
hits.append((needle, txt[max(0, i - 60):i + 190]))
|
|
308
|
+
for needle, ctx in hits[:4]:
|
|
309
|
+
print(f" [{needle}]\n …{ctx}…")
|
|
310
|
+
if not hits:
|
|
311
|
+
print(" (neither prefix found in the transcript)")
|
|
312
|
+
PY
|
|
313
|
+
if grep -q 'LAB-P1 entwurf mailbox notice' "$JSONL"; then
|
|
314
|
+
ok "P1(ii) model prefix replaced by our rewakeMessage"
|
|
315
|
+
else
|
|
316
|
+
bad "P1(ii) our rewakeMessage did NOT reach the model"
|
|
317
|
+
fi
|
|
318
|
+
if grep -q 'Stop hook blocking error' "$JSONL"; then
|
|
319
|
+
bad "P1(ii) model still framed with 'Stop hook blocking error'"
|
|
320
|
+
else
|
|
321
|
+
ok "P1(ii) 'Stop hook blocking error' framing is gone"
|
|
322
|
+
fi
|
|
323
|
+
else
|
|
324
|
+
bad "P1(ii) transcript not found at $JSONL"
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
echo
|
|
328
|
+
echo "===================== P2a / P2b: STATUSLINE RE-EXECUTION ====================="
|
|
329
|
+
echo " invocations before doorbell: $SL_BEFORE"
|
|
330
|
+
echo " --- statusline log lines added after idle ---"
|
|
331
|
+
tail -n +$((SL_BEFORE + 1)) "$SLOG" | sed 's/^/ /'
|
|
332
|
+
SL_AFTER=$(wc -l <"$SLOG")
|
|
333
|
+
WAKE_ADDED=$((SL_AFTER_WAKE - SL_BEFORE))
|
|
334
|
+
DRAIN_ADDED=$((SL_AFTER - SL_AFTER_WAKE))
|
|
335
|
+
echo " added by the doorbell turn: $WAKE_ADDED"
|
|
336
|
+
echo " added by the drain turn: $DRAIN_ADDED"
|
|
337
|
+
|
|
338
|
+
if [ "$WAKE_ADDED" -gt 0 ]; then
|
|
339
|
+
ok "P2a statusline RE-EXECUTED on the asyncRewake turn ($WAKE_ADDED invocation(s))"
|
|
340
|
+
else
|
|
341
|
+
bad "P2a statusline did NOT re-execute -- a badge would not appear when mail lands"
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
# P2a value check: the invocations during the doorbell turn must have SEEN the
|
|
345
|
+
# mail. A re-execution that still reports unread=0 would draw no badge.
|
|
346
|
+
if tail -n +$((SL_BEFORE + 1)) "$SLOG" | head -n "$WAKE_ADDED" | grep -q 'unread=[1-9]'; then
|
|
347
|
+
ok "P2a those invocations observed unread>=1 (a badge would have been drawn)"
|
|
348
|
+
else
|
|
349
|
+
bad "P2a re-executed but never observed the unread mail"
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
# P2b: after the mid-turn rename, an invocation must observe unread=0. If every
|
|
353
|
+
# line still says unread>=1, the badge would keep claiming mail the model already
|
|
354
|
+
# read -- option B would then weaken the rang-vs-read distinction, not show it.
|
|
355
|
+
if tail -n +$((SL_AFTER_WAKE + 1)) "$SLOG" | grep -q 'unread=0'; then
|
|
356
|
+
ok "P2b an invocation observed unread=0 after the mid-turn drain (badge clears)"
|
|
357
|
+
else
|
|
358
|
+
bad "P2b no invocation observed unread=0 -- badge would stay stale after the read"
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
echo
|
|
362
|
+
echo "===================== MAILBOX FINAL STATE ====================="
|
|
363
|
+
ls -1 "$ROOT/$SID/" | sed 's/^/ /'
|
|
364
|
+
echo " hook.log: $(cat "$ROOT/$SID/hook.log")"
|
|
365
|
+
|
|
366
|
+
echo
|
|
367
|
+
echo "===================== PANE (tail) ====================="
|
|
368
|
+
printf '%s\n' "$PANE" | grep -vE '^\s*$' | tail -22 | sed 's/^/ /'
|
|
369
|
+
echo "======================================================="
|
|
370
|
+
|
|
371
|
+
echo
|
|
372
|
+
echo "SUMMARY: $pass pass, $fail fail (claude $(claude --version 2>/dev/null | awk '{print $1}'))"
|
|
373
|
+
echo "receipts: pane above, transcript $JSONL, statusline log $SLOG"
|
|
374
|
+
if [ "$KEEP" = "keep" ]; then
|
|
375
|
+
echo "(tmux session '$SESSION' left alive)"
|
|
376
|
+
else
|
|
377
|
+
tmux kill-session -t "$SESSION" 2>/dev/null || true
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# The session inherited the operator's settings, so the PRODUCT meta-bridge minted
|
|
381
|
+
# a real citizen for this throwaway cwd. Give it back.
|
|
382
|
+
echo
|
|
383
|
+
echo "== OPERATOR-STATE CLEANUP =="
|
|
384
|
+
cleanup_citizens "$CWD"
|
|
385
|
+
echo " (left in place, written by Claude itself: ~/.claude/projects/$(printf '%s' "$CWD" | sed 's#/#-#g')/ and ~/.claude/sessions/*.json)"
|
|
386
|
+
|
|
387
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Raw measurement — what Claude Code's SessionStart actually says on a session switch (#101)
|
|
2
|
+
|
|
3
|
+
**Status: RUN on oracle, 2026-09-04, Claude Code 2.1.260, `--model haiku`.** GLG approved the
|
|
4
|
+
LIVE cells (S1–S5 first, S6 added after). Six cells, one host, one vendor version — that is the
|
|
5
|
+
whole claim. Everything below is verbatim from `~/.pi/agent/meta-bridge-hook.log`, produced by
|
|
6
|
+
the repaired hook, which now logs the envelope's `source` on every line it writes.
|
|
7
|
+
|
|
8
|
+
The #101 repair does **not** depend on any of it: the retirement rule reads the sender marker on
|
|
9
|
+
disk and branches on no vendor field. What this lab adds is the envelope order the diagnosis had
|
|
10
|
+
to inherit from vendor documentation — and it **refuted one inherited sentence** (see S2/S3).
|
|
11
|
+
|
|
12
|
+
## What the lab changed in the story
|
|
13
|
+
|
|
14
|
+
The diagnosis said the two SessionStarts come from the TUI resume picker firing once for a
|
|
15
|
+
placeholder id and again for the picked id. **Measured, that is not what happens.**
|
|
16
|
+
`claude --resume`, with or without the picker, fires exactly ONE SessionStart carrying the real
|
|
17
|
+
id (S2, S3). The two-SessionStart shape comes from a bare `claude` — which mints a real, empty
|
|
18
|
+
new session — followed by an in-session `/resume` or `/clear` (S4, S5). The field case fits: `ps`
|
|
19
|
+
showed pid 143742 started bare `claude` at 09:31:32, and the second SessionStart landed 4s later.
|
|
20
|
+
|
|
21
|
+
The consequence for the repair is nil (either way one pid stops serving one garden and starts
|
|
22
|
+
serving another, and the sender marker is what says so), but the prose was wrong and is now
|
|
23
|
+
corrected in `meta-bridge-hook.ts`, `entwurf-deliverability.ts`, `meta-session.ts` and
|
|
24
|
+
`check-meta-hook-session-switch.ts`.
|
|
25
|
+
|
|
26
|
+
## The three falsification conditions, judged
|
|
27
|
+
|
|
28
|
+
The pre-run README named three findings that would have changed the repair. All three are
|
|
29
|
+
answered:
|
|
30
|
+
|
|
31
|
+
1. **"A switch leaves the sender marker pointing at the OLD garden when the receiver marker has
|
|
32
|
+
already moved"** — the repair's one real assumption. **Not observed.** In S4 and S5 the
|
|
33
|
+
retirement line is written BEFORE the sender marker is rewritten, and both precede the new
|
|
34
|
+
garden's arm (log order below, same millisecond). The hook reads the pre-switch value it needs.
|
|
35
|
+
2. **"Compaction re-fires SessionStart under a NEW native id, so a compacted session is a garden
|
|
36
|
+
switch"** — **refuted.** Compaction (manual S6a and automatic S6b) re-fires SessionStart for
|
|
37
|
+
the SAME native id, attaches the SAME garden, and retires nothing. The same-garden rule covers
|
|
38
|
+
it; nothing had to change.
|
|
39
|
+
3. **"Three SessionStarts in one process"** — **not observed** in any cell. Every switch was
|
|
40
|
+
exactly one additional SessionStart, and the two-drive gate fixture stays adequate.
|
|
41
|
+
|
|
42
|
+
## The cells
|
|
43
|
+
|
|
44
|
+
| cell | how it was reached | SessionStarts | `source` | native id | retirement |
|
|
45
|
+
|---|---|---|---|---|---|
|
|
46
|
+
| S1 | bare `claude` | 1 | `startup` | new | — (nothing prior) |
|
|
47
|
+
| S2 | `claude --resume` → picker → Enter | **1** | `resume` | the picked one | — (no prior garden in this pid) |
|
|
48
|
+
| S3 | `claude --resume <id>` (argv) | **1** | `resume` | the given one | — |
|
|
49
|
+
| S4 | bare `claude`, then in-session `/resume` | **2** | `startup` → `resume` | new → picked | **yes** — the startup garden retired |
|
|
50
|
+
| S5 | in-session `/clear` | 1 more | `clear` | new | **yes** — the previous garden retired |
|
|
51
|
+
| S6a | in-session `/compact` | 1 more | `compact` | **same** | no (same garden) |
|
|
52
|
+
| S6b | automatic compaction at ~98% context | 1 more | `compact` | **same** | no (same garden) |
|
|
53
|
+
|
|
54
|
+
`UserPromptSubmit` carries **no** `source` at all (`source=(unset)`) — worth knowing, since it is
|
|
55
|
+
the event that rewrites the sender marker on every keystroke and must retire nothing.
|
|
56
|
+
|
|
57
|
+
It also always names the session it belongs to. Across the four lab pids, **8 of 8**
|
|
58
|
+
`UserPromptSubmit` events carried the native id their own pid's preceding `SessionStart` had
|
|
59
|
+
established, and none carried any other. The hook runs synchronously inside the session's own
|
|
60
|
+
process, so a keystroke envelope is that session speaking — which is why the pointer it moves is
|
|
61
|
+
treated as authoritative rather than as something to defend against.
|
|
62
|
+
|
|
63
|
+
## Verbatim receipts
|
|
64
|
+
|
|
65
|
+
**S1 — bare `claude`.** One SessionStart, a fresh garden, `source=startup`. Then one turn, whose
|
|
66
|
+
`UserPromptSubmit` carries no source and retires nothing (same garden):
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
2026-09-04T04:10:22.678Z INFO create record 20260904T131022-ac38b6.meta.json (event=SessionStart, source=startup, native=bbba9cbe-f11c-4b67-a0bd-991c3f678238)
|
|
70
|
+
2026-09-04T04:10:22.681Z INFO sender marker 564537 -> 20260904T131022-ac38b6 (event=SessionStart, source=startup)
|
|
71
|
+
2026-09-04T04:10:22.681Z INFO armed watch /home/junghan/.pi/agent/meta-mailbox/20260904T131022-ac38b6/inbox.signal
|
|
72
|
+
2026-09-04T04:10:22.682Z INFO receiver marker 20260904T131022-ac38b6 owner=564537 arm=SessionStart source=startup
|
|
73
|
+
2026-09-04T04:11:00.196Z INFO attach record 20260904T131022-ac38b6.meta.json (event=UserPromptSubmit, source=(unset), native=bbba9cbe-f11c-4b67-a0bd-991c3f678238)
|
|
74
|
+
2026-09-04T04:11:00.198Z INFO sender marker 564537 -> 20260904T131022-ac38b6 (event=UserPromptSubmit, source=(unset))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**S2 — `claude --resume`, picker, Enter.** Nothing at all is logged while the picker is open; the
|
|
78
|
+
single SessionStart lands on selection, already carrying the real native id. **No placeholder.**
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
2026-09-04T04:12:24.853Z INFO attach record 20260904T131022-ac38b6.meta.json (event=SessionStart, source=resume, native=bbba9cbe-f11c-4b67-a0bd-991c3f678238)
|
|
82
|
+
2026-09-04T04:12:24.855Z INFO sender marker 565726 -> 20260904T131022-ac38b6 (event=SessionStart, source=resume)
|
|
83
|
+
2026-09-04T04:12:24.855Z INFO armed watch /home/junghan/.pi/agent/meta-mailbox/20260904T131022-ac38b6/inbox.signal
|
|
84
|
+
2026-09-04T04:12:24.855Z INFO receiver marker 20260904T131022-ac38b6 owner=565726 arm=SessionStart source=resume
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**S3 — `claude --resume <id>` (argv).** Same shape as S2:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
2026-09-04T04:14:53.710Z INFO attach record 20260904T131022-ac38b6.meta.json (event=SessionStart, source=resume, native=bbba9cbe-f11c-4b67-a0bd-991c3f678238)
|
|
91
|
+
2026-09-04T04:14:53.711Z INFO sender marker 568726 -> 20260904T131022-ac38b6 (event=SessionStart, source=resume)
|
|
92
|
+
2026-09-04T04:14:53.711Z INFO armed watch /home/junghan/.pi/agent/meta-mailbox/20260904T131022-ac38b6/inbox.signal
|
|
93
|
+
2026-09-04T04:14:53.712Z INFO receiver marker 20260904T131022-ac38b6 owner=568726 arm=SessionStart source=resume
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**S4 — THE cell: bare `claude`, then `/resume`.** This is #101 reproduced deliberately, and the
|
|
97
|
+
repair firing on it. One pid (566479), two gardens, 33 seconds apart:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
2026-09-04T04:13:04.371Z INFO create record 20260904T131304-f85f0e.meta.json (event=SessionStart, source=startup, native=aa49d155-8416-41a5-9d59-52d747b428d7)
|
|
101
|
+
2026-09-04T04:13:04.372Z INFO sender marker 566479 -> 20260904T131304-f85f0e (event=SessionStart, source=startup)
|
|
102
|
+
2026-09-04T04:13:04.373Z INFO armed watch /home/junghan/.pi/agent/meta-mailbox/20260904T131304-f85f0e/inbox.signal
|
|
103
|
+
2026-09-04T04:13:04.373Z INFO receiver marker 20260904T131304-f85f0e owner=566479 arm=SessionStart source=startup
|
|
104
|
+
2026-09-04T04:13:37.966Z INFO attach record 20260904T131022-ac38b6.meta.json (event=SessionStart, source=resume, native=bbba9cbe-f11c-4b67-a0bd-991c3f678238)
|
|
105
|
+
2026-09-04T04:13:37.967Z INFO retired receiver marker 20260904T131304-f85f0e — owner pid 566479 switched to 20260904T131022-ac38b6 (event=SessionStart, source=resume)
|
|
106
|
+
2026-09-04T04:13:37.968Z INFO sender marker 566479 -> 20260904T131022-ac38b6 (event=SessionStart, source=resume)
|
|
107
|
+
2026-09-04T04:13:37.969Z INFO armed watch /home/junghan/.pi/agent/meta-mailbox/20260904T131022-ac38b6/inbox.signal
|
|
108
|
+
2026-09-04T04:13:37.970Z INFO receiver marker 20260904T131022-ac38b6 owner=566479 arm=SessionStart source=resume
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
State immediately after, read from the real store (read-only):
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
- 20260904T131022-ac38b6 backend=claude-code liveness=unsupported receiver=active transcript=exists cwd=…/scratchpad/lab
|
|
115
|
+
- 20260904T131304-f85f0e backend=claude-code liveness=unsupported receiver=none transcript=absent cwd=…/scratchpad/lab
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Both records survive. `meta-receivers/` holds only `20260904T131022-ac38b6.json`, and
|
|
119
|
+
`meta-senders/claude-code/566479.json` names `20260904T131022-ac38b6`. Before the repair the
|
|
120
|
+
abandoned garden would have read `receiver=active` and accepted mail — that is the #101 defect,
|
|
121
|
+
and this is the same shape as the field case (`ac7a1a`, still on this host as evidence).
|
|
122
|
+
|
|
123
|
+
**S5 — `/clear`.** Same switch shape, `source=clear`, and the retirement fires again:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
2026-09-04T04:14:18.703Z INFO create record 20260904T131418-31129e.meta.json (event=SessionStart, source=clear, native=eac3c9ae-2873-454b-90ca-11b7ee24b508)
|
|
127
|
+
2026-09-04T04:14:18.705Z INFO retired receiver marker 20260904T131022-ac38b6 — owner pid 566479 switched to 20260904T131418-31129e (event=SessionStart, source=clear)
|
|
128
|
+
2026-09-04T04:14:18.706Z INFO sender marker 566479 -> 20260904T131418-31129e (event=SessionStart, source=clear)
|
|
129
|
+
2026-09-04T04:14:18.706Z INFO armed watch /home/junghan/.pi/agent/meta-mailbox/20260904T131418-31129e/inbox.signal
|
|
130
|
+
2026-09-04T04:14:18.706Z INFO receiver marker 20260904T131418-31129e owner=566479 arm=SessionStart source=clear
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**S6a — manual `/compact`.** SAME native id, SAME garden, no retirement:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
2026-09-04T04:15:29.240Z INFO attach record 20260904T131022-ac38b6.meta.json (event=SessionStart, source=compact, native=bbba9cbe-f11c-4b67-a0bd-991c3f678238)
|
|
137
|
+
2026-09-04T04:15:29.242Z INFO sender marker 568726 -> 20260904T131022-ac38b6 (event=SessionStart, source=compact)
|
|
138
|
+
2026-09-04T04:15:29.242Z INFO armed watch /home/junghan/.pi/agent/meta-mailbox/20260904T131022-ac38b6/inbox.signal
|
|
139
|
+
2026-09-04T04:15:29.243Z INFO receiver marker 20260904T131022-ac38b6 owner=568726 arm=SessionStart source=compact
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**S6b — automatic compaction.** Reached by having the session read large repo files until the
|
|
143
|
+
context filled: 60% → 80% → 91% → 98% (196.5K/200K of a 200K window), then one more large read
|
|
144
|
+
tipped it. Byte-identical shape to S6a; context fell to 72.1K afterwards:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
2026-09-04T04:29:06.583Z INFO attach record 20260904T131022-ac38b6.meta.json (event=SessionStart, source=compact, native=bbba9cbe-f11c-4b67-a0bd-991c3f678238)
|
|
148
|
+
2026-09-04T04:29:06.585Z INFO sender marker 568726 -> 20260904T131022-ac38b6 (event=SessionStart, source=compact)
|
|
149
|
+
2026-09-04T04:29:06.586Z INFO armed watch /home/junghan/.pi/agent/meta-mailbox/20260904T131022-ac38b6/inbox.signal
|
|
150
|
+
2026-09-04T04:29:06.587Z INFO receiver marker 20260904T131022-ac38b6 owner=568726 arm=SessionStart source=compact
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Manual and automatic compaction are the same event to this hook. Note it needs ~98% of the window
|
|
154
|
+
before it fires, and that a session under a smaller model or a shorter window will reach it sooner
|
|
155
|
+
— the threshold is the vendor's, not ours.
|
|
156
|
+
|
|
157
|
+
## How to re-run
|
|
158
|
+
|
|
159
|
+
`tmux new-session -d -s lab -c <scratch> 'claude --model haiku'`, drive it with `send-keys` (send
|
|
160
|
+
the text and the `Enter` as SEPARATE calls — a combined one leaves the prompt typed but
|
|
161
|
+
unsubmitted), and read the delta of `~/.pi/agent/meta-bridge-hook.log` between cells. Nothing else
|
|
162
|
+
is needed: the hook log is the instrument. A cell costs a few seconds of a cheap model, except
|
|
163
|
+
S6b, which costs one full context window.
|
|
164
|
+
|
|
165
|
+
## Boundary
|
|
166
|
+
|
|
167
|
+
One host (oracle), one vendor version (2.1.260), one model (Haiku 4.5), one operator's config.
|
|
168
|
+
The cells say what this Claude does here; they do not certify another host, an older Claude, or
|
|
169
|
+
a different terminal. The repair does not rest on them — it rests on the sender marker — which is
|
|
170
|
+
why a refutation here (S2/S3) corrected the prose and left the code alone.
|
|
@@ -51,7 +51,11 @@ import * as os from "node:os";
|
|
|
51
51
|
import * as path from "node:path";
|
|
52
52
|
import * as readline from "node:readline";
|
|
53
53
|
import { fileURLToPath } from "node:url";
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
upsertMetaSession,
|
|
56
|
+
writeMetaReceiverMarker,
|
|
57
|
+
writeMetaSenderMarker,
|
|
58
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
55
59
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
56
60
|
import { skipLive } from "./lib/live-skip.ts";
|
|
57
61
|
import { PI_BOOT_TIMEOUT_MS, waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
@@ -210,7 +214,8 @@ async function main(): Promise<void> {
|
|
|
210
214
|
const sessionsDir = path.join(world, "sessions");
|
|
211
215
|
const mailboxDir = path.join(world, "mailbox");
|
|
212
216
|
const receiversDir = path.join(world, "receivers");
|
|
213
|
-
|
|
217
|
+
const sendersDir = path.join(world, "senders");
|
|
218
|
+
for (const d of [sessionsDir, mailboxDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
|
|
214
219
|
|
|
215
220
|
const nonce = `CORTEXV2-${crypto.randomBytes(5).toString("hex").toUpperCase()}`;
|
|
216
221
|
|
|
@@ -224,6 +229,12 @@ async function main(): Promise<void> {
|
|
|
224
229
|
dir: sessionsDir,
|
|
225
230
|
});
|
|
226
231
|
const receiverGid = receiver.record.gardenId;
|
|
232
|
+
// The receiver marker alone is no longer the whole fixture (#101): a `claude-code-cli` watch
|
|
233
|
+
// owner is deliverable only while ITS OWN sender marker still names the garden it is armed
|
|
234
|
+
// for — that join is what refuses a session which switched away in place. A receiver marker
|
|
235
|
+
// with no sender marker beside it models a state the product now treats as retired, so this
|
|
236
|
+
// smoke would fail on its own fixture instead of on the rail it exists to prove. Same shape
|
|
237
|
+
// as smoke-mux-lifecycle-live / smoke-omp-fresh-live, which already seeded both.
|
|
227
238
|
writeMetaReceiverMarker({
|
|
228
239
|
gardenId: receiverGid,
|
|
229
240
|
backend: "claude-code",
|
|
@@ -232,6 +243,14 @@ async function main(): Promise<void> {
|
|
|
232
243
|
armProvenance: "session-start",
|
|
233
244
|
receiversDir,
|
|
234
245
|
});
|
|
246
|
+
writeMetaSenderMarker({
|
|
247
|
+
backend: "claude-code",
|
|
248
|
+
gardenId: receiverGid,
|
|
249
|
+
nativeSessionId: receiver.record.nativeSessionId,
|
|
250
|
+
cwd: world,
|
|
251
|
+
ownerPid: process.pid,
|
|
252
|
+
sendersDir,
|
|
253
|
+
});
|
|
235
254
|
console.error(`[smoke-acp-cortex-live] peer: ${receiverGid} (armed mailbox receiver)`);
|
|
236
255
|
|
|
237
256
|
let senderGid = "";
|
|
@@ -252,6 +271,7 @@ async function main(): Promise<void> {
|
|
|
252
271
|
ENTWURF_META_SESSIONS_DIR: sessionsDir,
|
|
253
272
|
ENTWURF_META_MAILBOX_DIR: mailboxDir,
|
|
254
273
|
ENTWURF_META_RECEIVERS_DIR: receiversDir,
|
|
274
|
+
ENTWURF_META_SENDERS_DIR: sendersDir,
|
|
255
275
|
},
|
|
256
276
|
},
|
|
257
277
|
);
|
|
@@ -79,7 +79,11 @@ import * as os from "node:os";
|
|
|
79
79
|
import * as path from "node:path";
|
|
80
80
|
import * as readline from "node:readline";
|
|
81
81
|
import { fileURLToPath } from "node:url";
|
|
82
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
upsertMetaSession,
|
|
84
|
+
writeMetaReceiverMarker,
|
|
85
|
+
writeMetaSenderMarker,
|
|
86
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
83
87
|
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
84
88
|
import { skipLive } from "./lib/live-skip.ts";
|
|
85
89
|
import { PI_BOOT_TIMEOUT_MS, waitForPiRecord } from "./lib/pi-record-discovery.ts";
|
|
@@ -210,7 +214,8 @@ async function main(): Promise<void> {
|
|
|
210
214
|
const sessionsDir = path.join(world, "sessions");
|
|
211
215
|
const mailboxDir = path.join(world, "mailbox");
|
|
212
216
|
const receiversDir = path.join(world, "receivers");
|
|
213
|
-
|
|
217
|
+
const sendersDir = path.join(world, "senders");
|
|
218
|
+
for (const d of [sessionsDir, mailboxDir, receiversDir, sendersDir]) await fsp.mkdir(d, { recursive: true });
|
|
214
219
|
|
|
215
220
|
const nonce = `ACPV2-${crypto.randomBytes(5).toString("hex").toUpperCase()}`;
|
|
216
221
|
|
|
@@ -225,6 +230,13 @@ async function main(): Promise<void> {
|
|
|
225
230
|
});
|
|
226
231
|
const receiverGid = receiver.record.gardenId;
|
|
227
232
|
// armed = a presence marker owned by THIS (live) process → deliverable.
|
|
233
|
+
//
|
|
234
|
+
// The receiver marker alone is no longer the whole fixture (#101): a `claude-code-cli` watch
|
|
235
|
+
// owner is deliverable only while ITS OWN sender marker still names the garden it is armed
|
|
236
|
+
// for — that join is what refuses a session which switched away in place. A receiver marker
|
|
237
|
+
// with no sender marker beside it models a state the product now treats as retired, so this
|
|
238
|
+
// smoke would fail on its own fixture instead of on the rail it exists to prove. Same shape
|
|
239
|
+
// as smoke-mux-lifecycle-live / smoke-omp-fresh-live, which already seeded both.
|
|
228
240
|
writeMetaReceiverMarker({
|
|
229
241
|
gardenId: receiverGid,
|
|
230
242
|
backend: "claude-code",
|
|
@@ -233,6 +245,14 @@ async function main(): Promise<void> {
|
|
|
233
245
|
armProvenance: "session-start",
|
|
234
246
|
receiversDir,
|
|
235
247
|
});
|
|
248
|
+
writeMetaSenderMarker({
|
|
249
|
+
backend: "claude-code",
|
|
250
|
+
gardenId: receiverGid,
|
|
251
|
+
nativeSessionId: receiver.record.nativeSessionId,
|
|
252
|
+
cwd: world,
|
|
253
|
+
ownerPid: process.pid,
|
|
254
|
+
sendersDir,
|
|
255
|
+
});
|
|
236
256
|
console.error(`[smoke-acp-v2-send-live] peer: ${receiverGid} (armed mailbox receiver)`);
|
|
237
257
|
|
|
238
258
|
let senderGid = "";
|
|
@@ -252,6 +272,7 @@ async function main(): Promise<void> {
|
|
|
252
272
|
ENTWURF_META_SESSIONS_DIR: sessionsDir,
|
|
253
273
|
ENTWURF_META_MAILBOX_DIR: mailboxDir,
|
|
254
274
|
ENTWURF_META_RECEIVERS_DIR: receiversDir,
|
|
275
|
+
ENTWURF_META_SENDERS_DIR: sendersDir,
|
|
255
276
|
},
|
|
256
277
|
},
|
|
257
278
|
);
|