@junghanacs/entwurf 0.13.1 → 0.14.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.
Files changed (152) hide show
  1. package/AGENTS.md +48 -15
  2. package/BASELINE.md +3 -3
  3. package/CHANGELOG.md +45 -0
  4. package/CONTRIBUTING.md +13 -9
  5. package/DELIVERY.md +7 -6
  6. package/README.md +27 -27
  7. package/VERIFY.md +22 -14
  8. package/demo/README.md +1 -1
  9. package/demo/demo-baseline.sh +1 -3
  10. package/demo/demo.sh +2 -5
  11. package/docs/acp-backend-rail.md +9 -4
  12. package/docs/external-mcp-host.md +4 -5
  13. package/docs/setup-clean-host.md +8 -7
  14. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +148 -28
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +8 -6
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  37. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  38. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  39. package/mcp/entwurf-bridge/src/index.ts +164 -28
  40. package/mcp/entwurf-bridge/start.sh +2 -2
  41. package/mcp/entwurf-bridge/test.sh +23 -9
  42. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  43. package/package.json +22 -11
  44. package/pi-extensions/entwurf-control.ts +218 -40
  45. package/pi-extensions/lib/acp/backend.ts +71 -12
  46. package/pi-extensions/lib/acp/overlay.ts +8 -6
  47. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  48. package/pi-extensions/lib/entwurf-core.ts +15 -15
  49. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  50. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  51. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  52. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  53. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  54. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  55. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  56. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  57. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  58. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  59. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  60. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  61. package/pi-extensions/lib/meta-session.ts +93 -5
  62. package/pi-extensions/lib/mux-fresh-call.ts +328 -0
  63. package/pi-extensions/lib/mux-launch.ts +267 -0
  64. package/pi-extensions/lib/mux-placement.ts +387 -0
  65. package/pi-extensions/lib/mux-resume-call.ts +221 -0
  66. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  67. package/pi-extensions/lib/session-id.js +8 -5
  68. package/pi-extensions/lib/socket-discovery.ts +3 -3
  69. package/run.sh +471 -201
  70. package/scripts/agy-bridge-config.py +5 -1
  71. package/scripts/check-acp-backend-preflight.ts +1 -1
  72. package/scripts/check-acp-overlay.ts +13 -3
  73. package/scripts/check-acp-stream-hooks.ts +504 -0
  74. package/scripts/check-elapsed.sh +25 -0
  75. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  76. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  77. package/scripts/check-entwurf-control-rpc.ts +4 -3
  78. package/scripts/check-entwurf-resume-args.ts +72 -70
  79. package/scripts/check-entwurf-session-identity.ts +14 -10
  80. package/scripts/check-entwurf-v2-contract.ts +34 -59
  81. package/scripts/check-entwurf-v2-decider.ts +17 -177
  82. package/scripts/check-entwurf-v2-lock.ts +5 -2
  83. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  84. package/scripts/check-entwurf-v2-production.ts +2 -91
  85. package/scripts/check-entwurf-v2-release.ts +10 -105
  86. package/scripts/check-entwurf-v2-runner.ts +4 -85
  87. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  88. package/scripts/check-entwurf-v2-send.ts +0 -28
  89. package/scripts/check-entwurf-v2-surface.ts +157 -128
  90. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  91. package/scripts/check-fresh-cut-gate.sh +1 -1
  92. package/scripts/check-gate-qualification.ts +98 -7
  93. package/scripts/check-install-container.sh +10 -2
  94. package/scripts/check-install-surface.ts +1 -1
  95. package/scripts/check-keyset-overlap.py +1 -1
  96. package/scripts/check-meta-facts.ts +249 -0
  97. package/scripts/check-meta-identity-consumers.ts +1 -1
  98. package/scripts/check-meta-session.ts +169 -0
  99. package/scripts/check-mux-launch-tmux.ts +316 -0
  100. package/scripts/check-mux-launch.ts +288 -0
  101. package/scripts/check-mux-launcher-fence.ts +264 -0
  102. package/scripts/check-mux-parent-artifact.ts +195 -0
  103. package/scripts/check-mux-placement-tmux.ts +322 -0
  104. package/scripts/check-mux-placement.ts +323 -0
  105. package/scripts/check-mux-resume-call.ts +282 -0
  106. package/scripts/check-probe-cli-shim.ts +25 -22
  107. package/scripts/check-probe-ordering.ts +84 -76
  108. package/scripts/check-release-gate-outcomes.ts +127 -7
  109. package/scripts/check-resume-launch-identity.ts +244 -0
  110. package/scripts/check-socket-discovery.ts +1 -1
  111. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  112. package/scripts/inventory-verification-surface.ts +349 -0
  113. package/scripts/lib/claude-launcher-fence.ts +322 -0
  114. package/scripts/lib/mutation-qualify.ts +109 -3
  115. package/scripts/meta-bridge-doctor.sh +6 -8
  116. package/scripts/meta-facts.ts +60 -0
  117. package/scripts/mutants/acp-overlay.json +17 -0
  118. package/scripts/mutants/acp-stream-hooks.json +158 -0
  119. package/scripts/mutants/bridge-boot-resume.json +45 -0
  120. package/scripts/mutants/meta-facts.json +50 -0
  121. package/scripts/mutants/meta-identity.json +36 -0
  122. package/scripts/mutants/mux-boundary.json +196 -0
  123. package/scripts/mutants/mux-fresh-call.json +185 -0
  124. package/scripts/mutants/mux-launcher-fence.json +123 -0
  125. package/scripts/mutants/mux-parent-artifact.json +39 -0
  126. package/scripts/mutants/mux-resume-call.json +148 -0
  127. package/scripts/mutants/probe-ordering.json +0 -1037
  128. package/scripts/mutants/release-gate.json +35 -0
  129. package/scripts/mutants/resume-args.json +76 -0
  130. package/scripts/mutants/resume-launch-identity.json +96 -0
  131. package/scripts/mutants/v2-surface.json +58 -18
  132. package/scripts/mutants/v2-visible-resume.json +215 -0
  133. package/scripts/new-session-id.ts +9 -4
  134. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  135. package/scripts/smoke-agy-native-push-live.ts +6 -17
  136. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  137. package/scripts/smoke-meta-honesty.sh +1 -1
  138. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  139. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  140. package/scripts/smoke-pi-attach.ts +1 -1
  141. package/scripts/smoke-user-scope-citizen.sh +1 -1
  142. package/scripts/tsconfig.json +1 -0
  143. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  144. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  145. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  146. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  147. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  148. package/scripts/check-acp-sdk-surface.ts +0 -275
  149. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  150. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  151. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  152. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -467
