@junghanacs/entwurf 0.20.1 → 0.22.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 +85 -190
- package/BASELINE.md +6 -3
- package/CHANGELOG.md +473 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +332 -60
- package/README.md +100 -22
- package/VERIFY.md +93 -7
- package/docs/acp-backend-rail.md +0 -1
- package/docs/external-mcp-host.md +64 -33
- package/docs/setup-clean-host.md +151 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +154 -116
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
- package/package.json +13 -4
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +233 -29
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +342 -28
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +245 -12
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +8 -3
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-mux-launch-tmux.ts +345 -4
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +653 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +93 -10
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +75 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +700 -0
- package/scripts/smoke-codex-fresh-live.ts +1426 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +125 -10
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-codex-sender-identity — deterministic gate for the codex REQUEST-SCOPED sender rail
|
|
3
|
+
* (#95 step 6, the resolver half).
|
|
4
|
+
*
|
|
5
|
+
* WHAT IS DIFFERENT ABOUT THIS RAIL, and therefore what this gate has to pin. Every other
|
|
6
|
+
* native citizen is identified by a pid marker its hook wrote: one process, one identity, read
|
|
7
|
+
* once at bridge startup. Codex cannot be: in the delivery-capable (app-server-attached) launch
|
|
8
|
+
* mode every hook and every MCP child of N live threads shares ONE parent pid — the app-server
|
|
9
|
+
* — so a pid marker would be one marker for N citizens, which the store's `nativeSessionId`
|
|
10
|
+
* uniqueness forbids (measured 2026-09-08, `scripts/raw-codex-measure/README.md` S1b-C). The
|
|
11
|
+
* caller instead names itself on EVERY `tools/call`: `_meta.threadId` plus an
|
|
12
|
+
* `x-codex-turn-metadata.{session_id,thread_id}` block, and that string is byte-identical to
|
|
13
|
+
* the hook's `session_id` (S1b-D / D3). So identity here is per-REQUEST, and the whole risk
|
|
14
|
+
* moves with it: the wire is now an identity carrier, and a wire field is written by the caller.
|
|
15
|
+
*
|
|
16
|
+
* The three properties this gate exists to hold, in the order they can fail:
|
|
17
|
+
*
|
|
18
|
+
* 1. ADMISSION IS EXPLICIT. Provenance is an argument, never an environment read — in
|
|
19
|
+
* particular never `ENTWURF_BRIDGE_EXTERNAL_AGENT_ID`, which is omp's root-policy label
|
|
20
|
+
* with its own policy attached. A process that merely looks like codex is not codex.
|
|
21
|
+
* 2. METADATA IS A SELECTOR, NOT AN IDENTITY. The `_meta` block also carries `model` and a
|
|
22
|
+
* `workspaces` map. Every field of the answer — gardenId, cwd, model, backend — comes from
|
|
23
|
+
* the RECORD; a caller that could relabel its own citizen by asking would have turned a
|
|
24
|
+
* read into a write. This gate poisons all of them and demands the record's values.
|
|
25
|
+
* 3. A HALF CLAIM IS LOUD, AN ABSENT CLAIM IS `null`. The two ids are built by two different
|
|
26
|
+
* vendor call sites, and that redundancy is the only reason a wire field can select a
|
|
27
|
+
* record at all. So a claim that is present but broken (missing corroborator, three ids
|
|
28
|
+
* disagreeing) is a refusal WITH A CAUSE, while a call that never claimed identity is
|
|
29
|
+
* plain `null` and the bridge's default anonymous refusal owns it.
|
|
30
|
+
*
|
|
31
|
+
* The store lookup is the strict store-wide listing, so a rotten store anywhere refuses a
|
|
32
|
+
* healthy thread rather than answering from a snapshot the install doctor calls uncertifiable.
|
|
33
|
+
* Cells 5 and 6 pin that, including the case the strictness exists for: a DUPLICATE of this
|
|
34
|
+
* very thread's id, which a lenient listing would drop and then report as "no record".
|
|
35
|
+
*
|
|
36
|
+
* Everything here is deterministic and store-isolated: no codex binary, no app-server, no turn.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import assert from "node:assert/strict";
|
|
40
|
+
import * as fs from "node:fs";
|
|
41
|
+
import * as os from "node:os";
|
|
42
|
+
import * as path from "node:path";
|
|
43
|
+
import {
|
|
44
|
+
CODEX_BRIDGE_PROVENANCE_LABEL,
|
|
45
|
+
CODEX_MCP_CLIENT_NAME,
|
|
46
|
+
META_SENDER_BACKENDS,
|
|
47
|
+
readCodexRequestThreadId,
|
|
48
|
+
reconcileSenderIdentityClaims,
|
|
49
|
+
resolveCodexRequestSenderIdentity,
|
|
50
|
+
type SenderIdentityClaim,
|
|
51
|
+
} from "../pi-extensions/lib/meta-sender-identity.ts";
|
|
52
|
+
import {
|
|
53
|
+
type MetaIdentity,
|
|
54
|
+
metaRecordFilename,
|
|
55
|
+
mintMetaIdentity,
|
|
56
|
+
serializeMetaIdentity,
|
|
57
|
+
upsertMetaSession,
|
|
58
|
+
} from "../pi-extensions/lib/meta-session.ts";
|
|
59
|
+
|
|
60
|
+
let passed = 0;
|
|
61
|
+
function ok(label: string, cond: boolean): void {
|
|
62
|
+
assert.ok(cond, label);
|
|
63
|
+
console.log(` ok ${label}`);
|
|
64
|
+
passed++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A refusal is only useful if it names its own cause, so both the class NAME and a phrase from
|
|
68
|
+
* the message are pinned — a resolver that threw the wrong error, or degraded to `null`, fails
|
|
69
|
+
* here rather than looking green because "something threw". */
|
|
70
|
+
function refuses(label: string, fn: () => unknown, name: string, pattern: RegExp): void {
|
|
71
|
+
let caught: unknown;
|
|
72
|
+
try {
|
|
73
|
+
fn();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
caught = err;
|
|
76
|
+
}
|
|
77
|
+
const err = caught instanceof Error ? caught : null;
|
|
78
|
+
assert.ok(err, `${label} — expected a throw, got none`);
|
|
79
|
+
assert.equal(err.name, name, `${label} — wrong error class (${err.message})`);
|
|
80
|
+
assert.match(err.message, pattern, label);
|
|
81
|
+
console.log(` ok ${label}`);
|
|
82
|
+
passed++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Isolated store roots — this gate never reads or writes the operator's real garden. The env
|
|
86
|
+
// SSOT is pointed at the temp root as well, so even a call that takes the DEFAULT store cannot
|
|
87
|
+
// reach the real one if a future edit forgets to pass `sessionsDir`.
|
|
88
|
+
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-codex-sender-identity-"));
|
|
89
|
+
const SESSIONS_DIR = path.join(ROOT, "meta-sessions");
|
|
90
|
+
process.env.ENTWURF_META_SESSIONS_DIR = SESSIONS_DIR;
|
|
91
|
+
|
|
92
|
+
// Synthetic ids, deliberately not the measured live thread ids: a gate that wrote real-looking
|
|
93
|
+
// uuid7 threads into a log or a record would be planting evidence a later reader could mistake
|
|
94
|
+
// for a live measurement.
|
|
95
|
+
const THREAD = "00000000-gate-codex-thread-aaaa";
|
|
96
|
+
const OTHER_THREAD = "00000000-gate-codex-thread-bbbb";
|
|
97
|
+
const RECORD_CWD = "/gate/codex/record-cwd";
|
|
98
|
+
const RECORD_MODEL = "gate-model-from-record";
|
|
99
|
+
|
|
100
|
+
const CLIENT_INFO = { name: CODEX_MCP_CLIENT_NAME, title: "Codex", version: "0.153.4" };
|
|
101
|
+
|
|
102
|
+
/** A well-formed codex tool-call `_meta`, in the measured shape (S1b-D). `patch`/`turnPatch`
|
|
103
|
+
* bend exactly one field per case so a failing assertion names one deviation. */
|
|
104
|
+
function callMeta(
|
|
105
|
+
threadId: string,
|
|
106
|
+
patch: Record<string, unknown> = {},
|
|
107
|
+
turnPatch: Record<string, unknown> = {},
|
|
108
|
+
): Record<string, unknown> {
|
|
109
|
+
return {
|
|
110
|
+
callId: "exec-gate-0000",
|
|
111
|
+
"x-codex-turn-metadata": {
|
|
112
|
+
session_id: threadId,
|
|
113
|
+
thread_id: threadId,
|
|
114
|
+
turn_id: "00000000-gate-codex-turn-0001",
|
|
115
|
+
turn_started_at_unix_ms: 1788877797276,
|
|
116
|
+
thread_source: "user",
|
|
117
|
+
sandbox: "seccomp",
|
|
118
|
+
...turnPatch,
|
|
119
|
+
},
|
|
120
|
+
threadId,
|
|
121
|
+
itemId: "ctc_gate",
|
|
122
|
+
progressToken: 1,
|
|
123
|
+
...patch,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface ResolveOverrides {
|
|
128
|
+
provenance?: unknown;
|
|
129
|
+
clientInfo?: unknown;
|
|
130
|
+
sessionsDir?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolve(requestMeta: unknown, over: ResolveOverrides = {}) {
|
|
134
|
+
return resolveCodexRequestSenderIdentity({
|
|
135
|
+
provenance: CODEX_BRIDGE_PROVENANCE_LABEL,
|
|
136
|
+
clientInfo: CLIENT_INFO,
|
|
137
|
+
sessionsDir: SESSIONS_DIR,
|
|
138
|
+
requestMeta,
|
|
139
|
+
...over,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Write a record BESIDE the store writer, which is the only way to build the defects the
|
|
144
|
+
* writer refuses to create (a duplicate native id, a corrupt body). Never used for a healthy
|
|
145
|
+
* record — those go through `upsertMetaSession`, so the happy path is the production path. */
|
|
146
|
+
function plantRecord(dir: string, identity: MetaIdentity): void {
|
|
147
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
148
|
+
fs.writeFileSync(path.join(dir, metaRecordFilename(identity)), serializeMetaIdentity(identity));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// ── CELL 1 — the happy path, through the production writer ────────────────
|
|
153
|
+
const born = upsertMetaSession({
|
|
154
|
+
input: { backend: "codex", nativeSessionId: THREAD, cwd: RECORD_CWD, model: RECORD_MODEL },
|
|
155
|
+
dir: SESSIONS_DIR,
|
|
156
|
+
});
|
|
157
|
+
{
|
|
158
|
+
ok(
|
|
159
|
+
"birth mints a codex citizen whose nativeSessionId IS the threadId (no mapping layer)",
|
|
160
|
+
born.record.nativeSessionId === THREAD,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const sender = resolve(callMeta(THREAD));
|
|
164
|
+
ok("a well-formed codex tool call resolves to exactly one sender", sender !== null);
|
|
165
|
+
ok("the resolved citizen is the record the birth minted", sender?.identity.gardenId === born.record.gardenId);
|
|
166
|
+
ok("the resolved citizen carries backend=codex", sender?.identity.backend === "codex");
|
|
167
|
+
ok(
|
|
168
|
+
"the selector is echoed and equals the record's nativeSessionId",
|
|
169
|
+
sender?.threadId === THREAD && sender?.identity.nativeSessionId === THREAD,
|
|
170
|
+
);
|
|
171
|
+
ok("the answer states the provenance that admitted the rail", sender?.provenance === CODEX_BRIDGE_PROVENANCE_LABEL);
|
|
172
|
+
|
|
173
|
+
// The pure half is the wire contract: it must answer without any store at all, which is
|
|
174
|
+
// what lets the bridge validate a claim before it decides to touch the filesystem.
|
|
175
|
+
ok(
|
|
176
|
+
"the metadata reader is pure — it yields the threadId with no store in play",
|
|
177
|
+
readCodexRequestThreadId({
|
|
178
|
+
provenance: CODEX_BRIDGE_PROVENANCE_LABEL,
|
|
179
|
+
clientInfo: CLIENT_INFO,
|
|
180
|
+
requestMeta: callMeta(THREAD),
|
|
181
|
+
}) === THREAD,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── CELL 2 — the record is the ONLY source of identity fields ─────────────
|
|
186
|
+
// Every field below is one a caller could plausibly want to steer: its cwd (workspace
|
|
187
|
+
// relabelling), its model (grade inflation), its garden id (impersonation). All three are
|
|
188
|
+
// present in the real `_meta` shape, and all three are ignored.
|
|
189
|
+
{
|
|
190
|
+
const poisoned = callMeta(
|
|
191
|
+
THREAD,
|
|
192
|
+
{
|
|
193
|
+
cwd: "/gate/poison/from-the-wire",
|
|
194
|
+
model: "poison-model-from-the-wire",
|
|
195
|
+
gardenId: "20200101T000000-abcdef",
|
|
196
|
+
backend: "pi",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
model: "poison-model-from-turn-metadata",
|
|
200
|
+
workspaces: { "/gate/poison/workspace": { latest_git_commit_hash: "deadbeef" } },
|
|
201
|
+
cwd: "/gate/poison/turn-cwd",
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
const sender = resolve(poisoned);
|
|
205
|
+
ok("poisoned metadata still resolves (it is a selector, not a veto)", sender !== null);
|
|
206
|
+
ok("cwd comes from the record, never from `_meta` or the workspaces map", sender?.identity.cwd === RECORD_CWD);
|
|
207
|
+
ok("model comes from the record, never from the turn metadata", sender?.identity.model === RECORD_MODEL);
|
|
208
|
+
ok(
|
|
209
|
+
"gardenId comes from the record — a wire-supplied garden id cannot impersonate",
|
|
210
|
+
sender?.identity.gardenId === born.record.gardenId,
|
|
211
|
+
);
|
|
212
|
+
ok(
|
|
213
|
+
"backend comes from the record — a wire-supplied backend cannot switch rails",
|
|
214
|
+
sender?.identity.backend === "codex",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
ok(
|
|
218
|
+
"no provenance → null (admission is explicit, never inferred)",
|
|
219
|
+
resolve(callMeta(THREAD), { provenance: undefined }) === null,
|
|
220
|
+
);
|
|
221
|
+
ok(
|
|
222
|
+
"a foreign provenance namespace → null (`external-mcp/codex` is not this label)",
|
|
223
|
+
resolve(callMeta(THREAD), { provenance: "external-mcp/codex" }) === null,
|
|
224
|
+
);
|
|
225
|
+
ok("another host's provenance → null", resolve(callMeta(THREAD), { provenance: "external-mcp/omp" }) === null);
|
|
226
|
+
ok("a truthy non-string provenance → null", resolve(callMeta(THREAD), { provenance: true }) === null);
|
|
227
|
+
|
|
228
|
+
// The env label is omp's root-policy atom and carries policy of its own. Setting it here
|
|
229
|
+
// proves the codex rail cannot be opened by an environment a native host may hand down.
|
|
230
|
+
process.env.ENTWURF_BRIDGE_EXTERNAL_AGENT_ID = CODEX_BRIDGE_PROVENANCE_LABEL;
|
|
231
|
+
process.env.ENTWURF_BRIDGE_NATIVE_HOST = CODEX_BRIDGE_PROVENANCE_LABEL;
|
|
232
|
+
ok(
|
|
233
|
+
"ENTWURF_BRIDGE_EXTERNAL_AGENT_ID / ENTWURF_BRIDGE_NATIVE_HOST in the env open NOTHING — the resolver reads no env",
|
|
234
|
+
resolve(callMeta(THREAD), { provenance: undefined }) === null,
|
|
235
|
+
);
|
|
236
|
+
delete process.env.ENTWURF_BRIDGE_EXTERNAL_AGENT_ID;
|
|
237
|
+
delete process.env.ENTWURF_BRIDGE_NATIVE_HOST;
|
|
238
|
+
|
|
239
|
+
ok("no clientInfo → null", resolve(callMeta(THREAD), { clientInfo: undefined }) === null);
|
|
240
|
+
ok(
|
|
241
|
+
"a non-codex client name → null (host kind is a gate, and it is checked)",
|
|
242
|
+
resolve(callMeta(THREAD), { clientInfo: { name: "smoke", version: "0" } }) === null,
|
|
243
|
+
);
|
|
244
|
+
ok(
|
|
245
|
+
"the client name as a bare string, not an object → null",
|
|
246
|
+
resolve(callMeta(THREAD), { clientInfo: CODEX_MCP_CLIENT_NAME }) === null,
|
|
247
|
+
);
|
|
248
|
+
ok(
|
|
249
|
+
"clientInfo as an array → null (an array is not a JSON object)",
|
|
250
|
+
resolve(callMeta(THREAD), { clientInfo: [CLIENT_INFO] }) === null,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
ok("no `_meta` at all → null (initialize / tools-list shape)", resolve(undefined) === null);
|
|
254
|
+
ok("`_meta` null → null", resolve(null) === null);
|
|
255
|
+
ok("`_meta` a string → null", resolve("threadId") === null);
|
|
256
|
+
ok("`_meta` an array → null", resolve([callMeta(THREAD)]) === null);
|
|
257
|
+
ok(
|
|
258
|
+
"`_meta` present with NO threadId member → null (a call that never claimed identity)",
|
|
259
|
+
resolve({ callId: "exec-gate-0000", progressToken: 1 }) === null,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
ok(
|
|
263
|
+
"a well-formed claim for a thread no record holds → null (the pre-birth window)",
|
|
264
|
+
resolve(callMeta(OTHER_THREAD)) === null,
|
|
265
|
+
);
|
|
266
|
+
ok(
|
|
267
|
+
"an absent store → null (a host with no generation is not a broken host)",
|
|
268
|
+
resolve(callMeta(THREAD), { sessionsDir: path.join(ROOT, "never-existed") }) === null,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// ── CELL 4 — a PRESENT claim that does not hold together is loud ──────────
|
|
272
|
+
// The line between cell 3 and cell 4 is the presence of `threadId`: absent is silence,
|
|
273
|
+
// present-but-unusable is a broken claim, and folding them would report "no identity" for a
|
|
274
|
+
// caller that named itself.
|
|
275
|
+
{
|
|
276
|
+
const bad =
|
|
277
|
+
(patch: Record<string, unknown>, turnPatch: Record<string, unknown> = {}) =>
|
|
278
|
+
() =>
|
|
279
|
+
resolve(callMeta(THREAD, patch, turnPatch));
|
|
280
|
+
|
|
281
|
+
refuses(
|
|
282
|
+
"threadId empty string → loud",
|
|
283
|
+
bad({ threadId: "" }),
|
|
284
|
+
"EntwurfCodexIdentityMetadataError",
|
|
285
|
+
/not a nonempty string/,
|
|
286
|
+
);
|
|
287
|
+
refuses(
|
|
288
|
+
"threadId a number → loud",
|
|
289
|
+
bad({ threadId: 42 }),
|
|
290
|
+
"EntwurfCodexIdentityMetadataError",
|
|
291
|
+
/not a nonempty string \(got number 42\)/,
|
|
292
|
+
);
|
|
293
|
+
refuses(
|
|
294
|
+
"threadId null → loud",
|
|
295
|
+
bad({ threadId: null }),
|
|
296
|
+
"EntwurfCodexIdentityMetadataError",
|
|
297
|
+
/not a nonempty string \(got null\)/,
|
|
298
|
+
);
|
|
299
|
+
refuses(
|
|
300
|
+
"the corroborating turn-metadata block absent → loud (one id alone is a half claim)",
|
|
301
|
+
bad({ "x-codex-turn-metadata": undefined }),
|
|
302
|
+
"EntwurfCodexIdentityMetadataError",
|
|
303
|
+
/x-codex-turn-metadata` is undefined, not an object/,
|
|
304
|
+
);
|
|
305
|
+
refuses(
|
|
306
|
+
"the turn-metadata block a string → loud",
|
|
307
|
+
bad({ "x-codex-turn-metadata": "session_id=…" }),
|
|
308
|
+
"EntwurfCodexIdentityMetadataError",
|
|
309
|
+
/not an object/,
|
|
310
|
+
);
|
|
311
|
+
refuses(
|
|
312
|
+
"session_id missing → loud (that field is the one the birth hook also sees)",
|
|
313
|
+
bad({}, { session_id: undefined }),
|
|
314
|
+
"EntwurfCodexIdentityMetadataError",
|
|
315
|
+
/session_id` is not a nonempty string/,
|
|
316
|
+
);
|
|
317
|
+
refuses(
|
|
318
|
+
"session_id a number → loud",
|
|
319
|
+
bad({}, { session_id: 7 }),
|
|
320
|
+
"EntwurfCodexIdentityMetadataError",
|
|
321
|
+
/session_id` is not a nonempty string/,
|
|
322
|
+
);
|
|
323
|
+
refuses(
|
|
324
|
+
"thread_id missing → loud",
|
|
325
|
+
bad({}, { thread_id: undefined }),
|
|
326
|
+
"EntwurfCodexIdentityMetadataError",
|
|
327
|
+
/thread_id` is not a nonempty string/,
|
|
328
|
+
);
|
|
329
|
+
refuses(
|
|
330
|
+
"an outer threadId that disagrees with the block → loud, never resolved from either",
|
|
331
|
+
bad({ threadId: OTHER_THREAD }),
|
|
332
|
+
"EntwurfCodexIdentityMetadataError",
|
|
333
|
+
/the three ids disagree/,
|
|
334
|
+
);
|
|
335
|
+
refuses(
|
|
336
|
+
"a block whose thread_id disagrees with its own session_id → loud",
|
|
337
|
+
bad({}, { thread_id: OTHER_THREAD }),
|
|
338
|
+
"EntwurfCodexIdentityMetadataError",
|
|
339
|
+
/the three ids disagree/,
|
|
340
|
+
);
|
|
341
|
+
// The sharpest case: the disagreeing id is one a record DOES hold, so a resolver that
|
|
342
|
+
// preferred either field would have found a citizen and sent under it.
|
|
343
|
+
{
|
|
344
|
+
const otherBorn = upsertMetaSession({
|
|
345
|
+
input: { backend: "codex", nativeSessionId: OTHER_THREAD, cwd: RECORD_CWD },
|
|
346
|
+
dir: SESSIONS_DIR,
|
|
347
|
+
});
|
|
348
|
+
ok("a second codex citizen exists for the disagreeing id", otherBorn.record.nativeSessionId === OTHER_THREAD);
|
|
349
|
+
refuses(
|
|
350
|
+
"disagreeing ids refuse even when BOTH name real citizens (no field wins by priority)",
|
|
351
|
+
bad({ threadId: OTHER_THREAD }),
|
|
352
|
+
"EntwurfCodexIdentityMetadataError",
|
|
353
|
+
/the three ids disagree/,
|
|
354
|
+
);
|
|
355
|
+
fs.rmSync(path.join(SESSIONS_DIR, metaRecordFilename(otherBorn.record)));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── CELL 5 — the threadId selects a record that is not a codex citizen ────
|
|
360
|
+
{
|
|
361
|
+
const foreignStore = path.join(ROOT, "store-foreign");
|
|
362
|
+
plantRecord(foreignStore, mintMetaIdentity({ backend: "claude-code", nativeSessionId: THREAD, cwd: RECORD_CWD }));
|
|
363
|
+
refuses(
|
|
364
|
+
"a threadId bound to another backend → loud (never `null`, never the foreign citizen)",
|
|
365
|
+
() => resolve(callMeta(THREAD), { sessionsDir: foreignStore }),
|
|
366
|
+
"EntwurfCodexIdentityBackendError",
|
|
367
|
+
/backend is "claude-code", not "codex"/,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// A pi citizen is the same defect with the backend that is NOT even a v2 native host —
|
|
371
|
+
// pinned because `pi` is in the citizen set but not in `META_BACKENDS`.
|
|
372
|
+
const piStore = path.join(ROOT, "store-pi");
|
|
373
|
+
plantRecord(piStore, mintMetaIdentity({ backend: "pi", nativeSessionId: THREAD, cwd: RECORD_CWD }));
|
|
374
|
+
refuses(
|
|
375
|
+
"a threadId bound to a pi citizen → loud on the same rule",
|
|
376
|
+
() => resolve(callMeta(THREAD), { sessionsDir: piStore }),
|
|
377
|
+
"EntwurfCodexIdentityBackendError",
|
|
378
|
+
/backend is "pi", not "codex"/,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── CELL 6 — the store lookup is the STRICT store-wide certification ──────
|
|
383
|
+
{
|
|
384
|
+
// (a) A duplicate of THIS thread's id. This is the case the strictness exists for: a
|
|
385
|
+
// lenient listing drops both rivals, and the resolver would then answer `null` — "no
|
|
386
|
+
// record for your thread" — for a store that in fact holds two.
|
|
387
|
+
const dupStore = path.join(ROOT, "store-duplicate");
|
|
388
|
+
plantRecord(
|
|
389
|
+
dupStore,
|
|
390
|
+
mintMetaIdentity(
|
|
391
|
+
{ backend: "codex", nativeSessionId: THREAD, cwd: RECORD_CWD },
|
|
392
|
+
new Date("2026-01-01T00:00:00Z"),
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
plantRecord(
|
|
396
|
+
dupStore,
|
|
397
|
+
mintMetaIdentity(
|
|
398
|
+
{ backend: "codex", nativeSessionId: THREAD, cwd: RECORD_CWD },
|
|
399
|
+
new Date("2026-02-02T00:00:00Z"),
|
|
400
|
+
),
|
|
401
|
+
);
|
|
402
|
+
ok("the duplicate store really holds two records", fs.readdirSync(dupStore).length === 2);
|
|
403
|
+
refuses(
|
|
404
|
+
"two records claiming this threadId → loud, never a `null` that reads as 'no record'",
|
|
405
|
+
() => resolve(callMeta(THREAD), { sessionsDir: dupStore }),
|
|
406
|
+
"MetaRecordError",
|
|
407
|
+
/duplicate nativeSessionId/,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// (b) A rotten record ELSEWHERE in the store. The codex thread's own record is healthy,
|
|
411
|
+
// and it still refuses: an uncertifiable store is not a store to take an address from.
|
|
412
|
+
const rottenStore = path.join(ROOT, "store-rotten");
|
|
413
|
+
plantRecord(rottenStore, mintMetaIdentity({ backend: "codex", nativeSessionId: THREAD, cwd: RECORD_CWD }));
|
|
414
|
+
fs.writeFileSync(path.join(rottenStore, "20200101T000000-abcdef.meta.json"), "{ not json");
|
|
415
|
+
refuses(
|
|
416
|
+
"a rotten record elsewhere in the store refuses a HEALTHY thread (store-wide strictness)",
|
|
417
|
+
() => resolve(callMeta(THREAD), { sessionsDir: rottenStore }),
|
|
418
|
+
"MetaRecordError",
|
|
419
|
+
/unreadable meta-record/,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
// (c) The store path is not a directory at all — the ENOTDIR shape that must never
|
|
423
|
+
// launder into "certified empty".
|
|
424
|
+
const notADir = path.join(ROOT, "store-is-a-file");
|
|
425
|
+
fs.writeFileSync(notADir, "not a store\n");
|
|
426
|
+
refuses(
|
|
427
|
+
"a non-directory store path → loud (an unreadable store is not an empty one)",
|
|
428
|
+
() => resolve(callMeta(THREAD), { sessionsDir: notADir }),
|
|
429
|
+
"MetaRecordError",
|
|
430
|
+
/failure to inspect the store/,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── CELL 7 — conflict-ready output for the later MCP integration ──────────
|
|
435
|
+
// The bridge will hold up to three claims at once (pi env, the pid marker, this request).
|
|
436
|
+
// Reconciliation is order-independent by construction: agreement returns one address with
|
|
437
|
+
// every rail that named it, disagreement refuses, and no rail wins by position.
|
|
438
|
+
{
|
|
439
|
+
const codexClaim: SenderIdentityClaim = { rail: "codex-request", id: born.record.gardenId };
|
|
440
|
+
const markerClaim: SenderIdentityClaim = { rail: "meta-sender-marker", id: born.record.gardenId };
|
|
441
|
+
const piClaim: SenderIdentityClaim = { rail: "pi-session", id: "20200101T000000-000001" };
|
|
442
|
+
|
|
443
|
+
ok(
|
|
444
|
+
"no claims → null (anonymous; the bridge's default refusal owns that)",
|
|
445
|
+
reconcileSenderIdentityClaims([]) === null,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const single = reconcileSenderIdentityClaims([codexClaim]);
|
|
449
|
+
ok(
|
|
450
|
+
"one claim → that address, naming its rail",
|
|
451
|
+
single?.id === born.record.gardenId && single?.rails.join(",") === "codex-request",
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const agreed = reconcileSenderIdentityClaims([markerClaim, codexClaim]);
|
|
455
|
+
const agreedReversed = reconcileSenderIdentityClaims([codexClaim, markerClaim]);
|
|
456
|
+
ok(
|
|
457
|
+
"two rails AGREEING is not a conflict — one address, both rails",
|
|
458
|
+
agreed?.id === born.record.gardenId && agreed?.rails.join(",") === "codex-request,meta-sender-marker",
|
|
459
|
+
);
|
|
460
|
+
ok(
|
|
461
|
+
"the answer does not depend on the order the claims were collected in",
|
|
462
|
+
JSON.stringify(agreed) === JSON.stringify(agreedReversed),
|
|
463
|
+
);
|
|
464
|
+
ok(
|
|
465
|
+
"a rail naming the same address twice is de-duplicated, not counted twice",
|
|
466
|
+
reconcileSenderIdentityClaims([codexClaim, codexClaim])?.rails.join(",") === "codex-request",
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
refuses(
|
|
470
|
+
"two rails naming DIFFERENT addresses → loud, naming both rails and both ids",
|
|
471
|
+
() => reconcileSenderIdentityClaims([piClaim, codexClaim]),
|
|
472
|
+
"EntwurfSenderIdentityConflictError",
|
|
473
|
+
/pi-session=20200101T000000-000001, codex-request=/,
|
|
474
|
+
);
|
|
475
|
+
refuses(
|
|
476
|
+
"two agreeing rails plus one dissenter still refuses (majority is not authority)",
|
|
477
|
+
() => reconcileSenderIdentityClaims([markerClaim, codexClaim, piClaim]),
|
|
478
|
+
"EntwurfSenderIdentityConflictError",
|
|
479
|
+
/conflicting sender identity/,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
ok(
|
|
483
|
+
"`codex` is NOT in META_SENDER_BACKENDS — codex birth writes no pid marker, so the marker scan must never look for one",
|
|
484
|
+
!(META_SENDER_BACKENDS as readonly string[]).includes("codex"),
|
|
485
|
+
);
|
|
486
|
+
ok(
|
|
487
|
+
"the provenance label is the codex host atom, not an `external-mcp/*` namespace",
|
|
488
|
+
CODEX_BRIDGE_PROVENANCE_LABEL === "codex",
|
|
489
|
+
);
|
|
490
|
+
ok("the client-kind gate pins the vendor literal", CODEX_MCP_CLIENT_NAME === "codex-mcp-client");
|
|
491
|
+
|
|
492
|
+
console.log(`\ncheck-codex-sender-identity: ${passed} checks passed`);
|
|
493
|
+
} finally {
|
|
494
|
+
fs.rmSync(ROOT, { recursive: true, force: true });
|
|
495
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-compaction-send-guard — deterministic oracle for #111.
|
|
3
|
+
*
|
|
4
|
+
* A compacting Pi citizen is neither idle nor an ordinary streaming turn. The
|
|
5
|
+
* control-socket receiver must refuse every send mode without calling
|
|
6
|
+
* `pi.sendMessage` and without emitting `delivered:true`. Event-armed compaction
|
|
7
|
+
* names itself `compacting`. Quiet unknown non-idle (`!idle && !hasAgentSignal`)
|
|
8
|
+
* is fail-closed as `busy` — `ctx.signal` is not `AgentSession.isStreaming`, so
|
|
9
|
+
* that cell is not a proven compact. The predicate takes no send mode.
|
|
10
|
+
*
|
|
11
|
+
* No model / auth / pi process.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import {
|
|
19
|
+
BUSY_SEND_REJECT,
|
|
20
|
+
COMPACTION_SEND_REJECT,
|
|
21
|
+
compactionSendReject,
|
|
22
|
+
createCompactionGuard,
|
|
23
|
+
noteCompactionBefore,
|
|
24
|
+
noteCompactionTerminal,
|
|
25
|
+
} from "../pi-extensions/lib/compaction-send-guard.ts";
|
|
26
|
+
|
|
27
|
+
let passed = 0;
|
|
28
|
+
function ok(label: string, cond: boolean): void {
|
|
29
|
+
assert.ok(cond, label);
|
|
30
|
+
console.log(` ok ${label}`);
|
|
31
|
+
passed++;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
35
|
+
const LEAF_SRC = readFileSync(path.join(REPO, "pi-extensions/lib/compaction-send-guard.ts"), "utf8");
|
|
36
|
+
const CONTROL_SRC = readFileSync(path.join(REPO, "pi-extensions/entwurf-control.ts"), "utf8");
|
|
37
|
+
|
|
38
|
+
function stripComments(src: string): string {
|
|
39
|
+
return src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const leafCode = stripComments(LEAF_SRC);
|
|
43
|
+
const controlCode = stripComments(CONTROL_SRC);
|
|
44
|
+
|
|
45
|
+
ok("event-armed token is compacting", COMPACTION_SEND_REJECT === "compacting");
|
|
46
|
+
ok("quiet-unknown token is busy, not compacting", BUSY_SEND_REJECT === "busy");
|
|
47
|
+
|
|
48
|
+
ok(
|
|
49
|
+
"[QK:COMPACT-SEND-NO-MODE] the predicate takes no send mode — omitted/follow_up/steer cannot diverge",
|
|
50
|
+
!/mode/.test(leafCode) &&
|
|
51
|
+
/compactionSendReject\s*\(\s*guard:\s*CompactionGuard,\s*facts:\s*CompactionSendFacts\s*\)/.test(LEAF_SRC),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
const g = createCompactionGuard();
|
|
56
|
+
noteCompactionBefore(g);
|
|
57
|
+
ok(
|
|
58
|
+
"[QK:COMPACT-SEND-REJECT-ARMED] armed compacting-during-stream rejects as compacting",
|
|
59
|
+
compactionSendReject(g, { idle: false, hasAgentSignal: true }) === COMPACTION_SEND_REJECT,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
const g = createCompactionGuard();
|
|
65
|
+
ok(
|
|
66
|
+
"[QK:COMPACT-SEND-REJECT-QUIET] quiet unknown non-idle rejects as busy (start race / post-run / branch summary)",
|
|
67
|
+
compactionSendReject(g, { idle: false, hasAgentSignal: false }) === BUSY_SEND_REJECT,
|
|
68
|
+
);
|
|
69
|
+
ok(
|
|
70
|
+
"quiet unknown is not named compacting",
|
|
71
|
+
compactionSendReject(g, { idle: false, hasAgentSignal: false }) !== COMPACTION_SEND_REJECT,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
const g = createCompactionGuard();
|
|
77
|
+
noteCompactionBefore(g);
|
|
78
|
+
ok(
|
|
79
|
+
"[QK:COMPACT-SEND-ADMIT-IDLE] idle admits even when armed (missed session_compact recovery)",
|
|
80
|
+
compactionSendReject(g, { idle: true, hasAgentSignal: false }) === null,
|
|
81
|
+
);
|
|
82
|
+
ok("idle recovery disarms so a later ordinary stream is not stuck refused", g.armed === false);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
const g = createCompactionGuard();
|
|
87
|
+
ok(
|
|
88
|
+
"[QK:COMPACT-SEND-ADMIT-STREAM] live agent run (not armed, signal present) keeps steer/followUp",
|
|
89
|
+
compactionSendReject(g, { idle: false, hasAgentSignal: true }) === null,
|
|
90
|
+
);
|
|
91
|
+
noteCompactionBefore(g);
|
|
92
|
+
noteCompactionTerminal(g);
|
|
93
|
+
ok(
|
|
94
|
+
"session_compact / session_compact_failed disarms, so the next live run admits",
|
|
95
|
+
compactionSendReject(g, { idle: false, hasAgentSignal: true }) === null,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
ok("fresh guard is not armed", createCompactionGuard().armed === false);
|
|
100
|
+
ok(
|
|
101
|
+
"idle + not-armed admits (the preserved idle path)",
|
|
102
|
+
compactionSendReject(createCompactionGuard(), { idle: true, hasAgentSignal: false }) === null,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
ok(
|
|
106
|
+
"[QK:COMPACT-SEND-WIRE] resident imports the leaf, consults it before pi.sendMessage, and refuses with the returned token",
|
|
107
|
+
(() => {
|
|
108
|
+
const imported =
|
|
109
|
+
/from "\.\/lib\/compaction-send-guard\.js"/.test(CONTROL_SRC) && /compactionSendReject/.test(controlCode);
|
|
110
|
+
const sendIdx = controlCode.indexOf('if (command.type === "send")');
|
|
111
|
+
if (sendIdx < 0) return false;
|
|
112
|
+
const sendBlock = controlCode.slice(sendIdx);
|
|
113
|
+
const consult = sendBlock.search(/compactionSendReject\s*\(/);
|
|
114
|
+
const sendMessage = sendBlock.search(/pi\.sendMessage\s*\(/);
|
|
115
|
+
const ordered = consult >= 0 && sendMessage >= 0 && consult < sendMessage;
|
|
116
|
+
const refused = /respond\(\s*false,\s*"send",\s*undefined,\s*compactionReject\s*\)/.test(controlCode);
|
|
117
|
+
return imported && ordered && refused;
|
|
118
|
+
})(),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
ok(
|
|
122
|
+
"[QK:COMPACT-SEND-EVENTS] resident arms on session_before_compact and disarms on both terminal compact events",
|
|
123
|
+
/pi\.on\(\s*"session_before_compact"/.test(controlCode) &&
|
|
124
|
+
/noteCompactionBefore\s*\(\s*state\.compaction\s*\)/.test(controlCode) &&
|
|
125
|
+
/pi\.on\(\s*"session_compact"/.test(controlCode) &&
|
|
126
|
+
/pi\.on\(\s*"session_compact_failed"/.test(controlCode) &&
|
|
127
|
+
(controlCode.match(/noteCompactionTerminal\s*\(\s*state\.compaction\s*\)/g) ?? []).length >= 2,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
console.log(`\ncheck-compaction-send-guard: ${passed} checks passed`);
|
|
@@ -866,12 +866,15 @@ ok(
|
|
|
866
866
|
return true; // ESRCH — the pid is gone
|
|
867
867
|
}
|
|
868
868
|
});
|
|
869
|
+
// Reap before asserting: the parent-death mutant deliberately keeps this child alive,
|
|
870
|
+
// and ok() throws on that red path. Cleanup after ok() leaked one pid-1 Node process per
|
|
871
|
+
// qualification run — exactly the failure shape this assertion exists to prevent.
|
|
872
|
+
if (!gone) process.kill(orphanPid, "SIGKILL");
|
|
869
873
|
ok(
|
|
870
874
|
"[QK:COPILOT-RECEIVE-STUB-CHILD-DIES-WITH-PARENT] a fixture child whose parent disappeared exits by " +
|
|
871
875
|
"itself — no pid-1 receiver survives a red or interrupted run",
|
|
872
876
|
gone,
|
|
873
877
|
);
|
|
874
|
-
if (!gone) process.kill(orphanPid, "SIGKILL");
|
|
875
878
|
}
|
|
876
879
|
|
|
877
880
|
// ── 11. the installer's honest inverse ───────────────────────────────────────
|
|
@@ -178,6 +178,44 @@ async function main(): Promise<void> {
|
|
|
178
178
|
);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
// ── #112: expensive observation follows the 32-row human budget ───────────
|
|
182
|
+
{
|
|
183
|
+
const gids = Array.from({ length: 40 }, (_, i) => `20260612T${String(i).padStart(6, "0")}-aaaaaa`);
|
|
184
|
+
const meta = Object.fromEntries(gids.map((gid) => [`${gid}.meta.json`, rec(gid, "claude-code")]));
|
|
185
|
+
const allObserved: string[] = [];
|
|
186
|
+
const all = deps(meta, {});
|
|
187
|
+
all.observe = (identity) => {
|
|
188
|
+
allObserved.push(identity.gardenId);
|
|
189
|
+
return { receiver: "active", transcript: "exists" };
|
|
190
|
+
};
|
|
191
|
+
await listEntwurfFacts(all);
|
|
192
|
+
ok("Q112: generic provider without a presentation limit observes all 40 citizens", allObserved.length === 40);
|
|
193
|
+
|
|
194
|
+
const observed: string[] = [];
|
|
195
|
+
const d = deps(meta, {});
|
|
196
|
+
d.observationLimit = 32;
|
|
197
|
+
d.observe = (identity) => {
|
|
198
|
+
observed.push(identity.gardenId);
|
|
199
|
+
return { receiver: "active", transcript: "exists" };
|
|
200
|
+
};
|
|
201
|
+
const r = await listEntwurfFacts(d);
|
|
202
|
+
ok("Q112: full machine payload retains all 40 certified citizens", r.facts.peers.length === 40);
|
|
203
|
+
assert.deepStrictEqual(
|
|
204
|
+
observed,
|
|
205
|
+
gids.slice(-32),
|
|
206
|
+
"[QK:PEERS-OBSERVATION-BUDGET] observer must run for exactly the newest rendered rows",
|
|
207
|
+
);
|
|
208
|
+
ok("Q112: expensive observer runs exactly 32 times", observed.length === 32);
|
|
209
|
+
ok(
|
|
210
|
+
"Q112: eight historical payload rows honestly say unobserved",
|
|
211
|
+
r.facts.peers.slice(0, 8).every((p) => p.receiver === "unobserved" && p.transcript === "unobserved"),
|
|
212
|
+
);
|
|
213
|
+
ok(
|
|
214
|
+
"Q112: all 32 renderable rows retain measured observations",
|
|
215
|
+
r.facts.peers.slice(-32).every((p) => p.receiver === "active" && p.transcript === "exists"),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
181
219
|
// ── corrupt record does NOT blind the listing ──────────────────────────────
|
|
182
220
|
{
|
|
183
221
|
const r = await listEntwurfFacts(
|