@junghanacs/entwurf 0.23.0 → 0.24.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 +3 -1
- package/BASELINE.md +2 -0
- package/CHANGELOG.md +307 -0
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +43 -2
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +97 -18
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +33 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +7 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +13 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -1
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +15 -13
- package/pi-extensions/entwurf-control.ts +56 -0
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +20 -10
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +12 -4
- package/pi-extensions/lib/herdr-placement.ts +13 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/run.sh +327 -246
- package/scripts/check-acp-backend-preflight.ts +117 -9
- package/scripts/check-acp-carrier-augment.ts +6 -4
- package/scripts/check-acp-event-mapper.ts +17 -6
- package/scripts/check-acp-prompt-builder.ts +17 -9
- package/scripts/check-acp-prompt-lifecycle.ts +23 -6
- package/scripts/check-acp-session-reuse.ts +64 -51
- package/scripts/check-acp-session-store.ts +31 -8
- package/scripts/check-acp-stop-reason.ts +20 -2
- package/scripts/check-acp-stream-hooks.ts +22 -5
- package/scripts/check-acp-usage-accounting.ts +42 -18
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +45 -5
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +22 -6
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-pi-launch.ts +361 -0
- package/scripts/check-release-gate-outcomes.ts +7 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +11 -0
- package/scripts/mutants/herdr-plugin-build.json +1 -1
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +56 -28
- package/scripts/smoke-herdr-raw-install-live.sh +877 -0
- package/scripts/smoke-omp-fresh-live.ts +4 -2
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-provider-state.sh +2 -2
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -391
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -1,657 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* check-entwurf-v2-decider — deterministic gate for the PURE dispatch decider
|
|
3
|
-
* (0.11 Stage 0 step 5b). Drives `decideDispatch` over INJECTED fakes (no IO) and
|
|
4
|
-
* proves the frozen invariants (design §5):
|
|
5
|
-
*
|
|
6
|
-
* 1. reject ⇒ NO plan AND NO lock retained (any acquired lock was released).
|
|
7
|
-
* 2. execute ⇒ plan present ∧ receipt.transport === plan.transport ∧
|
|
8
|
-
* (in-domain ⇒ lock non-null, mailbox ⇒ lock null).
|
|
9
|
-
* 3. pre-probe rejects (bad-target / target-locked / target-address-conflict)
|
|
10
|
-
* carry observedLiveness=null; every post-probe reject carries the measured
|
|
11
|
-
* `dead` (non-null), and the deny path releases the lock (nonce-owned).
|
|
12
|
-
* 4. meta-mailbox plan has NO mode. (The resume-plan cell — no mode, wantsReply,
|
|
13
|
-
* expectedSocketPath/observeTimeoutMs/releaseWhen — went with the transport in
|
|
14
|
-
* the visible-first cut; there is no plan variant left to hold those fields.)
|
|
15
|
-
* 5. control-socket execute lock non-null; meta-mailbox execute lock null (?7).
|
|
16
|
-
* 6. the unsupported (mailbox) path acquires NO lock (?7).
|
|
17
|
-
* 7. a pre-probe address conflict rejects WITHOUT probing (inspectSocket unused).
|
|
18
|
-
* 8. unsupported + fire-and-forget: deliverability comes from the REQUIRED
|
|
19
|
-
* mailboxDeliverabilityFor seam (active-receiver), NOT wake-mode alone. seam
|
|
20
|
-
* deliverable=true ⇒ meta-mailbox execute; false ⇒ mailbox-undeliverable reject —
|
|
21
|
-
* a self-fetch citizen with an INACTIVE receiver is refused (SE-2 2d-3), no plan,
|
|
22
|
-
* no lock, no probe; the seam is consulted exactly once on the resolved identity.
|
|
23
|
-
* 9. an invalid garden id throws BEFORE any path/lock is built (F2-P1).
|
|
24
|
-
* 11. (B2) a throw AFTER the lock is acquired (inspect/probe) releases
|
|
25
|
-
* the held lock before the error propagates — no leak. A reject-path
|
|
26
|
-
* releaseLock that itself throws is RETRIED (the unlink never happened, so the
|
|
27
|
-
* lock is still ours) and the ORIGINAL error propagates — retry-pinned so a
|
|
28
|
-
* refactor cannot drop it.
|
|
29
|
-
* 12. (B3) a target-locked reject carries the lock's holder evidence as a
|
|
30
|
-
* diagnostic (pid/lockPath/detail), including the corrupt null-holder case;
|
|
31
|
-
* no other reject carries one.
|
|
32
|
-
*
|
|
33
|
-
* No IO — the target lookup, lock, and socket inspection/probe are all
|
|
34
|
-
* REQUIRED injected deps (the decider keeps NO live IO default); lock
|
|
35
|
-
* acquire/release calls are tracked so "no-lock-retained" is proven, not assumed.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import assert from "node:assert/strict";
|
|
39
|
-
import {
|
|
40
|
-
type DispatchDeciderDeps,
|
|
41
|
-
type DispatchDecision,
|
|
42
|
-
decideDispatch,
|
|
43
|
-
type ExecutionPlan,
|
|
44
|
-
resolveMailboxWakeModeCapability,
|
|
45
|
-
type TargetResolution,
|
|
46
|
-
} from "../pi-extensions/lib/entwurf-v2-decider.ts";
|
|
47
|
-
import type { AcquireLockResult, LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
48
|
-
import type { MetaCapability, MetaCitizenBackend, MetaIdentity } from "../pi-extensions/lib/meta-session.ts";
|
|
49
|
-
import type { NativePushProbeResult } from "../pi-extensions/lib/native-push/adapter.ts";
|
|
50
|
-
import { controlSocketPath, type TargetSocketInspection } from "../pi-extensions/lib/socket-discovery.ts";
|
|
51
|
-
import type { SocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
|
|
52
|
-
|
|
53
|
-
let passed = 0;
|
|
54
|
-
function ok(label: string, cond: boolean): void {
|
|
55
|
-
assert.ok(cond, label);
|
|
56
|
-
console.log(` ok ${label}`);
|
|
57
|
-
passed++;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const GID = "20260612T100000-aaaaaa";
|
|
61
|
-
const CWD = "/home/junghan/repos/gh/entwurf";
|
|
62
|
-
|
|
63
|
-
function identity(backend: MetaCitizenBackend): MetaIdentity {
|
|
64
|
-
return {
|
|
65
|
-
schemaVersion: 3,
|
|
66
|
-
gardenId: GID,
|
|
67
|
-
backend,
|
|
68
|
-
nativeSessionId: `native-${GID}`,
|
|
69
|
-
cwd: CWD,
|
|
70
|
-
model: null,
|
|
71
|
-
transcriptPath: null,
|
|
72
|
-
createdAt: "2026-06-12T01:00:00.000Z",
|
|
73
|
-
recordUpdatedAt: "2026-06-12T01:00:00.000Z",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function lockClaim(gardenId: string): LockClaim {
|
|
78
|
-
return {
|
|
79
|
-
gardenId,
|
|
80
|
-
pid: 4242,
|
|
81
|
-
hostname: "test-host",
|
|
82
|
-
createdAt: "2026-06-12T01:00:00.000Z",
|
|
83
|
-
nonce: "deadbeefcafef00d",
|
|
84
|
-
owner: "entwurf_v2",
|
|
85
|
-
lockPath: `/fake/locks/${gardenId}.lock`,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function capability(wakeMode: "self-fetch" | "direct-inject"): MetaCapability {
|
|
90
|
-
return { wakeMode, deliveryLevel: "D6", nativeIdLabel: "session" };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
interface ScenarioOpts {
|
|
94
|
-
resolution: TargetResolution;
|
|
95
|
-
lock?: "ok" | "conflict";
|
|
96
|
-
inspection?: TargetSocketInspection;
|
|
97
|
-
probe?: SocketLiveness;
|
|
98
|
-
/** SE-2 2d-3: the verdict the injected mailboxDeliverabilityFor seam returns on the
|
|
99
|
-
* unsupported path. Default false (fail-closed) — the decider trusts the seam, never
|
|
100
|
-
* wake-mode alone, so this is how a test asserts both the deliverable and inactive cells. */
|
|
101
|
-
mailboxDeliverable?: boolean;
|
|
102
|
-
/** 봉인 4: the native-push adapter probe result the injected nativePushProbe seam returns
|
|
103
|
-
* (only reached on a nativePushSupported backend, e.g. antigravity). */
|
|
104
|
-
nativePush?: NativePushProbeResult;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
interface Tracked {
|
|
108
|
-
deps: DispatchDeciderDeps;
|
|
109
|
-
acquireCalls: string[];
|
|
110
|
-
releaseCalls: LockClaim[];
|
|
111
|
-
inspectCalls: string[];
|
|
112
|
-
mailboxCalls: MetaIdentity[];
|
|
113
|
-
nativePushCalls: MetaIdentity[];
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Build injected deps with call tracking. A lock "conflict" returns a failed
|
|
117
|
-
// acquire; "ok" returns a fresh claim. If the unsupported path is exercised the
|
|
118
|
-
// acquireLock fake STILL records its (non-)use so the no-lock invariant is provable.
|
|
119
|
-
function mkDeps(opts: ScenarioOpts): Tracked {
|
|
120
|
-
const acquireCalls: string[] = [];
|
|
121
|
-
const releaseCalls: LockClaim[] = [];
|
|
122
|
-
const inspectCalls: string[] = [];
|
|
123
|
-
const mailboxCalls: MetaIdentity[] = [];
|
|
124
|
-
const nativePushCalls: MetaIdentity[] = [];
|
|
125
|
-
const deps: DispatchDeciderDeps = {
|
|
126
|
-
resolveTarget: () => opts.resolution,
|
|
127
|
-
acquireLock: (gardenId: string): AcquireLockResult => {
|
|
128
|
-
acquireCalls.push(gardenId);
|
|
129
|
-
if (opts.lock === "conflict") {
|
|
130
|
-
return {
|
|
131
|
-
ok: false,
|
|
132
|
-
conflict: {
|
|
133
|
-
reason: "target-locked",
|
|
134
|
-
lockPath: `/fake/locks/${gardenId}.lock`,
|
|
135
|
-
holder: lockClaim(gardenId),
|
|
136
|
-
detail: "held by a live dispatcher",
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
return { ok: true, claim: lockClaim(gardenId) };
|
|
141
|
-
},
|
|
142
|
-
releaseLock: (claim: LockClaim) => {
|
|
143
|
-
releaseCalls.push(claim);
|
|
144
|
-
},
|
|
145
|
-
inspectSocket: async (gardenId: string): Promise<TargetSocketInspection> => {
|
|
146
|
-
inspectCalls.push(gardenId);
|
|
147
|
-
return opts.inspection ?? { kind: "absent", socketPath: controlSocketPath(gardenId, "/fake/ctl") };
|
|
148
|
-
},
|
|
149
|
-
probeSocket: async (): Promise<SocketLiveness> => opts.probe ?? "dead",
|
|
150
|
-
mailboxDeliverabilityFor: (identity: MetaIdentity) => {
|
|
151
|
-
mailboxCalls.push(identity);
|
|
152
|
-
return { deliverable: opts.mailboxDeliverable ?? false, reason: "fake-deliverability" };
|
|
153
|
-
},
|
|
154
|
-
nativePushProbe: (identity: MetaIdentity): NativePushProbeResult => {
|
|
155
|
-
nativePushCalls.push(identity);
|
|
156
|
-
return opts.nativePush ?? { status: "dead", reason: "fake: no native-push probe configured" };
|
|
157
|
-
},
|
|
158
|
-
mailboxDir: "/fake/mailbox",
|
|
159
|
-
sessionsDir: "/fake/sessions",
|
|
160
|
-
};
|
|
161
|
-
return { deps, acquireCalls, releaseCalls, inspectCalls, mailboxCalls, nativePushCalls };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function isExecute(d: DispatchDecision): d is Extract<DispatchDecision, { kind: "execute" }> {
|
|
165
|
-
return d.kind === "execute";
|
|
166
|
-
}
|
|
167
|
-
function planKeys(plan: ExecutionPlan): string[] {
|
|
168
|
-
return Object.keys(plan).sort();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function main(): Promise<void> {
|
|
172
|
-
// ── 1+3: bad-target — no citizen → reject, observedLiveness null, NO lock ────
|
|
173
|
-
{
|
|
174
|
-
const t = mkDeps({ resolution: { identity: null, preProbeAddressConflict: false } });
|
|
175
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
|
|
176
|
-
ok("bad-target: reject", d.kind === "reject");
|
|
177
|
-
ok("bad-target: no plan field", !("plan" in d));
|
|
178
|
-
ok("bad-target: observedLiveness null (pre-probe)", d.kind === "reject" && d.receipt.observedLiveness === null);
|
|
179
|
-
ok("bad-target: reason", d.kind === "reject" && d.receipt.reason === "bad-target");
|
|
180
|
-
ok("bad-target: no lock acquired", t.acquireCalls.length === 0);
|
|
181
|
-
ok("bad-target: no diagnostic (only target-locked carries one)", d.kind === "reject" && d.diagnostic === undefined);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ── 7: pre-probe address conflict — reject WITHOUT probing or locking ────────
|
|
185
|
-
{
|
|
186
|
-
const t = mkDeps({ resolution: { identity: identity("claude-code"), preProbeAddressConflict: true } });
|
|
187
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
|
|
188
|
-
ok(
|
|
189
|
-
"preprobe-conflict: reject target-address-conflict",
|
|
190
|
-
d.kind === "reject" && d.receipt.reason === "target-address-conflict",
|
|
191
|
-
);
|
|
192
|
-
ok("preprobe-conflict: observedLiveness null", d.kind === "reject" && d.receipt.observedLiveness === null);
|
|
193
|
-
ok("preprobe-conflict: NOT probed (inspectSocket unused)", t.inspectCalls.length === 0);
|
|
194
|
-
ok("preprobe-conflict: no lock acquired", t.acquireCalls.length === 0);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// ── target-locked: acquire conflict → reject, null, nothing to release ───────
|
|
198
|
-
{
|
|
199
|
-
const t = mkDeps({ resolution: { identity: identity("pi"), preProbeAddressConflict: false }, lock: "conflict" });
|
|
200
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
|
|
201
|
-
ok("target-locked: reject", d.kind === "reject" && d.receipt.reason === "target-locked");
|
|
202
|
-
ok("target-locked: observedLiveness null (pre-probe)", d.kind === "reject" && d.receipt.observedLiveness === null);
|
|
203
|
-
ok("target-locked: lock acquire attempted", t.acquireCalls.length === 1);
|
|
204
|
-
ok("target-locked: no release (acquire failed → nothing held)", t.releaseCalls.length === 0);
|
|
205
|
-
ok("target-locked: not probed (lock before probe)", t.inspectCalls.length === 0);
|
|
206
|
-
// B3: the holder evidence the lock produced must survive to the decision.
|
|
207
|
-
ok("target-locked: diagnostic carried", d.kind === "reject" && d.diagnostic?.kind === "target-locked");
|
|
208
|
-
ok(
|
|
209
|
-
"target-locked: diagnostic holder pid preserved",
|
|
210
|
-
d.kind === "reject" && d.diagnostic?.kind === "target-locked" && d.diagnostic.conflict.holder?.pid === 4242,
|
|
211
|
-
);
|
|
212
|
-
ok(
|
|
213
|
-
"target-locked: diagnostic lockPath preserved",
|
|
214
|
-
d.kind === "reject" &&
|
|
215
|
-
d.diagnostic?.kind === "target-locked" &&
|
|
216
|
-
d.diagnostic.conflict.lockPath === `/fake/locks/${GID}.lock`,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ── B3: target-locked with a corrupt (null holder) lockfile still carries the ─
|
|
221
|
-
// diagnostic (lockPath + detail) — a human needs the path even when the body is
|
|
222
|
-
// empty/corrupt and there is no pid to show.
|
|
223
|
-
{
|
|
224
|
-
const d = await decideDispatch(
|
|
225
|
-
{ target: GID, intent: "fire-and-forget", message: "hi" },
|
|
226
|
-
{
|
|
227
|
-
resolveTarget: () => ({ identity: identity("pi"), preProbeAddressConflict: false }),
|
|
228
|
-
acquireLock: () => ({
|
|
229
|
-
ok: false,
|
|
230
|
-
conflict: {
|
|
231
|
-
reason: "target-locked",
|
|
232
|
-
lockPath: `/fake/locks/${GID}.lock`,
|
|
233
|
-
holder: null,
|
|
234
|
-
detail: "lockfile is empty, corrupt, or holds a different garden id",
|
|
235
|
-
},
|
|
236
|
-
}),
|
|
237
|
-
releaseLock: () => {},
|
|
238
|
-
inspectSocket: async () => {
|
|
239
|
-
throw new Error("target-locked(corrupt): must not probe after a lock conflict");
|
|
240
|
-
},
|
|
241
|
-
probeSocket: async () => "dead",
|
|
242
|
-
mailboxDeliverabilityFor: () => {
|
|
243
|
-
throw new Error("target-locked(corrupt): in-domain pi never consults the mailbox seam");
|
|
244
|
-
},
|
|
245
|
-
nativePushProbe: () => {
|
|
246
|
-
throw new Error("target-locked(corrupt): in-domain pi never consults the native-push seam");
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
);
|
|
250
|
-
ok("target-locked(corrupt): reject", d.kind === "reject" && d.receipt.reason === "target-locked");
|
|
251
|
-
ok(
|
|
252
|
-
"target-locked(corrupt): diagnostic carried with null holder",
|
|
253
|
-
d.kind === "reject" && d.diagnostic?.kind === "target-locked" && d.diagnostic.conflict.holder === null,
|
|
254
|
-
);
|
|
255
|
-
ok(
|
|
256
|
-
"target-locked(corrupt): diagnostic detail preserved",
|
|
257
|
-
d.kind === "reject" &&
|
|
258
|
-
d.diagnostic?.kind === "target-locked" &&
|
|
259
|
-
d.diagnostic.conflict.detail === "lockfile is empty, corrupt, or holds a different garden id",
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ── post-lock address conflict (pi + symlinked own socket) → release+reject ──
|
|
264
|
-
{
|
|
265
|
-
const t = mkDeps({
|
|
266
|
-
resolution: { identity: identity("pi"), preProbeAddressConflict: false },
|
|
267
|
-
lock: "ok",
|
|
268
|
-
inspection: { kind: "address-conflict", socketPath: "/fake/ctl/s.sock", reason: "symlink" },
|
|
269
|
-
});
|
|
270
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
|
|
271
|
-
ok(
|
|
272
|
-
"postlock-conflict: reject target-address-conflict",
|
|
273
|
-
d.kind === "reject" && d.receipt.reason === "target-address-conflict",
|
|
274
|
-
);
|
|
275
|
-
ok(
|
|
276
|
-
"postlock-conflict: observedLiveness null (pre-probe reason)",
|
|
277
|
-
d.kind === "reject" && d.receipt.observedLiveness === null,
|
|
278
|
-
);
|
|
279
|
-
ok("postlock-conflict: lock acquired", t.acquireCalls.length === 1);
|
|
280
|
-
ok("postlock-conflict: lock RELEASED (no-lock-retained)", t.releaseCalls.length === 1);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ── indeterminate → indeterminate-no-spawn, release ──────────────────────────
|
|
284
|
-
{
|
|
285
|
-
const t = mkDeps({
|
|
286
|
-
resolution: { identity: identity("pi"), preProbeAddressConflict: false },
|
|
287
|
-
lock: "ok",
|
|
288
|
-
inspection: { kind: "indeterminate", socketPath: "/fake/ctl/s.sock", error: "EACCES" },
|
|
289
|
-
});
|
|
290
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
|
|
291
|
-
ok(
|
|
292
|
-
"indeterminate: reject indeterminate-no-spawn",
|
|
293
|
-
d.kind === "reject" && d.receipt.reason === "indeterminate-no-spawn",
|
|
294
|
-
);
|
|
295
|
-
ok(
|
|
296
|
-
"indeterminate: observedLiveness indeterminate (non-null)",
|
|
297
|
-
d.kind === "reject" && d.receipt.observedLiveness === "indeterminate",
|
|
298
|
-
);
|
|
299
|
-
ok("indeterminate: lock released", t.releaseCalls.length === 1);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ── fire-and-forget + DORMANT pi → dormant-fire-forget-unsupported, release ──
|
|
303
|
-
{
|
|
304
|
-
const t = mkDeps({
|
|
305
|
-
resolution: { identity: identity("pi"), preProbeAddressConflict: false },
|
|
306
|
-
lock: "ok",
|
|
307
|
-
inspection: { kind: "absent", socketPath: "/fake/ctl/s.sock" },
|
|
308
|
-
});
|
|
309
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
|
|
310
|
-
ok(
|
|
311
|
-
"ff-dormant: reject dormant-fire-forget-unsupported",
|
|
312
|
-
d.kind === "reject" && d.receipt.reason === "dormant-fire-forget-unsupported",
|
|
313
|
-
);
|
|
314
|
-
ok("ff-dormant: observedLiveness dead (non-null)", d.kind === "reject" && d.receipt.observedLiveness === "dead");
|
|
315
|
-
ok("ff-dormant: lock released", t.releaseCalls.length === 1);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// ── 2+4+5: control-socket SEND execute (ff + live) — lock RETAINED ───────────
|
|
319
|
-
{
|
|
320
|
-
const t = mkDeps({
|
|
321
|
-
resolution: { identity: identity("pi"), preProbeAddressConflict: false },
|
|
322
|
-
lock: "ok",
|
|
323
|
-
inspection: { kind: "socket-file", socketPath: "/fake/ctl/s.sock" },
|
|
324
|
-
probe: "alive",
|
|
325
|
-
});
|
|
326
|
-
const d = await decideDispatch(
|
|
327
|
-
{ target: GID, intent: "fire-and-forget", mode: "steer", wantsReply: true, message: "ping" },
|
|
328
|
-
t.deps,
|
|
329
|
-
);
|
|
330
|
-
ok("send-execute: kind execute", isExecute(d));
|
|
331
|
-
if (isExecute(d)) {
|
|
332
|
-
ok("send-execute: plan transport control-socket", d.plan.transport === "control-socket");
|
|
333
|
-
ok("send-execute: receipt.transport === plan.transport", d.receipt.transport === d.plan.transport);
|
|
334
|
-
ok("send-execute: lock RETAINED (non-null)", d.lock !== null);
|
|
335
|
-
ok("send-execute: NOT released", t.releaseCalls.length === 0);
|
|
336
|
-
ok("send-execute: observedLiveness alive", d.receipt.observedLiveness === "alive");
|
|
337
|
-
if (d.plan.transport === "control-socket") {
|
|
338
|
-
ok("send-execute: socketPath planted", d.plan.socketPath === "/fake/ctl/s.sock");
|
|
339
|
-
ok("send-execute: mode carried", d.plan.mode === "steer");
|
|
340
|
-
ok("send-execute: wantsReply carried", d.plan.wantsReply === true);
|
|
341
|
-
assert.deepStrictEqual(
|
|
342
|
-
planKeys(d.plan),
|
|
343
|
-
["action", "message", "mode", "socketPath", "targetGardenId", "transport", "wantsReply"],
|
|
344
|
-
`control-socket plan keyset drift: ${planKeys(d.plan).join(",")}`,
|
|
345
|
-
);
|
|
346
|
-
ok("send-execute: plan keyset exact (no provider/model)", true);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ── 2+5+6: meta-mailbox SEND execute (unsupported claude + ff + deliverable) ─
|
|
352
|
-
// Deliverability comes from the required seam (active receiver), NOT wake-mode alone.
|
|
353
|
-
{
|
|
354
|
-
const t = mkDeps({
|
|
355
|
-
resolution: { identity: identity("claude-code"), preProbeAddressConflict: false },
|
|
356
|
-
mailboxDeliverable: true,
|
|
357
|
-
});
|
|
358
|
-
const d = await decideDispatch(
|
|
359
|
-
{ target: GID, intent: "fire-and-forget", mode: "steer", wantsReply: true, message: "mail" },
|
|
360
|
-
t.deps,
|
|
361
|
-
);
|
|
362
|
-
ok("mailbox-execute: kind execute", isExecute(d));
|
|
363
|
-
ok("mailbox-execute: deliverability seam consulted exactly once", t.mailboxCalls.length === 1);
|
|
364
|
-
ok("mailbox-execute: seam asked about the resolved identity", t.mailboxCalls[0]?.gardenId === GID);
|
|
365
|
-
if (isExecute(d) && d.plan.transport === "meta-mailbox") {
|
|
366
|
-
ok("mailbox-execute: receipt.transport === plan.transport", d.receipt.transport === "meta-mailbox");
|
|
367
|
-
ok("mailbox-execute: lock NULL (?7 no lock)", d.lock === null);
|
|
368
|
-
ok("mailbox-execute: acquireLock NOT called", t.acquireCalls.length === 0);
|
|
369
|
-
ok("mailbox-execute: observedLiveness unsupported", d.receipt.observedLiveness === "unsupported");
|
|
370
|
-
ok("mailbox-execute: mailboxDir planted", d.plan.mailboxDir === "/fake/mailbox");
|
|
371
|
-
ok("mailbox-execute: sessionsDir planted (D2)", d.plan.sessionsDir === "/fake/sessions");
|
|
372
|
-
assert.deepStrictEqual(
|
|
373
|
-
planKeys(d.plan),
|
|
374
|
-
["action", "mailboxDir", "message", "sessionsDir", "targetGardenId", "transport", "wantsReply"],
|
|
375
|
-
`meta-mailbox plan keyset drift: ${planKeys(d.plan).join(",")}`,
|
|
376
|
-
);
|
|
377
|
-
ok("mailbox-execute: NO mode in plan (?2)", !("mode" in d.plan));
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ── 8: unsupported + ff + seam says undeliverable → reject, no plan, no lock ─
|
|
382
|
-
{
|
|
383
|
-
const t = mkDeps({
|
|
384
|
-
resolution: { identity: identity("omp"), preProbeAddressConflict: false },
|
|
385
|
-
mailboxDeliverable: false,
|
|
386
|
-
});
|
|
387
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "mail" }, t.deps);
|
|
388
|
-
ok("mailbox-undeliverable: reject", d.kind === "reject" && d.receipt.reason === "mailbox-undeliverable");
|
|
389
|
-
ok(
|
|
390
|
-
"mailbox-undeliverable: observedLiveness unsupported (non-null)",
|
|
391
|
-
d.kind === "reject" && d.receipt.observedLiveness === "unsupported",
|
|
392
|
-
);
|
|
393
|
-
ok("mailbox-undeliverable: no plan", !("plan" in d));
|
|
394
|
-
ok("mailbox-undeliverable: acquireLock NOT called", t.acquireCalls.length === 0);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// ── SE-2 2d-3 KEY ROW: a self-fetch CITIZEN whose receiver is INACTIVE (the seam
|
|
398
|
-
// says not deliverable — terminated session / drifted marker) → mailbox-undeliverable
|
|
399
|
-
// reject, NO plan, NO lock, NO inspect/probe. This is the v2 closure of the gap slice
|
|
400
|
-
// 2d-2 closed for v1: the decider no longer trusts wake-mode alone; the required seam's
|
|
401
|
-
// active-receiver verdict governs, so a reply to a dead claude-code is refused, not
|
|
402
|
-
// enqueued as mailbox garbage. ───────────────────────────────────────────────────
|
|
403
|
-
{
|
|
404
|
-
const t = mkDeps({
|
|
405
|
-
resolution: { identity: identity("claude-code"), preProbeAddressConflict: false },
|
|
406
|
-
mailboxDeliverable: false,
|
|
407
|
-
});
|
|
408
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "mail" }, t.deps);
|
|
409
|
-
ok(
|
|
410
|
-
"se2-inactive: reject mailbox-undeliverable",
|
|
411
|
-
d.kind === "reject" && d.receipt.reason === "mailbox-undeliverable",
|
|
412
|
-
);
|
|
413
|
-
ok("se2-inactive: no plan (no enqueue plan minted)", !("plan" in d));
|
|
414
|
-
ok("se2-inactive: acquireLock NOT called (?7)", t.acquireCalls.length === 0);
|
|
415
|
-
ok("se2-inactive: never inspect/probe (unsupported axis)", t.inspectCalls.length === 0);
|
|
416
|
-
ok("se2-inactive: deliverability seam consulted exactly once", t.mailboxCalls.length === 1);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// ── #50 C4: record-less socket (identity null + recordLessSocket) ──────────
|
|
420
|
-
// The record is the sole address authority, so a bare control socket is NOT an
|
|
421
|
-
// addressable citizen — EVERY intent rejects pre-probe as `record-less-socket`
|
|
422
|
-
// (a diagnostic state, observedLiveness=null): no lock, no inspect, no
|
|
423
|
-
// probe, no mailbox seam, no plan. NOT `bad-target` (something real answers to
|
|
424
|
-
// the gid — "absent" would hide the state). The retired A1 narrow used to accept
|
|
425
|
-
// ff sends into this socket; that acceptance must never come back.
|
|
426
|
-
const recordLess: TargetResolution = { identity: null, preProbeAddressConflict: false, recordLessSocket: true };
|
|
427
|
-
for (const intent of ["fire-and-forget"] as const) {
|
|
428
|
-
const t = mkDeps({ resolution: recordLess });
|
|
429
|
-
const d = await decideDispatch({ target: GID, intent, message: "ping" }, t.deps);
|
|
430
|
-
ok(
|
|
431
|
-
`recordLess ${intent}: reject record-less-socket`,
|
|
432
|
-
d.kind === "reject" && d.receipt.reason === "record-less-socket",
|
|
433
|
-
);
|
|
434
|
-
ok(
|
|
435
|
-
`recordLess ${intent}: observedLiveness null (pre-probe — no citizen, no probe)`,
|
|
436
|
-
d.kind === "reject" && d.receipt.observedLiveness === null,
|
|
437
|
-
);
|
|
438
|
-
ok(`recordLess ${intent}: NO lock acquired`, t.acquireCalls.length === 0);
|
|
439
|
-
ok(`recordLess ${intent}: NO socket inspect/probe`, t.inspectCalls.length === 0);
|
|
440
|
-
ok(`recordLess ${intent}: mailbox seam NEVER consulted`, t.mailboxCalls.length === 0);
|
|
441
|
-
ok(`recordLess ${intent}: no plan`, !("plan" in d));
|
|
442
|
-
}
|
|
443
|
-
{
|
|
444
|
-
// The hint is only meaningful with identity null — a PLAIN absent gid (no socket)
|
|
445
|
-
// stays the honest bad-target, and the two must not blur.
|
|
446
|
-
const t = mkDeps({ resolution: { identity: null, preProbeAddressConflict: false, recordLessSocket: false } });
|
|
447
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "ping" }, t.deps);
|
|
448
|
-
ok("recordLess=false absent gid: reject bad-target", d.kind === "reject" && d.receipt.reason === "bad-target");
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ── B2: a throw AFTER the lock is acquired RELEASES it before rethrowing ─────
|
|
452
|
-
// inspectSocket / probeSocket / preflightForCwd are the three post-lock IO sites
|
|
453
|
-
// that can throw. Each must leave NO held lock (else a long-lived MCP bridge pins
|
|
454
|
-
// the gid forever). The decision still propagates the error (the decider does not
|
|
455
|
-
// swallow it); only the lock is cleaned up.
|
|
456
|
-
{
|
|
457
|
-
const runThrowing = async (label: string, over: Partial<DispatchDeciderDeps>): Promise<void> => {
|
|
458
|
-
const released: LockClaim[] = [];
|
|
459
|
-
let threw = false;
|
|
460
|
-
try {
|
|
461
|
-
await decideDispatch(
|
|
462
|
-
{ target: GID, intent: "fire-and-forget", message: "x" },
|
|
463
|
-
{
|
|
464
|
-
resolveTarget: () => ({ identity: identity("pi"), preProbeAddressConflict: false }),
|
|
465
|
-
acquireLock: () => ({ ok: true, claim: lockClaim(GID) }),
|
|
466
|
-
releaseLock: (c: LockClaim) => {
|
|
467
|
-
released.push(c);
|
|
468
|
-
},
|
|
469
|
-
inspectSocket: async () => ({ kind: "absent", socketPath: "/fake/ctl/s.sock" }),
|
|
470
|
-
probeSocket: async () => "dead",
|
|
471
|
-
mailboxDeliverabilityFor: () => ({ deliverable: false, reason: "in-domain pi: seam unused" }),
|
|
472
|
-
nativePushProbe: () => {
|
|
473
|
-
throw new Error("lock-leak: in-domain pi never consults the native-push seam");
|
|
474
|
-
},
|
|
475
|
-
...over,
|
|
476
|
-
},
|
|
477
|
-
);
|
|
478
|
-
} catch {
|
|
479
|
-
threw = true;
|
|
480
|
-
}
|
|
481
|
-
ok(`lock-leak(${label}): error rethrown`, threw);
|
|
482
|
-
ok(`lock-leak(${label}): lock RELEASED before rethrow`, released.length === 1);
|
|
483
|
-
ok(`lock-leak(${label}): released the held claim`, released[0]?.gardenId === GID);
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
await runThrowing("inspect throw", {
|
|
487
|
-
inspectSocket: async () => {
|
|
488
|
-
throw new Error("inspect boom");
|
|
489
|
-
},
|
|
490
|
-
});
|
|
491
|
-
await runThrowing("probe throw", {
|
|
492
|
-
inspectSocket: async () => ({ kind: "socket-file", socketPath: "/fake/ctl/s.sock" }),
|
|
493
|
-
probeSocket: async () => {
|
|
494
|
-
throw new Error("probe boom");
|
|
495
|
-
},
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// ── B2 retry-pin (Fable 2차 권고): a reject-path releaseLock that THROWS is ──
|
|
500
|
-
// retried by the catch. releaseLock can only throw when the unlink did NOT happen
|
|
501
|
-
// (a successful unlink returns "released"), so at catch time the lock is still
|
|
502
|
-
// ours → the 2nd attempt is a legitimate retry, not a double-free. The ORIGINAL
|
|
503
|
-
// error (not the retry's) propagates, and releaseLock is called exactly twice.
|
|
504
|
-
// This pins the retry so a future refactor cannot silently drop it.
|
|
505
|
-
{
|
|
506
|
-
const released: LockClaim[] = [];
|
|
507
|
-
let caught: unknown = null;
|
|
508
|
-
try {
|
|
509
|
-
await decideDispatch(
|
|
510
|
-
{ target: GID, intent: "fire-and-forget", message: "x" },
|
|
511
|
-
{
|
|
512
|
-
resolveTarget: () => ({ identity: identity("pi"), preProbeAddressConflict: false }),
|
|
513
|
-
acquireLock: () => ({ ok: true, claim: lockClaim(GID) }),
|
|
514
|
-
releaseLock: (c: LockClaim) => {
|
|
515
|
-
released.push(c);
|
|
516
|
-
throw new Error(`release boom ${released.length}`);
|
|
517
|
-
},
|
|
518
|
-
// fire-and-forget + DORMANT → dormant-fire-forget-unsupported → rejectAfterRelease
|
|
519
|
-
inspectSocket: async () => ({ kind: "socket-file", socketPath: "/fake/ctl/s.sock" }),
|
|
520
|
-
probeSocket: async () => "dead",
|
|
521
|
-
mailboxDeliverabilityFor: () => ({ deliverable: false, reason: "in-domain pi: seam unused" }),
|
|
522
|
-
nativePushProbe: () => {
|
|
523
|
-
throw new Error("reject-release-throw: in-domain pi never consults the native-push seam");
|
|
524
|
-
},
|
|
525
|
-
},
|
|
526
|
-
);
|
|
527
|
-
} catch (e) {
|
|
528
|
-
caught = e;
|
|
529
|
-
}
|
|
530
|
-
ok(
|
|
531
|
-
"reject-release-throw: ORIGINAL error propagates (not the retry's)",
|
|
532
|
-
caught instanceof Error && caught.message === "release boom 1",
|
|
533
|
-
);
|
|
534
|
-
ok("reject-release-throw: releaseLock retried exactly twice", released.length === 2);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// ── 9: invalid garden id throws BEFORE resolveTarget (F2-P1) ─────────────────
|
|
538
|
-
{
|
|
539
|
-
let threw = false;
|
|
540
|
-
let resolveCalled = false;
|
|
541
|
-
const deps: DispatchDeciderDeps = {
|
|
542
|
-
resolveTarget: () => {
|
|
543
|
-
resolveCalled = true;
|
|
544
|
-
return { identity: null, preProbeAddressConflict: false };
|
|
545
|
-
},
|
|
546
|
-
// Required IO seams: the throw at requireGardenId must precede every one of
|
|
547
|
-
// them, so they are wired as "must not be reached" tripwires.
|
|
548
|
-
acquireLock: () => {
|
|
549
|
-
throw new Error("invalid-gid: acquireLock must not be reached");
|
|
550
|
-
},
|
|
551
|
-
releaseLock: () => {
|
|
552
|
-
throw new Error("invalid-gid: releaseLock must not be reached");
|
|
553
|
-
},
|
|
554
|
-
inspectSocket: async () => {
|
|
555
|
-
throw new Error("invalid-gid: inspectSocket must not be reached");
|
|
556
|
-
},
|
|
557
|
-
probeSocket: async () => {
|
|
558
|
-
throw new Error("invalid-gid: probeSocket must not be reached");
|
|
559
|
-
},
|
|
560
|
-
mailboxDeliverabilityFor: () => {
|
|
561
|
-
throw new Error("invalid-gid: mailboxDeliverabilityFor must not be reached");
|
|
562
|
-
},
|
|
563
|
-
nativePushProbe: () => {
|
|
564
|
-
throw new Error("invalid-gid: nativePushProbe must not be reached");
|
|
565
|
-
},
|
|
566
|
-
};
|
|
567
|
-
try {
|
|
568
|
-
await decideDispatch({ target: "../etc/passwd", intent: "fire-and-forget", message: "x" }, deps);
|
|
569
|
-
} catch {
|
|
570
|
-
threw = true;
|
|
571
|
-
}
|
|
572
|
-
ok("invalid-gid: throws", threw);
|
|
573
|
-
ok("invalid-gid: throws BEFORE resolveTarget (no path/lookup built)", !resolveCalled);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// ── native-push rail (봉인 4): antigravity is intercepted BEFORE the unsupported ──
|
|
577
|
-
// mailbox branch, routed by nativePushProbe + the NATIVE_PUSH table, LOCK-FREE.
|
|
578
|
-
const npResolution = { identity: identity("antigravity"), preProbeAddressConflict: false };
|
|
579
|
-
{
|
|
580
|
-
// ff × alive → execute native-push send; plan carries route/backend/nativeSessionId; lock null.
|
|
581
|
-
const t = mkDeps({
|
|
582
|
-
resolution: npResolution,
|
|
583
|
-
nativePush: { status: "alive", route: { backend: "antigravity", lsAddress: "127.0.0.1:5599" } },
|
|
584
|
-
});
|
|
585
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "yo" }, t.deps);
|
|
586
|
-
ok("native-push ff+alive: execute", isExecute(d));
|
|
587
|
-
ok("native-push ff+alive: transport native-push", isExecute(d) && d.plan.transport === "native-push");
|
|
588
|
-
ok("native-push ff+alive: LOCK-FREE (decision lock null)", isExecute(d) && d.lock === null);
|
|
589
|
-
ok("native-push ff+alive: acquireLock NOT called (lock-free rail)", t.acquireCalls.length === 0);
|
|
590
|
-
ok("native-push ff+alive: nativePushProbe consulted once", t.nativePushCalls.length === 1);
|
|
591
|
-
ok(
|
|
592
|
-
"native-push ff+alive: mailbox seam NOT consulted (intercepts before unsupported branch)",
|
|
593
|
-
t.mailboxCalls.length === 0,
|
|
594
|
-
);
|
|
595
|
-
if (isExecute(d) && d.plan.transport === "native-push") {
|
|
596
|
-
ok(
|
|
597
|
-
"native-push ff+alive: plan route = probed route",
|
|
598
|
-
d.plan.route.backend === "antigravity" && d.plan.route.lsAddress === "127.0.0.1:5599",
|
|
599
|
-
);
|
|
600
|
-
ok("native-push ff+alive: plan backend = antigravity", d.plan.backend === "antigravity");
|
|
601
|
-
ok("native-push ff+alive: plan nativeSessionId from identity", d.plan.nativeSessionId === `native-${GID}`);
|
|
602
|
-
ok(
|
|
603
|
-
"native-push ff+alive: plan keyset exact",
|
|
604
|
-
planKeys(d.plan).join(",") ===
|
|
605
|
-
"action,backend,message,nativeSessionId,route,targetGardenId,transport,wantsReply",
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
ok(
|
|
609
|
-
"native-push ff+alive: receipt success transport native-push, observedLiveness alive",
|
|
610
|
-
isExecute(d) && d.receipt.ok && d.receipt.transport === "native-push" && d.receipt.observedLiveness === "alive",
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
{
|
|
614
|
-
// ff × dead → reject native-push-target-dead (post-probe, dead), lock-free.
|
|
615
|
-
const t = mkDeps({ resolution: npResolution, nativePush: { status: "dead", reason: "no host" } });
|
|
616
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "yo" }, t.deps);
|
|
617
|
-
ok(
|
|
618
|
-
"native-push ff+dead: reject native-push-target-dead",
|
|
619
|
-
d.kind === "reject" && d.receipt.reason === "native-push-target-dead",
|
|
620
|
-
);
|
|
621
|
-
ok(
|
|
622
|
-
"native-push ff+dead: observedLiveness dead (post-probe stamp)",
|
|
623
|
-
d.kind === "reject" && d.receipt.observedLiveness === "dead",
|
|
624
|
-
);
|
|
625
|
-
ok("native-push ff+dead: acquireLock NOT called", t.acquireCalls.length === 0);
|
|
626
|
-
}
|
|
627
|
-
{
|
|
628
|
-
// ff × indeterminate → reject native-push-probe-indeterminate.
|
|
629
|
-
const t = mkDeps({ resolution: npResolution, nativePush: { status: "indeterminate", reason: "no port served" } });
|
|
630
|
-
const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "yo" }, t.deps);
|
|
631
|
-
ok(
|
|
632
|
-
"native-push ff+indeterminate: reject native-push-probe-indeterminate",
|
|
633
|
-
d.kind === "reject" && d.receipt.reason === "native-push-probe-indeterminate",
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// The wake-mode capability HELPER stays gate-pinned (renamed; the decider no longer
|
|
638
|
-
// calls it directly — deliverability flows through the required seam). It answers the
|
|
639
|
-
// capability HALF only; the active-receiver half lives in the production seam.
|
|
640
|
-
ok(
|
|
641
|
-
"wakeMode-capability: claude-code (self-fetch) → capability-deliverable",
|
|
642
|
-
resolveMailboxWakeModeCapability(identity("claude-code"), () => capability("self-fetch")),
|
|
643
|
-
);
|
|
644
|
-
ok(
|
|
645
|
-
"wakeMode-capability: codex (direct-inject) → NOT capability-deliverable (fail-closed)",
|
|
646
|
-
!resolveMailboxWakeModeCapability(identity("codex"), () => capability("direct-inject")),
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
main()
|
|
651
|
-
.then(() => {
|
|
652
|
-
console.log(`\n[check-entwurf-v2-decider] ${passed} assertions ok`);
|
|
653
|
-
})
|
|
654
|
-
.catch((err) => {
|
|
655
|
-
console.error(err);
|
|
656
|
-
process.exit(1);
|
|
657
|
-
});
|