@@ -3,23 +3,22 @@
3
3
  * (0.11 Stage 0 step 5b). It turns a caller's (target, intent, …) request plus
4
4
  * already-resolvable facts into a single `DispatchDecision`: either a reject
5
5
  * receipt, or an execute receipt + an `ExecutionPlan` + (for in-domain backends) a
6
- * held `LockClaim`. It performs NO transport: no send, no enqueue, no spawn, no
7
- * release-watcher. Step 5c executes the chosen plan; step 5d registers the MCP
8
- * surface. The decider only DECIDES.
6
+ * held `LockClaim`. It performs NO transport: no send, no enqueue, no release-watcher.
7
+ * Step 5c executes the chosen plan; step 5d registers the MCP surface. The decider
8
+ * only DECIDES.
9
+ *
10
+ * Since the visible-first cut (contract header) this module also starts NO process:
11
+ * the resume verdict, its trust preflight, and the detached spawn-bg plan are gone.
12
+ * Every plan it can emit now targets a citizen that is already running.
9
13
  *
10
14
  * Why a separate pure module (step 4 discipline = gate-first → pure-before-IO →
11
15
  * wire): every IO surface the decision needs — the target lookup, the per-gid lock,
12
- * the lstat/connect socket inspection, the trust preflight, the mailbox-deliverability
13
- * seam — is INJECTED via `DispatchDeciderDeps`, so the gate drives every branch with
14
- * fakes and the live wrappers wire the real fns. The plan is shaped so 5c's
15
- * transport hand consumes it WITHOUT re-deriving any path/arg (socketPath,
16
- * mailboxDir, sessionsDir, launchArgs are all planted here once — 4c "재유도 금지"):
17
- * the hand is a plan-keyed dispatcher, never a second brain. The two deliberate
18
- * exceptions (GPT힣 1차 검수): the spawned child's runtime pid is NOT in the plan
19
- * (it is born during 5c execution → it is the watcher's release-context, not a plan
20
- * input), and the launch identity (provider/model) is read by the 5c launcher from
21
- * the saved session JSONL (its existing authority) — putting that read in the
22
- * decider would make it impure. So the plan carries no provider/model.
16
+ * the lstat/connect socket inspection, the mailbox-deliverability seam is INJECTED
17
+ * via `DispatchDeciderDeps`, so the gate drives every branch with fakes and the live
18
+ * wrappers wire the real fns. The plan is shaped so 5c's transport hand consumes it
19
+ * WITHOUT re-deriving any path/arg (socketPath, mailboxDir, sessionsDir are all
20
+ * planted here once — 4c "재유도 금지"): the hand is a plan-keyed dispatcher, never a
21
+ * second brain.
23
22
  *
24
23
  * The frozen 7-step order (NEXT.md "통합 decider 순서"):
25
24
  * 1. requireGardenId — runtime guard BEFORE any path is built (F2-P1; closes the
@@ -32,8 +31,7 @@
32
31
  * 4. acquireLock — IN-DOMAIN ONLY (?7), BEFORE lstat/connect, so the probe
33
32
  * happens under the lock (the TOCTOU 5a's lock closes).
34
33
  * 5. in-domain — inspectTargetControlSocket (lstat-then-connect, ?2) →
35
- * resolveDispatch → on a resume verdict, preflight the target cwd (1B: deny
36
- * nonce-owned release → untrusted-fail-fast) → plan.
34
+ * resolveDispatch → control-socket send plan, or a reject that releases the lock.
37
35
  * 6. unsupported — NO lock; deps.mailboxDeliverabilityFor (REQUIRED seam: wake-mode
38
36
  * capability AND a live active-receiver, fail-closed) → resolveDispatch → meta-mailbox
39
37
  * plan or reject. SE-2 2d-3: a terminated/drifted self-fetch citizen is refused, never
@@ -48,7 +46,6 @@
48
46
 
49
47
  import type { MailboxDeliverabilityResult } from "./entwurf-deliverability.ts";
50
48
  import { isOutOfSocketDomainGardenIdConflict } from "./entwurf-facts.ts";
51
- import type { PreflightOutcome } from "./entwurf-preflight.ts";
52
49
  import {
53
50
  type EntwurfIntent,
54
51
  type EntwurfV2Receipt,
@@ -77,17 +74,6 @@ import type { SocketLiveness } from "./socket-probe.ts";
77
74
  // single import site for it (it is the SAME fn the fact-provider listing uses).
78
75
  export { isOutOfSocketDomainGardenIdConflict };
79
76
 
80
- // ── observe timeout (?3) ───────────────────────────────────────────────────
81
- // The bounded wait 5c's release-watcher gives a spawned child to surface its
82
- // control socket / exit. Planted into the resume plan so the hand does not invent
83
- // a timeout. A standalone constant + env override (NOT a multiple of the probe
84
- // timeout — a different concern); 30s initial, tunable to 45s after live data.
85
- export const ENTWURF_V2_OBSERVE_TIMEOUT_MS = ((): number => {
86
- const raw = process.env.ENTWURF_V2_OBSERVE_TIMEOUT_MS;
87
- const n = raw !== undefined && raw !== "" ? Number(raw) : Number.NaN;
88
- return Number.isFinite(n) && n > 0 ? n : 30_000;
89
- })();
90
-
91
77
  export const ENTWURF_V2_MODE_DEFAULT = "follow_up" as const;
92
78
 
93
79
  // ── receipt branch aliases ──────────────────────────────────────────────────
@@ -97,9 +83,9 @@ export type EntwurfV2Mode = "steer" | "follow_up";
97
83
 
98
84
  // ── ExecutionPlan (5c-consumable, no re-derivation) ─────────────────────────
99
85
  // Each plan kind carries every value 5c's transport fn needs, planted once by the
100
- // decider. control-socket send and meta-mailbox send carry the message; spawn-bg
101
- // resume carries the launch inputs MINUS provider/model (D4: 5c-owned identity
102
- // read) and MINUS the child pid (born at execution, watcher's release-context).
86
+ // decider. Every remaining kind is a SEND to an already-running citizen: the
87
+ // spawn-bg resume plan (launch inputs, expected socket path, observe timeout) went
88
+ // with the transport in the visible-first cut.
103
89
  export type ExecutionPlan =
104
90
  | {
105
91
  transport: "control-socket";
@@ -119,22 +105,6 @@ export type ExecutionPlan =
119
105
  wantsReply: boolean;
120
106
  message: string;
121
107
  }
122
- | {
123
- transport: "spawn-bg";
124
- action: "resume";
125
- targetGardenId: string;
126
- sessionId: string;
127
- cwd: string;
128
- prompt: string;
129
- // wantsReply rides the plan so the dormant rail's <sender_info> carries the
130
- // same wants_reply etiquette marker a live socket delivery renders (#50 F2).
131
- // mode stays live-injection-only (meaningless for a resume prompt).
132
- wantsReply: boolean;
133
- launchArgs: readonly string[];
134
- expectedSocketPath: string;
135
- observeTimeoutMs: number;
136
- releaseWhen: "socket-alive-or-child-exited";
137
- }
138
108
  // native-push send (봉인 4): direct-inject into a live app-server conversation. LOCK-FREE
139
109
  // (the DispatchDecision carries lock:null). Carries the decider-probed VOLATILE route so
140
110
  // the executor sends without re-deriving it (봉인 3 "used within the same dispatch"); the
@@ -154,9 +124,9 @@ export type ExecutionPlan =
154
124
  // ── DispatchDecision (the decider's only output) ────────────────────────────
155
125
  // reject ⇒ NO plan AND NO retained lock (any acquired lock was released before
156
126
  // return). execute ⇒ a plan + a receipt whose transport matches plan.transport;
157
- // `lock` is non-null for an in-domain execute (control-socket send OR spawn-bg
158
- // resume — both keep the claim so 5c's at-most-once re-resolve runs under the same
159
- // nonce) and null for the lock-free meta-mailbox path (?7).
127
+ // `lock` is non-null for an in-domain execute (the control-socket send keeps the
128
+ // claim so 5c's at-most-once re-resolve runs under the same nonce) and null for the
129
+ // lock-free meta-mailbox path (?7).
160
130
  //
161
131
  // A reject's optional machine-readable diagnostic. Only `target-locked` carries one:
162
132
  // the `LockConflict` (holder pid/host/createdAt, lockPath, human detail) the lock
@@ -218,10 +188,6 @@ export interface DispatchDeciderDeps {
218
188
  inspectSocket: (gardenId: string) => Promise<TargetSocketInspection>;
219
189
  probeSocket: (socketPath: string) => Promise<SocketLiveness>;
220
190
  // MaybePromise (0.12.1 B-2): production lazy-imports the pi-coding-agent-backed
221
- // preflight via `await import()` so the harness-neutral bridge boots pi-free;
222
- // only the owned-outcome resume branch (below) awaits it. Sync test fakes that
223
- // return a plain PreflightOutcome still satisfy this.
224
- preflightForCwd: (cwd: string) => PreflightOutcome | Promise<PreflightOutcome>;
225
191
  /**
226
192
  * SE-2 slice 2d-3: the REQUIRED mailbox-deliverability seam (no default). The decider
227
193
  * does NOT judge deliverability itself — it asks this injected fn, which combines the
@@ -246,7 +212,6 @@ export interface DispatchDeciderDeps {
246
212
  nativePushProbe: (identity: MetaIdentity) => NativePushProbeResult | Promise<NativePushProbeResult>;
247
213
  mailboxDir?: string;
248
214
  sessionsDir?: string;
249
- observeTimeoutMs?: number;
250
215
  }
251
216
 
252
217
  /**
@@ -290,10 +255,9 @@ export function resolveMailboxWakeModeCapability(
290
255
  export async function decideDispatch(input: DispatchInput, deps: DispatchDeciderDeps): Promise<DispatchDecision> {
291
256
  const mailboxDir = deps.mailboxDir ?? defaultMetaMailboxDir();
292
257
  const sessionsDir = deps.sessionsDir ?? defaultMetaSessionsDir();
293
- const observeTimeoutMs = deps.observeTimeoutMs ?? ENTWURF_V2_OBSERVE_TIMEOUT_MS;
294
258
  const mode: EntwurfV2Mode = input.mode ?? ENTWURF_V2_MODE_DEFAULT;
295
259
  const wantsReply = input.wantsReply ?? false;
296
- const ctx: InDomainCtx = { mode, wantsReply, observeTimeoutMs };
260
+ const ctx: InDomainCtx = { mode, wantsReply };
297
261
 
298
262
  const reject = (receipt: RejectReceipt, diagnostic?: RejectDiagnostic): DispatchDecision =>
299
263
  diagnostic ? { kind: "reject", receipt, diagnostic } : { kind: "reject", receipt };
@@ -378,7 +342,7 @@ export async function decideDispatch(input: DispatchInput, deps: DispatchDecider
378
342
  }
379
343
 
380
344
  // 4-5. control-socket domain (currently backend pi): lock → inspect → route.
381
- return decideInDomain(gardenId, input, deps, ctx, identity.cwd);
345
+ return decideInDomain(gardenId, input, deps, ctx);
382
346
  }
383
347
 
384
348
  // ── control-socket-domain probe (steps 4-5; currently backend pi). Every target
@@ -386,17 +350,14 @@ export async function decideDispatch(input: DispatchInput, deps: DispatchDecider
386
350
  // The lock lifecycle (B2) lives here: acquire BEFORE lstat/connect, every reject path
387
351
  // releases explicitly (rejectAfterRelease), every execute path that keeps the lock sets
388
352
  // retainLock=true, and a thrown IO error releases the still-held lock before rethrowing so
389
- // the long-lived MCP bridge never pins a gid. `cwd` comes from the meta-record — the only
390
- // resume authority so the resume verdict's preflight+spawn always launch into a
391
- // record-owned working directory.
392
- type InDomainCtx = { mode: EntwurfV2Mode; wantsReply: boolean; observeTimeoutMs: number };
353
+ // the long-lived MCP bridge never pins a gid.
354
+ type InDomainCtx = { mode: EntwurfV2Mode; wantsReply: boolean };
393
355
 
394
356
  async function decideInDomain(
395
357
  gardenId: string,
396
358
  input: DispatchInput,
397
359
  deps: DispatchDeciderDeps,
398
360
  ctx: InDomainCtx,
399
- cwd: string,
400
361
  ): Promise<DispatchDecision> {
401
362
  const { acquireLock, releaseLock, inspectSocket, probeSocket } = deps;
402
363
 
@@ -431,37 +392,15 @@ async function decideInDomain(
431
392
 
432
393
  const receipt = resolveDispatch(input.intent, liveness, false);
433
394
  if (!receipt.ok) {
434
- // resolver reject (owned-live-no-autosend / indeterminate-no-spawn / …) — the
435
- // lock was for an in-domain probe that yielded no execute, so release it.
395
+ // resolver reject (dormant-fire-forget-unsupported / indeterminate-no-spawn) —
396
+ // the lock was for an in-domain probe that yielded no execute, so release it.
436
397
  return rejectAfterRelease(receipt);
437
398
  }
438
399
 
439
- if (receipt.action === "resume") {
440
- // 1B: preflight runs ONLY here (the sole branch that launches a child into a
441
- // target cwd). deny nonce-owned release untrusted-fail-fast, with the
442
- // honest measured liveness (dormant = the `dead` we just probed).
443
- const outcome = await deps.preflightForCwd(cwd);
444
- if (outcome.kind === "deny") {
445
- return rejectAfterRelease(makeRejectReceipt("untrusted-fail-fast", liveness));
446
- }
447
- const plan: ExecutionPlan = {
448
- transport: "spawn-bg",
449
- action: "resume",
450
- targetGardenId: gardenId,
451
- sessionId: gardenId, // D3: gid is the pi resume authority, not nativeSessionId.
452
- cwd,
453
- prompt: input.message,
454
- wantsReply: ctx.wantsReply,
455
- launchArgs: outcome.launchArgs,
456
- expectedSocketPath: socketPath,
457
- observeTimeoutMs: ctx.observeTimeoutMs,
458
- releaseWhen: "socket-alive-or-child-exited",
459
- };
460
- retainLock = true;
461
- return { kind: "execute", receipt, plan, lock };
462
- }
463
-
464
- // receipt.action === "send" → control-socket send (lock kept for 5c re-resolve).
400
+ // `send` is the only action this table can return since the visible-first cut, so
401
+ // there is exactly one execute shape here control-socket send (lock kept for the
402
+ // 5c re-resolve). The resume branch that used to sit above this — preflight the
403
+ // target cwd, then plan a detached spawn-bg child is gone with the transport.
465
404
  const plan: ExecutionPlan = {
466
405
  transport: "control-socket",
467
406
  action: "send",
@@ -1,12 +1,21 @@
1
1
  /**
2
2
  * entwurf-v2-lock — the per-gid dispatch lock primitive (0.11 Stage 0 step 5a,
3
- * 버킷 B F2). LOAD-BEARING: the guard against a double-spawn of the same dormant
4
- * target by two V2 dispatchers that share the substrate through different entry
5
- * points. SCOPE (honest): this protects v2/v2 only. It was written while the legacy
6
- * `entwurf_resume` still ran unchanged (동결결정 10 scope A) without taking this
7
- * lock, which left a v2/legacy concurrent-resume gap. That verb was REMOVED in the
8
- * 0.12 cutover, so the gap is closed by subtraction not by this lock growing to
9
- * cover it. Any NEW resume entry point must take this lock or the gap reopens.
3
+ * 버킷 B F2). LOAD-BEARING, and its SHIPPED ROLE HAS NARROWED: today it serializes
4
+ * concurrent in-domain dispatch at one garden id, so two V2 dispatchers entering
5
+ * the substrate through different entry points cannot interleave against the same
6
+ * control socket.
7
+ *
8
+ * It was BORN as a double-spawn guard: the dormant cell's `spawn-bg` resume ran
9
+ * under this same lock, and "two dispatchers resume one citizen twice" was the
10
+ * failure it was designed against. That transport was withdrawn under the
11
+ * visible-first rule, so no shipped path can double-launch anything — the
12
+ * mechanism below is unchanged, the threat model it currently answers is smaller.
13
+ * Read every "double-spawn" note in this file as the ORIGINAL motivation, kept
14
+ * because it explains why the reclaim rules are as strict as they are.
15
+ *
16
+ * SCOPE (honest): this protects v2/v2 only. Any NEW resume entry point — the
17
+ * visible same-id resume, when it lands — MUST take this lock, or the original
18
+ * concurrent-resume gap reopens the day a relaunch path exists again.
10
19
  *
11
20
  * ENVIRONMENT ASSUMPTION (stale reclaim): `hostname` equality is used as the
12
21
  * proxy for "same machine", so a holder pid is reclaim-probed with kill(0) only
@@ -8,10 +8,10 @@
8
8
  *
9
9
  * Three invariants this factory exists to guarantee (each gate-proven):
10
10
  * - ONE lock domain (Q2/QB3). A single `release` closure bound to `lockDir` is the lock
11
- * authority for EVERY hand: the decider's `releaseLock`, the control-send hand's
12
- * `releaseLock`, AND the spawn watcher's `releaseFn`. The spawn factory's default
13
- * `releaseFn` ignores `lockDir` (it would release into the DEFAULT lock dir) passing
14
- * `release` explicitly is what closes that split-brain.
11
+ * authority for EVERY hand: the decider's `releaseLock` and the control-send hand's
12
+ * `releaseLock`. (A third hand used to share it — the spawn watcher's `releaseFn`,
13
+ * whose own default would have released into the DEFAULT lock dir; that split-brain
14
+ * went with the transport, the single-authority rule did not.)
15
15
  * - ONE mailbox sender (Q3). A single `makeProductionSendViaMailbox` instance is the
16
16
  * top-level meta-mailbox hand AND the control-send dead-fallback's mailbox enqueue, so a
17
17
  * direct send and a fallback send never drift in sender envelope / dirs.
@@ -30,7 +30,6 @@
30
30
  */
31
31
 
32
32
  import {
33
- formatSenderInfoBlock,
34
33
  type RpcClientOptions,
35
34
  type RpcCommand,
36
35
  type RpcResponse,
@@ -43,12 +42,6 @@ import {
43
42
  receiverMarkerMatchesIdentity,
44
43
  } from "./entwurf-deliverability.ts";
45
44
  import { isOutOfSocketDomainGardenIdConflict } from "./entwurf-facts.ts";
46
- // 0.12.1 B-2: TYPE-ONLY import — `entwurf-preflight.ts` value-imports
47
- // `@earendil-works/pi-coding-agent` (ProjectTrustStore), so a static value-import
48
- // here would re-couple the harness-neutral MCP bridge to pi at boot
49
- // (check-entwurf-bridge-pi-free). The real preflight is reached ONLY via the lazy
50
- // `await import()` in `lazyProductionPreflight`, on the owned-outcome resume branch.
51
- import type { PreflightInput, PreflightOutcome } from "./entwurf-preflight.ts";
52
45
  import { isLivenessSupported } from "./entwurf-v2-contract.ts";
53
46
  import {
54
47
  type DispatchDeciderDeps,
@@ -74,9 +67,6 @@ import {
74
67
  type RpcSendResult,
75
68
  } from "./entwurf-v2-send.ts";
76
69
  import { resolveDeadControlSendFallback } from "./entwurf-v2-send-fallback.ts";
77
- import type { SpawnBgPlan } from "./entwurf-v2-spawn.ts";
78
- import { executeSpawnBgResume } from "./entwurf-v2-spawn.ts";
79
- import { makeProductionSpawnBgResumeDeps, type ProductionSpawnOpts } from "./entwurf-v2-spawn-production.ts";
80
70
  import {
81
71
  defaultMetaMailboxDir,
82
72
  defaultMetaSessionsDir,
@@ -127,10 +117,6 @@ export interface ProductionEntwurfV2Seams {
127
117
  releaseLock: (claim: LockClaim, deps: { dir?: string }) => unknown;
128
118
  inspectSocket: (gid: string, dir: string) => Promise<TargetSocketInspection>;
129
119
  probeSocket: (socketPath: string) => Promise<SocketLiveness>;
130
- // MaybePromise (0.12.1 B-2): the production default is the lazy wrapper, which
131
- // `await import()`s the pi-coding-agent-backed preflight only on a resume verdict.
132
- // A deterministic gate may still inject a sync spy returning a plain PreflightOutcome.
133
- preflight: (input: PreflightInput) => PreflightOutcome | Promise<PreflightOutcome>;
134
120
  classifyConnect: (code: string | undefined) => "dead" | "indeterminate";
135
121
  sendRpc: (socketPath: string, command: RpcCommand, options?: RpcClientOptions) => Promise<{ response: RpcResponse }>;
136
122
  enqueue: (opts: EnqueueMetaMessageOptions) => EnqueueMetaMessageResult;
@@ -139,9 +125,6 @@ export interface ProductionEntwurfV2Seams {
139
125
  * fake adapter drives both the probe (routing decision) and the send (delivery + retry).
140
126
  * Default: the real registry resolver. */
141
127
  resolveNativePushAdapter: (backend: string) => NativePushAdapter;
142
- /** Extra spawn-factory overrides (timers/spawnChild/probe) for a deterministic spawn gate.
143
- * `releaseFn` is NOT overridable here — the factory injects the shared `release` (QB3). */
144
- spawnOverrides: Omit<ProductionSpawnOpts, "releaseFn">;
145
128
  }
146
129
 
147
130
  export interface ProductionEntwurfV2Opts {
@@ -150,44 +133,14 @@ export interface ProductionEntwurfV2Opts {
150
133
  * control socket actually exists, not a hardcoded true). ONE provider feeds the
151
134
  * control-socket RPC sender AND the meta-mailbox body sender (they share the envelope). */
152
135
  senderProvider: () => SenderEnvelope | undefined;
153
- /** pi agent dir holding `trust.json` (preflight). Omit → preflight's own default. */
154
- agentDir?: string;
155
- /** Operator-policy auto-approve roots (preflight prefix promotion). No package default. */
156
- prefixRoots?: readonly string[];
157
136
  lockDir?: string;
158
137
  sessionsDir?: string;
159
138
  mailboxDir?: string;
160
139
  controlSocketDir?: string;
161
- observeTimeoutMs?: number;
162
- killGraceMs?: number;
163
140
  /** Gate/smoke seam overrides — defaults are the real IO. */
164
141
  seams?: Partial<ProductionEntwurfV2Seams>;
165
142
  }
166
143
 
167
- /**
168
- * 0.12.1 B-2: the production `preflight` seam default. preflight value-imports
169
- * `@earendil-works/pi-coding-agent` (ProjectTrustStore), so importing it eagerly
170
- * would pull pi into the harness-neutral MCP bridge's boot closure. This wrapper
171
- * defers that to a lazy `await import()` reached ONLY on the owned-outcome resume
172
- * branch (the decider awaits it). peers/self/list/mailbox-deliver therefore boot
173
- * with no pi package present.
174
- *
175
- * WHAT THIS DEFERRAL DOES NOT DO — corrected 0.12.8. The old sentence framed the
176
- * pi-less case as an edge ("a pi-less environment that DOES hit a spawn-bg resume
177
- * surfaces an honest module-not-found"). In a published consumer tree that is not an
178
- * edge, it is the DEFAULT: `@earendil-works/pi-coding-agent` is an optional peer that
179
- * a neutral `npm install` does not resolve (entwurf-preflight.ts:51), so this import
180
- * throws for every installed user and the owned-outcome lane has never lived in any
181
- * published version. The deferral still buys what it claims — a pi-free boot — and
182
- * "module-not-found" is still honest, but it is a permanent floor on the install path,
183
- * not a rare condition. Reviving that lane (declared dep / pi CLI subprocess / PATH
184
- * resolution) is a separate decision; nothing here should read as if it works today.
185
- */
186
- async function lazyProductionPreflight(input: PreflightInput): Promise<PreflightOutcome> {
187
- const { preflight } = await import("./entwurf-preflight.ts");
188
- return preflight(input);
189
- }
190
-
191
144
  /** Map a record-side socket inspection to the singleton (socketGids, symlinkedGids) the
192
145
  * `isOutOfSocketDomainGardenIdConflict` predicate consumes. `indeterminate` fails LOUD (QB2): an
193
146
  * unprovable conflict must NOT be folded to "no conflict" — that would silently allow an
@@ -235,12 +188,10 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
235
188
  releaseLock: s.releaseLock ?? realReleaseLock,
236
189
  inspectSocket: s.inspectSocket ?? inspectTargetControlSocket,
237
190
  probeSocket: s.probeSocket ?? probeSocketLiveness,
238
- preflight: s.preflight ?? lazyProductionPreflight,
239
191
  classifyConnect: s.classifyConnect ?? classifyConnectError,
240
192
  sendRpc: s.sendRpc ?? realSendRpc,
241
193
  enqueue: s.enqueue ?? enqueueMetaMessage,
242
194
  resolveNativePushAdapter: s.resolveNativePushAdapter ?? realResolveNativePushAdapter,
243
- spawnOverrides: s.spawnOverrides ?? {},
244
195
  };
245
196
 
246
197
  // ── ONE lock domain (Q2/QB3): a single lockDir-bound release for ALL hands ─
@@ -324,8 +275,6 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
324
275
  releaseLock: release,
325
276
  inspectSocket,
326
277
  probeSocket,
327
- preflightForCwd: (cwd: string): PreflightOutcome | Promise<PreflightOutcome> =>
328
- io.preflight({ cwd, agentDir: opts.agentDir, prefixRoots: opts.prefixRoots }),
329
278
  mailboxDeliverabilityFor,
330
279
  // 봉인 4: resolve the native-push adapter for this backend + probe the conversation.
331
280
  // Only reached on a nativePushSupported backend (the decider gates it), so the resolver
@@ -334,7 +283,6 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
334
283
  Promise.resolve(io.resolveNativePushAdapter(identity.backend).probe(identity.nativeSessionId)),
335
284
  mailboxDir,
336
285
  sessionsDir,
337
- observeTimeoutMs: opts.observeTimeoutMs,
338
286
  };
339
287
 
340
288
  // ── control-send hand deps (5c-2): the dead-fallback shares resolveTarget /
@@ -368,28 +316,6 @@ export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): Entw
368
316
  // ── executor: the three transport hands, each pre-bound ───────────────────
369
317
  const executor: DispatchExecutorDeps = {
370
318
  sendControl: (plan, lock) => executeControlSocketSend(plan as ControlSocketPlan, lock, controlSendDeps),
371
- resumeSpawnBg: (plan, lock) => {
372
- const spawnPlan = plan as SpawnBgPlan;
373
- // Caller-edge preservation (#50 C3): the dormant rail delivers the SAME
374
- // structured <sender_info> the live socket rail synthesizes at its
375
- // receiver — one formatter (entwurf-control-rpc SSOT), appended after the
376
- // task text exactly as a live delivery would render it. Without this, a
377
- // resumed citizen woke with an anonymous prompt while every other rail
378
- // carried the sender envelope.
379
- const sender = opts.senderProvider();
380
- const prompt = sender ? spawnPlan.prompt + formatSenderInfoBlock(sender, spawnPlan.wantsReply) : spawnPlan.prompt;
381
- return executeSpawnBgResume(
382
- { ...spawnPlan, prompt },
383
- lock,
384
- // QB3: inject the shared lockDir-bound `release` — never the spawn factory's
385
- // default releaseFn (which would release into the DEFAULT lock dir).
386
- makeProductionSpawnBgResumeDeps({
387
- ...io.spawnOverrides,
388
- killGraceMs: opts.killGraceMs ?? io.spawnOverrides.killGraceMs,
389
- releaseFn: release,
390
- }),
391
- );
392
- },
393
319
  sendMailbox: (plan, _lock) => sendViaMailbox(plan as MetaMailboxPlan, _lock as LockClaim),
394
320
  // native-push (봉인 4): the SAME injected adapter resolver drives the executor send,
395
321
  // so the decider's probe and the delivery use one adapter. Lock-free (lock ignored).
@@ -6,24 +6,25 @@
6
6
  * AT MOST ONCE.
7
7
  *
8
8
  * Why a pure reducer before any transport IO (5b discipline = gate-first →
9
- * pure-before-IO → wire): the load-bearing risk of 5c is NOT the spawn/send IO, it
10
- * is the RELEASE TIMING (Fable 3, "release-after-observation"). A spawn-bg dispatch
11
- * that releases its lock too early on spawn-started/ack instead of on an observed
12
- * liveness transition reopens the exact double-spawn window 5a's per-gid lock
13
- * exists to close. So the timing logic is isolated here as a pure state machine the
14
- * gate drives over every event order, and 5c-2/5c-3/5c-4 only WIRE the real
15
- * send/spawn/mailbox IO onto it. The hand feeds events; this module decides release.
9
+ * pure-before-IO → wire): the load-bearing risk of 5c is NOT the send IO, it is the
10
+ * RELEASE TIMING. A dispatch that releases its lock too early — on an ack instead of
11
+ * on a final outcome reopens the window 5a's per-gid lock exists to close. So the
12
+ * timing logic is isolated here as a pure state machine the gate drives over every
13
+ * event order, and 5c-2/5c-4 only WIRE the real send/mailbox IO onto it. The hand
14
+ * feeds events; this module decides release.
16
15
  *
17
- * The three policies (one per ExecutionPlan transport):
18
- * - no-lock — meta-mailbox (?7: no lock was ever held).
19
- * Nothing to release on any event.
16
+ * The two policies (one per ExecutionPlan transport):
17
+ * - no-lock — meta-mailbox and native-push (?7 / 봉인 4: no
18
+ * lock was ever held). Nothing to release on any event.
20
19
  * - release-after-send-final — control-socket send. The lock is held only for
21
20
  * the at-most-once re-resolve; release once the send reaches a FINAL outcome.
22
- * - release-after-spawn-observation — spawn-bg resume. The lock is held until the
23
- * FIRST observable transition: socket-alive OR child-exited (any code). A
24
- * spawn-started event is explicitly NOT a release trigger (that is the whole
25
- * point). A spawn that never started (spawn-start-failed) has no child to watch,
26
- * so it releases to free the gidkeeping the lock would pin it forever.
21
+ *
22
+ * There was a third, `release-after-spawn-observation`, for the spawn-bg resume: hold
23
+ * the lock until an OBSERVED liveness transition (socket-alive / child-exited), never
24
+ * on spawn-started. It went with that transport in the visible-first cut, and with it
25
+ * the spawn event vocabulary. The rule it encoded a launch is finalized by an
26
+ * observation, not by having started — is the one a visible resume will have to
27
+ * restate for itself; it is not inherited by anything shipped here.
27
28
  *
28
29
  * The release authority is ALWAYS the LockClaim the decider handed over (5c wires it
29
30
  * through). This module never re-derives a lock by gid, never re-resolves a path —
@@ -34,24 +35,15 @@ import type { ExecutionPlan } from "./entwurf-v2-decider.ts";
34
35
  import type { LockClaim } from "./entwurf-v2-lock.ts";
35
36
 
36
37
  // ── ReleasePolicy (derived once from the plan + held lock) ───────────────────
37
- export type ReleasePolicy =
38
- | { kind: "no-lock" }
39
- | { kind: "release-after-send-final" }
40
- | { kind: "release-after-spawn-observation" };
38
+ export type ReleasePolicy = { kind: "no-lock" } | { kind: "release-after-send-final" };
41
39
 
42
40
  // ── ReleaseEvent (the transport hand feeds these as execution proceeds) ──────
43
41
  // `send-final` carries the terminal send outcome (legacy parity: a fallback-sent is
44
- // a real final outcome, not a hidden retry). `child-exited` carries the code (null =
45
- // killed by signal) but the policy releases on ANY code. `mailbox-enqueued` is the
46
- // terminal ack on the lock-free path. `spawn-started`/`spawn-start-failed` bracket
47
- // the spawn attempt — only the FAILURE finalizes; a successful start must wait for an
48
- // observed transition.
42
+ // a real final outcome, not a hidden retry). `mailbox-enqueued` is the terminal ack on
43
+ // the lock-free path. The four spawn/observation events (spawn-started,
44
+ // spawn-start-failed, socket-alive, child-exited) went with the spawn-bg transport.
49
45
  export type ReleaseEvent =
50
46
  | { kind: "send-final"; outcome: "sent" | "fallback-sent" | "rejected" | "failed" }
51
- | { kind: "spawn-started"; pid: number }
52
- | { kind: "spawn-start-failed"; error: string }
53
- | { kind: "socket-alive" }
54
- | { kind: "child-exited"; code: number | null }
55
47
  | { kind: "mailbox-enqueued" };
56
48
 
57
49
  // ── ReleaseState (single-release accumulator) ───────────────────────────────
@@ -64,7 +56,7 @@ export function initialReleaseState(): ReleaseState {
64
56
  }
65
57
 
66
58
  /**
67
- * An in-domain execute (control-socket / spawn-bg) must hold a lock whose gardenId
59
+ * An in-domain execute (control-socket) must hold a lock whose gardenId
68
60
  * IS the plan's target. Both halves are 5b decider-contract invariants, not runtime
69
61
  * conditions: a null lock or a lock paired with the WRONG plan are the same grade of
70
62
  * mis-wiring (a later release would free nothing, or free a DIFFERENT gid's lock), so
@@ -85,7 +77,7 @@ function assertInDomainLock(plan: ExecutionPlan, lock: LockClaim | null): assert
85
77
  /**
86
78
  * Derive the release policy from the plan, cross-checking the lock invariants the 5b
87
79
  * decider guarantees: meta-mailbox ⇒ lock null (?7); in-domain (control-socket /
88
- * spawn-bg) ⇒ lock non-null AND lock.gardenId === plan.targetGardenId. A mismatch is
80
+ * control-socket) ⇒ lock non-null AND lock.gardenId === plan.targetGardenId. A mismatch is
89
81
  * a decider contract violation, not a runtime condition — it throws so a mis-wired
90
82
  * hand fails loud instead of silently dropping or mis-releasing a lock.
91
83
  */
@@ -108,9 +100,6 @@ export function decideReleasePolicy(plan: ExecutionPlan, lock: LockClaim | null)
108
100
  case "control-socket":
109
101
  assertInDomainLock(plan, lock);
110
102
  return { kind: "release-after-send-final" };
111
- case "spawn-bg":
112
- assertInDomainLock(plan, lock);
113
- return { kind: "release-after-spawn-observation" };
114
103
  }
115
104
  }
116
105
 
@@ -123,8 +112,9 @@ export interface ReduceReleaseResult {
123
112
  * Fold one execution event into the release decision. `shouldRelease` is true
124
113
  * EXACTLY ONCE — on the first event that satisfies the policy's release condition;
125
114
  * every later event (after `state.released`) returns false. The single-release
126
- * guarantee is what lets the spawn watcher race socket-alive against child-exit
127
- * without a double release: whichever fires first releases, the other is a no-op.
115
+ * guarantee is what let the retired spawn watcher race two observations without a
116
+ * double release, and it is why the control-socket hand can feed a final outcome more
117
+ * than once without paying for it twice.
128
118
  */
129
119
  export function reduceRelease(policy: ReleasePolicy, state: ReleaseState, event: ReleaseEvent): ReduceReleaseResult {
130
120
  if (state.released) {
@@ -140,19 +130,5 @@ export function reduceRelease(policy: ReleasePolicy, state: ReleaseState, event:
140
130
  case "release-after-send-final":
141
131
  // Release once the send reaches a final outcome; hold before that.
142
132
  return event.kind === "send-final" ? release() : hold();
143
- case "release-after-spawn-observation":
144
- // Fable 3: spawn-started is NOT a release event. Release on the first
145
- // observed transition (socket-alive / child-exited, any code), or on a
146
- // failed start (no child to watch).
147
- switch (event.kind) {
148
- case "socket-alive":
149
- case "child-exited":
150
- case "spawn-start-failed":
151
- return release();
152
- case "spawn-started":
153
- case "send-final":
154
- case "mailbox-enqueued":
155
- return hold();
156
- }
157
133
  }
158
134
  }
@@ -6,9 +6,9 @@
6
6
  * to the right 5c transport hand, returning ONE outcome-rich `EntwurfV2RunResult`. It performs
7
7
  * ZERO IO of its own and makes ZERO routing decisions — `decideDispatch` chose the plan,
8
8
  * this only DISPATCHES it. Each hand is an injected dep (the gate fakes them; 5d-2 wires
9
- * the production `executeControlSocketSend` / `executeSpawnBgResume` / production
10
- * `sendViaMailbox`), so the routing + result mapping is gate-provable without a socket,
11
- * spawn, or timer — the same pure-before-IO, IO-via-dep discipline 5b/5c kept.
9
+ * the production `executeControlSocketSend` / `sendViaMailbox` / native-push sender), so
10
+ * the routing + result mapping is gate-provable without a socket — the same
11
+ * pure-before-IO, IO-via-dep discipline 5b/5c kept.
12
12
  *
13
13
  * Why a result type richer than the receipt: the carry-over contracts from 5c demand it.
14
14
  * - N3 (5c-2b): a dead-path re-resolve `rejected` carries the resolver's `rejectReason`
@@ -19,8 +19,6 @@
19
19
  * re-send would double-deliver. The runner surfaces this as `execution-failed` with
20
20
  * `finalizedOutcome` + `releaseFailed` so the surface renders "delivered, lock dirty,
21
21
  * do NOT retry", never "send failed".
22
- * - spawn-bg `lock-retained` is a RETURNED result (fail-closed, not a throw): it rides
23
- * the `executed` branch so 5d's surface can render the retained-lock diagnostic.
24
22
  *
25
23
  * `retrySafe` is conservatively `false` on EVERY `execution-failed`: a thrown send is
26
24
  * never confidently retry-safe (an `indeterminate` connect may have delivered to an
@@ -47,17 +45,15 @@ import {
47
45
  SendDeliveredReleaseFailedError,
48
46
  type SendFinalOutcome,
49
47
  } from "./entwurf-v2-send.ts";
50
- import type { SpawnBgPlan, SpawnBgResumeResult } from "./entwurf-v2-spawn.ts";
51
48
 
52
49
  /**
53
50
  * The three transport hands, each PRE-BOUND with its own deps (production or fake). Lock
54
51
  * is typed `LockClaim | null` to mirror the real hands EXACTLY — the runner passes
55
52
  * `decision.lock` straight through and the hand fails loud on a null/mis-paired lock
56
- * (control-socket / spawn-bg get a non-null claim; meta-mailbox gets null — ?7).
53
+ * (control-socket gets a non-null claim; meta-mailbox and native-push get null — ?7 / 봉인 4).
57
54
  */
58
55
  export interface DispatchExecutorDeps {
59
56
  sendControl: (plan: ControlSocketPlan, lock: LockClaim | null) => Promise<ControlSocketSendResult>;
60
- resumeSpawnBg: (plan: SpawnBgPlan, lock: LockClaim | null) => Promise<SpawnBgResumeResult>;
61
57
  sendMailbox: (plan: MetaMailboxPlan, lock: LockClaim | null) => Promise<RpcSendResult>;
62
58
  // native-push (봉인 4): lock-free like meta-mailbox — the runner passes the null lock
63
59
  // verbatim and the hand ignores it. Owns the 1-shot re-probe→re-send retry internally.
@@ -66,12 +62,10 @@ export interface DispatchExecutorDeps {
66
62
 
67
63
  /** The per-transport success outcome, discriminated by transport so the surface renders
68
64
  * each without guessing. `control-socket` carries the optional N3 `rejectReason`;
69
- * `spawn-bg` carries the whole `SpawnBgResumeResult` (incl. the `lock-retained`
70
- * fail-closed diagnostic); `meta-mailbox` is always `success:true` (enqueue has no
71
- * in-band refuse — a failure is a throw, handled as `execution-failed`). */
65
+ * `meta-mailbox` is always `success:true` (enqueue has no in-band refuse — a failure is
66
+ * a throw, handled as `execution-failed`). */
72
67
  export type ExecutedOutcome =
73
68
  | { transport: "control-socket"; outcome: SendFinalOutcome; rejectReason?: string }
74
- | { transport: "spawn-bg"; result: SpawnBgResumeResult }
75
69
  | { transport: "meta-mailbox"; success: true }
76
70
  // native-push carries `retried` so the surface can note the 1-shot re-probe retry fired.
77
71
  | { transport: "native-push"; success: true; retried: boolean };
@@ -143,15 +137,6 @@ export async function executeDispatch(
143
137
  return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
144
138
  }
145
139
  }
146
- case "spawn-bg": {
147
- try {
148
- // `lock-retained` is a RETURNED result (fail-closed), not a throw — it rides `executed`.
149
- const result = await deps.resumeSpawnBg(plan, lock);
150
- return { kind: "executed", receipt, transport, outcome: { transport: "spawn-bg", result } };
151
- } catch (err) {
152
- return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
153
- }
154
- }
155
140
  case "meta-mailbox": {
156
141
  try {
157
142
  // lock is null here (?7) — passed verbatim; the production adapter ignores it.