@junghanacs/entwurf 0.13.0 → 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.
- package/AGENTS.md +52 -17
- package/BASELINE.md +60 -206
- package/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -10
- package/DELIVERY.md +118 -261
- package/README.md +91 -431
- package/VERIFY.md +74 -95
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +236 -0
- package/docs/external-mcp-host.md +131 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +124 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
- package/mcp/entwurf-bridge/src/index.ts +176 -67
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +25 -11
- package/pi-extensions/entwurf-control.ts +220 -71
- package/pi-extensions/lib/acp/acp-client.ts +22 -0
- package/pi-extensions/lib/acp/augment.ts +42 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
- package/pi-extensions/lib/acp/backend.ts +366 -38
- package/pi-extensions/lib/acp/config.ts +1 -1
- package/pi-extensions/lib/acp/engraving.ts +43 -1
- package/pi-extensions/lib/acp/event-mapper.ts +16 -13
- package/pi-extensions/lib/acp/models.ts +4 -4
- package/pi-extensions/lib/acp/overlay.ts +11 -9
- package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
- package/pi-extensions/lib/entwurf-core.ts +15 -15
- package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
- package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
- package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
- package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
- package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
- package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
- package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
- package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
- package/pi-extensions/lib/meta-session.ts +93 -5
- package/pi-extensions/lib/mux-fresh-call.ts +328 -0
- package/pi-extensions/lib/mux-launch.ts +267 -0
- package/pi-extensions/lib/mux-placement.ts +387 -0
- package/pi-extensions/lib/mux-resume-call.ts +221 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +688 -282
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +198 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +101 -7
- package/scripts/check-install-container.sh +10 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-keyset-overlap.py +1 -1
- package/scripts/check-meta-facts.ts +249 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-meta-session.ts +169 -0
- package/scripts/check-mux-launch-tmux.ts +316 -0
- package/scripts/check-mux-launch.ts +288 -0
- package/scripts/check-mux-launcher-fence.ts +264 -0
- package/scripts/check-mux-parent-artifact.ts +195 -0
- package/scripts/check-mux-placement-tmux.ts +322 -0
- package/scripts/check-mux-placement.ts +323 -0
- package/scripts/check-mux-resume-call.ts +282 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +121 -81
- package/scripts/check-release-gate-outcomes.ts +452 -0
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +185 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +6 -1021
- package/scripts/mutants/release-gate.json +140 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +74 -23
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
- package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +8 -14
- package/scripts/smoke-acp-long-turn-live.ts +185 -0
- package/scripts/smoke-acp-mcp-live.ts +2 -2
- package/scripts/smoke-acp-memory-containment-live.ts +2 -2
- package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
- package/scripts/smoke-acp-overlay-live.ts +2 -2
- package/scripts/smoke-acp-provider-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +3 -3
- package/scripts/smoke-acp-session-reuse-live.ts +2 -2
- package/scripts/smoke-acp-skill-live.ts +2 -2
- package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
- package/scripts/smoke-acp-v2-send-live.ts +4 -3
- package/scripts/smoke-agy-native-push-live.ts +10 -20
- package/scripts/smoke-claude-native-resume-live.sh +13 -3
- package/scripts/smoke-entwurf-chain-live.ts +352 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +4 -0
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -236
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
|
@@ -17,20 +17,43 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Source-verified invariants folded in (Opus 실측 + GPT 보정 + Fable R1-R5, 2026-06-11):
|
|
19
19
|
* - F1: caller intent is DECLARED in the input, so the contract a caller
|
|
20
|
-
* receives is deterministic — never computed from liveness at call time.
|
|
21
|
-
*
|
|
20
|
+
* receives is deterministic — never computed from liveness at call time. One
|
|
21
|
+
* intent remains: `fire-and-forget` (the ack is the end of the contract). The
|
|
22
|
+
* second intent, `owned-outcome`, is GONE — see the visible-first note below.
|
|
23
|
+
*
|
|
24
|
+
* ── Visible-first subtraction (2026-08-06, GLG) ────────────────────────────
|
|
25
|
+
* `owned-outcome` and the `spawn-bg` transport are removed from this contract, not
|
|
26
|
+
* deprecated. The only resume this verb ever had launched a DETACHED, window-less
|
|
27
|
+
* `pi` child; under the visible-first operating rule a citizen the operator cannot
|
|
28
|
+
* see must not be started at all, and a reachable hidden path is worse than a
|
|
29
|
+
* temporary absence of the capability. So the subtraction is a hard cut: the intent
|
|
30
|
+
* leaves the enum, the transport leaves the table, and an old caller fails schema
|
|
31
|
+
* validation rather than meeting a polite reject string.
|
|
32
|
+
*
|
|
33
|
+
* What this leaves behind, deliberately: a DORMANT in-domain citizen is currently
|
|
34
|
+
* unreachable (`fire-and-forget` × dormant has always rejected). Visible same-id
|
|
35
|
+
* resume is a SEPARATE lifecycle capability — a distinct verb over the mux launch
|
|
36
|
+
* lane, not an intent on this delivery verb — and it is not implemented here. The
|
|
37
|
+
* leaves it will need (record-authoritative launch identity, the resume argv
|
|
38
|
+
* builder, the per-target lock, socket inspection) are preserved and still shipped.
|
|
39
|
+
*
|
|
40
|
+
* READ THE SCOPE EXACTLY. Background execution is not judged impossible and is not forbidden
|
|
41
|
+
* forever. It is retired NOW so that no hidden bypass exists while visible-first governance is
|
|
42
|
+
* being established. A future background lifecycle may be added back as an explicitly
|
|
43
|
+
* authorized, FIRST-CLASS capability with its own verb and its own receipts. What it must never
|
|
44
|
+
* be again is what it was here: an automatic fallback, or a hidden branch inside visible resume
|
|
45
|
+
* or `entwurf_v2`. The shape is what was removed, not the idea of a detached process.
|
|
22
46
|
* - R1: liveness is defined PER CAPABILITY DOMAIN. The control-socket domain
|
|
23
47
|
* currently contains backend `pi`; claude-code is self-fetch with no socket,
|
|
24
48
|
* so its socket liveness is `unsupported`, NOT folded into dead/indeterminate.
|
|
25
49
|
* This is transport capability, not identity rank: every target is first a
|
|
26
50
|
* record citizen. `unsupported` is a 4th FACT value, not a 4th table column.
|
|
27
51
|
* - R2: `target` is the garden-id of an EXISTING citizen. spawn-new is out of
|
|
28
|
-
* v2 scope —
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `dormant` target is "reject for now" (mailbox-wake lacks a reply-correlation
|
|
52
|
+
* v2 scope — fresh creation now has the separate `entwurf_fresh_call` surface,
|
|
53
|
+
* never a fallback inside this resolver. Absent/typo gid = `bad-target` (so F6
|
|
54
|
+
* "오타 gid가 신규 spawn 사고 막기" holds automatically).
|
|
55
|
+
* - N1/F3: an `indeterminate` target is never launched into. N2: `fire-and-forget`
|
|
56
|
+
* to a `dormant` target is "reject for now" (mailbox-wake lacks a reply-correlation
|
|
34
57
|
* id in the substrate; an additive extension later, not a permanent no).
|
|
35
58
|
* - Q2: every cell is a SINGLE verdict — no "default", no escape hatch (a
|
|
36
59
|
* "default reject" would re-admit the call-time nondeterminism F1 closes).
|
|
@@ -38,8 +61,7 @@
|
|
|
38
61
|
* is NOT a reject — the 0.10.0 meta-bridge mailbox delivers without liveness.
|
|
39
62
|
* `unsupported` is the "no liveness predicate" fact, not a delivery verdict; so
|
|
40
63
|
* ff+unsupported routes to the `meta-mailbox` transport, gated by a SEPARATE
|
|
41
|
-
* `mailboxDeliverable` fact (NOT a column of the
|
|
42
|
-
* owned-outcome+unsupported still rejects (self-fetch needs real liveness).
|
|
64
|
+
* `mailboxDeliverable` fact (NOT a column of the 3-cell table — Fable (i)).
|
|
43
65
|
*
|
|
44
66
|
* The decision table here is a constant; `check-entwurf-v2-contract` asserts it
|
|
45
67
|
* exhaustively + proves the "table cell ↔ receipt" round-trip. THAT round-trip
|
|
@@ -51,9 +73,15 @@ import type { SocketLiveness } from "./socket-probe.ts";
|
|
|
51
73
|
|
|
52
74
|
// ── Caller-declared intent (F1) ────────────────────────────────────────────
|
|
53
75
|
// The outcome contract is an INPUT, not an inference. `fire-and-forget` = the
|
|
54
|
-
// RPC ack is the end of the contract (entwurf-control.ts:29-37).
|
|
55
|
-
//
|
|
56
|
-
|
|
76
|
+
// RPC ack is the end of the contract (entwurf-control.ts:29-37).
|
|
77
|
+
//
|
|
78
|
+
// This enum has ONE member on purpose. It kept a second — `owned-outcome`, whose
|
|
79
|
+
// only allow cell launched a hidden detached child — until the visible-first cut
|
|
80
|
+
// (header). A one-member enum is not a smell here: the axis is real (a caller
|
|
81
|
+
// still declares its outcome contract), and the value that is gone is gone from
|
|
82
|
+
// the wire, so an old caller is refused by schema validation instead of being
|
|
83
|
+
// quietly re-routed.
|
|
84
|
+
export const ENTWURF_INTENTS = ["fire-and-forget"] as const;
|
|
57
85
|
export type EntwurfIntent = (typeof ENTWURF_INTENTS)[number];
|
|
58
86
|
|
|
59
87
|
// ── Liveness axes ──────────────────────────────────────────────────────────
|
|
@@ -63,9 +91,11 @@ export const FACT_LIVENESSES = ["alive", "dead", "indeterminate", "unsupported"]
|
|
|
63
91
|
export type FactLiveness = SocketLiveness | "unsupported";
|
|
64
92
|
|
|
65
93
|
// DispatchLiveness = the in-domain routing axis the table is keyed on. The
|
|
66
|
-
// socket result maps: alive→live (send), dead→dormant (
|
|
67
|
-
// indeterminate→indeterminate (never
|
|
68
|
-
// handled by the domain guard before the table
|
|
94
|
+
// socket result maps: alive→live (send), dead→dormant (unreachable since the
|
|
95
|
+
// visible-first cut), indeterminate→indeterminate (never dispatched into).
|
|
96
|
+
// `unsupported` is NOT here — it is handled by the domain guard before the table
|
|
97
|
+
// is consulted. The axis keeps all three values because they are FACTS about a
|
|
98
|
+
// target; only the verdicts behind two of them changed.
|
|
69
99
|
export const DISPATCH_LIVENESSES = ["live", "dormant", "indeterminate"] as const;
|
|
70
100
|
export type DispatchLiveness = (typeof DISPATCH_LIVENESSES)[number];
|
|
71
101
|
|
|
@@ -135,17 +165,17 @@ export function dispatchLivenessOf(socket: SocketLiveness): DispatchLiveness {
|
|
|
135
165
|
// attempted. A post-dispatch "send-fail fallback" (transport failed after the
|
|
136
166
|
// verdict) is a SEPARATE axis (bucket B) and must NOT be merged into this enum.
|
|
137
167
|
export const ENTWURF_V2_REJECT_REASONS = [
|
|
138
|
-
|
|
168
|
+
// The name is inherited vocabulary and stays a frozen wire string: with the spawn
|
|
169
|
+
// transport gone there is nothing left to spawn, but the RULE it names is intact —
|
|
170
|
+
// an in-domain target whose probe was inconclusive is never dispatched into. Renaming
|
|
171
|
+
// a public reject string is its own contract cut, not a side effect of this one.
|
|
172
|
+
"indeterminate-no-spawn", // N1/F3: never dispatch into an indeterminate target
|
|
139
173
|
"dormant-fire-forget-unsupported", // N2: fire-and-forget to a dormant target — reject for now
|
|
140
|
-
"owned-live-no-autosend", // Q2/F1: owned-outcome to a live target is not an auto-send
|
|
141
|
-
"backend-liveness-unsupported", // R1: backend has no liveness predicate (e.g. claude-code) — owned-outcome only
|
|
142
174
|
"mailbox-undeliverable", // F-mailbox: fire-and-forget to an unsupported citizen whose mailbox is not deliverable (fail-closed; future pi-backend non-drainable mailbox)
|
|
143
|
-
"native-push-target-dead", // 봉인 1: fire-and-forget to a native-push (agy) target whose adapter probe found NO live conversation. Post-probe; observedLiveness = dead.
|
|
144
|
-
"native-push-probe-indeterminate", // 봉인 1: fire-and-forget to a native-push target whose adapter probe was inconclusive (agy alive but no port served the conv, or a probe error). Post-probe; observedLiveness = indeterminate. Never
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"untrusted-fail-fast", // 동결결정 5: controlled launch into an untrusted cwd
|
|
148
|
-
"record-less-socket", // #50 C4: a gid-shaped non-symlink control socket exists but NO meta-record claims it. The record is the sole address authority (목표 ②), so a bare socket is not an addressable citizen — it is a diagnostic state, refused for EVERY intent before any lock/probe (pre-probe, observedLiveness=null). NOT `bad-target`: something real answers to this gid, and mislabeling it absent would hide the very state the reject exists to surface. Replaces the retired A1-narrow acceptance (and its post-probe `socket-only-no-resume-authority`).
|
|
175
|
+
"native-push-target-dead", // 봉인 1: fire-and-forget to a native-push (agy) target whose adapter probe found NO live conversation. Post-probe; observedLiveness = dead.
|
|
176
|
+
"native-push-probe-indeterminate", // 봉인 1: fire-and-forget to a native-push target whose adapter probe was inconclusive (agy alive but no port served the conv, or a probe error). Post-probe; observedLiveness = indeterminate. Never coerced to dead.
|
|
177
|
+
"bad-target", // R2: absent/typo garden-id (no existing citizen); fresh creation out of v2 scope
|
|
178
|
+
"record-less-socket", // #50 C4: a gid-shaped non-symlink control socket exists but NO meta-record claims it. The record is the sole address authority (목표 ②), so a bare socket is not an addressable citizen — it is a diagnostic state, refused for EVERY intent before any lock/probe (pre-probe, observedLiveness=null). NOT `bad-target`: something real answers to this gid, and mislabeling it absent would hide the very state the reject exists to surface.
|
|
149
179
|
"target-locked", // R5 pre-claim for bucket B F2 per-gid lockfile conflict
|
|
150
180
|
"target-address-conflict", // F3: a quarantined citizen (garden-id-socket-conflict / symlinked socket) — the gid resolves to two different receivers (record vs socket), so dispatch refuses to pick. The ONLY in-band honest channel for a dispatch-level identity-split (the listing diagnostic channel is not visible to a v2 caller, who only gets a receipt). Pre-resolver, like bad-target/target-locked — NOT a RESOLVER_REJECT_REASONS member.
|
|
151
181
|
] as const;
|
|
@@ -161,10 +191,11 @@ export type EntwurfV2RejectReason = (typeof ENTWURF_V2_REJECT_REASONS)[number];
|
|
|
161
191
|
// in-domain probe was inconclusive (≠ "not looked yet"); `unsupported` means the
|
|
162
192
|
// backend has no predicate (≠ "pre-probe"). So a pre-probe reject's
|
|
163
193
|
// observedLiveness is `null`, NOT one of the four values. Every OTHER reject —
|
|
164
|
-
// the RESOLVER_REJECT_REASONS (
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
194
|
+
// the RESOLVER_REJECT_REASONS (3, post-probe) and the native-push set (2, also
|
|
195
|
+
// post-probe) — carries a non-null FactLiveness, as does every success. (The
|
|
196
|
+
// visible-first cut removed the one post-probe reject that was NOT a resolver
|
|
197
|
+
// member: `untrusted-fail-fast`, which only ever fired behind a resume verdict.)
|
|
198
|
+
// This null/non-null split is REASON-DEPENDENT, so the receipt
|
|
168
199
|
// schema (which allows null on every reject branch) cannot enforce it alone — the
|
|
169
200
|
// semantic fixture in `check-entwurf-v2-contract` does, via `isPreProbeReject` /
|
|
170
201
|
// `rejectObservedLivenessWellFormed` below (the SSOT 5b mints against).
|
|
@@ -185,14 +216,9 @@ export function isPreProbeReject(reason: EntwurfV2RejectReason): reason is PrePr
|
|
|
185
216
|
* pure SSOT predicate so 5b mints against it and the gate proves it: a pre-probe
|
|
186
217
|
* reject MUST carry `null`; every other reject MUST carry a non-null FactLiveness.
|
|
187
218
|
* Catches the illegal `{ok:false, reason:"bad-target", observedLiveness:"indeterminate"}`
|
|
188
|
-
* (pre-probe with a stamped value) and `{ok:false, reason:"
|
|
219
|
+
* (pre-probe with a stamped value) and `{ok:false, reason:"dormant-fire-forget-unsupported",
|
|
189
220
|
* observedLiveness:null}` (post-probe with no value) — both reason-dependent, so
|
|
190
221
|
* unreachable by the schema's blanket `FactLiveness | null`.
|
|
191
|
-
*
|
|
192
|
-
* NOTE this predicate FREEZES the 1B ordering into the contract: classifying
|
|
193
|
-
* `untrusted-fail-fast` as post-probe (non-null required) encodes "preflight runs
|
|
194
|
-
* AFTER the probe". Moving preflight back ahead of the probe would make its
|
|
195
|
-
* observedLiveness un-measured (null) and reopen this predicate + the enum split.
|
|
196
222
|
*/
|
|
197
223
|
export function rejectObservedLivenessWellFormed(
|
|
198
224
|
reason: EntwurfV2RejectReason,
|
|
@@ -201,21 +227,17 @@ export function rejectObservedLivenessWellFormed(
|
|
|
201
227
|
return isPreProbeReject(reason) ? observedLiveness === null : observedLiveness !== null;
|
|
202
228
|
}
|
|
203
229
|
|
|
204
|
-
// Reasons the RESOLVER emits — the in-domain
|
|
205
|
-
// unsupported domain-guard mini-table (
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
// stage, not an earlier one. All three are pre-claimed in the enum so bucket B
|
|
213
|
-
// does not reopen it.
|
|
230
|
+
// Reasons the RESOLVER emits — the in-domain 3-cell table cells PLUS the
|
|
231
|
+
// unsupported domain-guard mini-table (mailbox-undeliverable for a fail-closed
|
|
232
|
+
// fire-and-forget). NOT just the table (the F-mailbox mini-table emits one of
|
|
233
|
+
// these), hence RESOLVER_ not TABLE_. The remaining taxonomy members are produced
|
|
234
|
+
// by stages BEFORE the resolver: `bad-target` (target resolution) and
|
|
235
|
+
// `target-locked` (lockfile). Both are pre-claimed in the enum so bucket B does
|
|
236
|
+
// not reopen it. There is no longer any AFTER-the-resolver reject: the one that
|
|
237
|
+
// existed, `untrusted-fail-fast`, fired only behind the removed resume verdict.
|
|
214
238
|
export const RESOLVER_REJECT_REASONS = [
|
|
215
239
|
"indeterminate-no-spawn",
|
|
216
240
|
"dormant-fire-forget-unsupported",
|
|
217
|
-
"owned-live-no-autosend",
|
|
218
|
-
"backend-liveness-unsupported",
|
|
219
241
|
"mailbox-undeliverable",
|
|
220
242
|
] as const satisfies readonly EntwurfV2RejectReason[];
|
|
221
243
|
|
|
@@ -228,18 +250,22 @@ export const RESOLVER_REJECT_REASONS = [
|
|
|
228
250
|
// send arm (ack-only), but it requires a live-probe (NATIVE_PUSH_DISPATCH_TABLE),
|
|
229
251
|
// where meta-mailbox is liveness-free. It is NOT a mailbox enqueue and NOT a pi socket
|
|
230
252
|
// send — it is its own rail.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
253
|
+
//
|
|
254
|
+
// This list contains DELIVERY outcomes only. A mux may launch a live runtime, but
|
|
255
|
+
// its session handle is neither a receipt transport nor address authority. Once
|
|
256
|
+
// that runtime becomes a record-backed citizen, delivery still uses one of the
|
|
257
|
+
// rails below. `spawn-bg` — the detached, window-less resume child — was removed
|
|
258
|
+
// with `owned-outcome` (header): every remaining transport delivers to a citizen
|
|
259
|
+
// that is already running, so this verb no longer starts a process at all.
|
|
260
|
+
export const ENTWURF_V2_TRANSPORTS = ["control-socket", "meta-mailbox", "native-push"] as const;
|
|
238
261
|
export type EntwurfV2Transport = (typeof ENTWURF_V2_TRANSPORTS)[number];
|
|
239
262
|
|
|
240
263
|
// Allow-branch facets (exported so the schema↔types gate asserts every enum).
|
|
241
|
-
|
|
242
|
-
|
|
264
|
+
// Both are single-valued since the cut: the only `resume`/`owned` verdict was the
|
|
265
|
+
// spawn-bg cell. They stay as enums because they are the receipt's own vocabulary
|
|
266
|
+
// and a future visible-resume verb would speak them again.
|
|
267
|
+
export const ENTWURF_V2_ACTIONS = ["send"] as const;
|
|
268
|
+
export const ENTWURF_V2_OWNERSHIPS = ["ack-only"] as const;
|
|
243
269
|
// Delivery mode of the message to the target (how it is injected) — steer =
|
|
244
270
|
// interrupt the current turn, follow_up = queue after it. A SEPARATE axis from
|
|
245
271
|
// both the intent/ownership axis (F1) and the liveness-routing axis. The removed
|
|
@@ -249,33 +275,30 @@ export const ENTWURF_V2_MODES = ["steer", "follow_up"] as const;
|
|
|
249
275
|
|
|
250
276
|
export type DispatchVerdict =
|
|
251
277
|
| { action: "send"; transport: "control-socket" | "meta-mailbox" | "native-push"; ownership: "ack-only" }
|
|
252
|
-
| { action: "resume"; transport: "spawn-bg" | "tmux-live"; ownership: "owned" }
|
|
253
278
|
| { action: "reject"; reason: EntwurfV2RejectReason };
|
|
254
279
|
|
|
255
280
|
// ── The FROZEN decision table ──────────────────────────────────────────────
|
|
256
|
-
// intent × dispatch-liveness → exactly one verdict (Q2).
|
|
257
|
-
// exactly
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
281
|
+
// intent × dispatch-liveness → exactly one verdict (Q2). THREE cells since the
|
|
282
|
+
// visible-first cut, of which exactly ONE allows: fire-and-forget+live = send.
|
|
283
|
+
// The other two reject. The reject cells are honest "지금은 없음" locks (N2).
|
|
284
|
+
// They were written while the legacy 3-verb surface still covered those flows;
|
|
285
|
+
// that surface is GONE, so a reject cell is a real absence with no fallback verb
|
|
286
|
+
// behind it — reopening one takes a new contract, never a quiet re-admission.
|
|
287
|
+
//
|
|
288
|
+
// The `dormant` cell is the one that now carries the whole cost of the cut: an
|
|
289
|
+
// in-domain citizen that is not running cannot be reached by ANY intent. That is
|
|
290
|
+
// the intended fail-closed state, not an oversight — the resume that used to
|
|
291
|
+
// answer here was hidden, and a visible replacement is a separate capability.
|
|
263
292
|
export const DISPATCH_TABLE: Record<EntwurfIntent, Record<DispatchLiveness, DispatchVerdict>> = {
|
|
264
293
|
"fire-and-forget": {
|
|
265
294
|
live: { action: "send", transport: "control-socket", ownership: "ack-only" },
|
|
266
295
|
dormant: { action: "reject", reason: "dormant-fire-forget-unsupported" },
|
|
267
296
|
indeterminate: { action: "reject", reason: "indeterminate-no-spawn" },
|
|
268
297
|
},
|
|
269
|
-
"owned-outcome": {
|
|
270
|
-
// wants_reply is etiquette, not ownership — owned+live never auto-sends (Q2/F1).
|
|
271
|
-
live: { action: "reject", reason: "owned-live-no-autosend" },
|
|
272
|
-
dormant: { action: "resume", transport: "spawn-bg", ownership: "owned" },
|
|
273
|
-
indeterminate: { action: "reject", reason: "indeterminate-no-spawn" },
|
|
274
|
-
},
|
|
275
298
|
};
|
|
276
299
|
|
|
277
300
|
// ── The unsupported-backend mailbox mini-table (F-mailbox) ─────────────────
|
|
278
|
-
// SEPARATE from the in-domain
|
|
301
|
+
// SEPARATE from the in-domain 3-cell DISPATCH_TABLE (Fable (i)): a backend with no
|
|
279
302
|
// pi-socket liveness predicate never enters the liveness-keyed table. Instead the
|
|
280
303
|
// domain guard routes it here, keyed on intent alone. Reaches here: claude-code
|
|
281
304
|
// (self-fetch mailbox) and codex (no adapter yet). Does NOT reach here: antigravity —
|
|
@@ -288,17 +311,20 @@ export const DISPATCH_TABLE: Record<EntwurfIntent, Record<DispatchLiveness, Disp
|
|
|
288
311
|
// fact is false (fail-closed). The ack is enqueue+doorbell, NOT read, and
|
|
289
312
|
// observedLiveness stays `unsupported` — the receipt's `meta-mailbox` transport
|
|
290
313
|
// is what says "this went to the mailbox".
|
|
291
|
-
// - owned-outcome has no real liveness to own on a self-fetch backend → reject.
|
|
292
314
|
//
|
|
293
315
|
// N2 asymmetry (명문화 — without this the two tables read as contradictory):
|
|
294
316
|
// fire-and-forget+dormant-PI = reject vs fire-and-forget+unsupported-CITIZEN = mailbox.
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
317
|
+
// The axis is NOT which backend a citizen runs, and this cell grants no backend a
|
|
318
|
+
// privilege. It is one question asked of both: does an ACTIVE RECEIVER actually drain
|
|
319
|
+
// the mailbox this message would land in? A self-fetch citizen reaches this allow cell
|
|
320
|
+
// only through the separate `mailboxDeliverable` fact, which IS that receiver check —
|
|
321
|
+
// when nobody drains, this very cell rejects as `mailbox-undeliverable`. An in-domain
|
|
322
|
+
// `dormant` pi has no drainer at all: its mailbox reader was the running session, and
|
|
323
|
+
// the session is confirmed not running, so an enqueue would be a silent pileup no one
|
|
324
|
+
// ever reads. Same rule, two answers. (It reads as a backend split only if you skip
|
|
325
|
+
// the deliverability fact — which is exactly what makes claude-code look privileged.)
|
|
299
326
|
export const UNSUPPORTED_DISPATCH_TABLE: Record<EntwurfIntent, DispatchVerdict> = {
|
|
300
327
|
"fire-and-forget": { action: "send", transport: "meta-mailbox", ownership: "ack-only" },
|
|
301
|
-
"owned-outcome": { action: "reject", reason: "backend-liveness-unsupported" },
|
|
302
328
|
};
|
|
303
329
|
|
|
304
330
|
// ── Dispatch receipt (R3) ──────────────────────────────────────────────────
|
|
@@ -307,9 +333,9 @@ export const UNSUPPORTED_DISPATCH_TABLE: Record<EntwurfIntent, DispatchVerdict>
|
|
|
307
333
|
export type EntwurfV2Receipt =
|
|
308
334
|
| {
|
|
309
335
|
ok: true;
|
|
310
|
-
action: "send"
|
|
336
|
+
action: "send";
|
|
311
337
|
transport: EntwurfV2Transport;
|
|
312
|
-
ownership: "ack-only"
|
|
338
|
+
ownership: "ack-only";
|
|
313
339
|
observedLiveness: FactLiveness;
|
|
314
340
|
}
|
|
315
341
|
// observedLiveness is `FactLiveness | null` (?6): null for the pre-probe
|
|
@@ -331,9 +357,9 @@ export type EntwurfV2RejectReceipt = Extract<EntwurfV2Receipt, { ok: false }>;
|
|
|
331
357
|
* observedLiveness:"indeterminate"}`, which the blanket `FactLiveness | null`
|
|
332
358
|
* schema accepts. This constructor THROWS on a well-formedness violation, so
|
|
333
359
|
* every reject path (resolveDispatch's own mints below + the 5b stages that
|
|
334
|
-
* produce bad-target / target-locked / target-address-conflict /
|
|
335
|
-
*
|
|
336
|
-
*
|
|
360
|
+
* produce bad-target / target-locked / target-address-conflict / record-less-socket)
|
|
361
|
+
* routes through one chokepoint and the bypass surface is zero. 5b MUST build
|
|
362
|
+
* rejects with this, never by object literal.
|
|
337
363
|
*/
|
|
338
364
|
export function makeRejectReceipt(
|
|
339
365
|
reason: EntwurfV2RejectReason,
|
|
@@ -353,11 +379,11 @@ export function makeRejectReceipt(
|
|
|
353
379
|
* PURE dispatch decision over already-resolved facts. Before reaching here the
|
|
354
380
|
* caller has resolved the target (→ `bad-target` if no existing citizen) and, for
|
|
355
381
|
* an in-domain backend, acquired the per-gid lock (→ `target-locked`) and probed
|
|
356
|
-
* liveness UNDER that lock. This function only decides the liveness-routed
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
382
|
+
* liveness UNDER that lock. This function only decides the liveness-routed verdict.
|
|
383
|
+
* There is NO trust preflight anywhere on this path: it existed solely to guard the
|
|
384
|
+
* resume verdict that launched a child into a target cwd, and that verdict is gone.
|
|
385
|
+
* Do NOT reintroduce a global pre-resolver preflight — that re-breaks F-mailbox,
|
|
386
|
+
* and this verb no longer launches anything into any cwd.
|
|
361
387
|
*
|
|
362
388
|
* Two facts in: `liveness` (the 4-value FactLiveness) and `mailboxDeliverable`
|
|
363
389
|
* (F-mailbox — a SEPARATE axis from liveness, NOT a column of either table, NOT
|
|
@@ -369,8 +395,8 @@ export function makeRejectReceipt(
|
|
|
369
395
|
* backend the liveness-routed table is authoritative and the flag is ignored.
|
|
370
396
|
*
|
|
371
397
|
* R1 domain guard runs first: an `unsupported` liveness is routed through the
|
|
372
|
-
* UNSUPPORTED_DISPATCH_TABLE (mailbox mini-table), never the
|
|
373
|
-
* No
|
|
398
|
+
* UNSUPPORTED_DISPATCH_TABLE (mailbox mini-table), never the 3-cell table.
|
|
399
|
+
* No send, no I/O — step 5 executes the chosen transport.
|
|
374
400
|
*/
|
|
375
401
|
export function resolveDispatch(
|
|
376
402
|
intent: EntwurfIntent,
|
|
@@ -396,7 +422,7 @@ export function resolveDispatch(
|
|
|
396
422
|
};
|
|
397
423
|
}
|
|
398
424
|
// liveness is now narrowed to SocketLiveness; deliverability does not apply.
|
|
399
|
-
const cell = DISPATCH_TABLE[intent][dispatchLivenessOf(liveness)];
|
|
425
|
+
const cell: DispatchVerdict = DISPATCH_TABLE[intent][dispatchLivenessOf(liveness)];
|
|
400
426
|
if (cell.action === "reject") {
|
|
401
427
|
return makeRejectReceipt(cell.reason, liveness);
|
|
402
428
|
}
|
|
@@ -420,20 +446,16 @@ export function resolveDispatch(
|
|
|
420
446
|
// fire-and-forget × alive → native-push send (the ONE allow cell)
|
|
421
447
|
// fire-and-forget × dead → reject native-push-target-dead
|
|
422
448
|
// fire-and-forget × indeterminate → reject native-push-probe-indeterminate
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
449
|
+
//
|
|
450
|
+
// The `owned-outcome` row is gone with the intent (header). It had rejected on every
|
|
451
|
+
// liveness with `native-push-no-resume-authority` — a real fact about this rail that
|
|
452
|
+
// simply has no caller left to tell, since no caller can declare that intent.
|
|
426
453
|
export const NATIVE_PUSH_DISPATCH_TABLE: Record<EntwurfIntent, Record<NativePushLiveness, DispatchVerdict>> = {
|
|
427
454
|
"fire-and-forget": {
|
|
428
455
|
alive: { action: "send", transport: "native-push", ownership: "ack-only" },
|
|
429
456
|
dead: { action: "reject", reason: "native-push-target-dead" },
|
|
430
457
|
indeterminate: { action: "reject", reason: "native-push-probe-indeterminate" },
|
|
431
458
|
},
|
|
432
|
-
"owned-outcome": {
|
|
433
|
-
alive: { action: "reject", reason: "native-push-no-resume-authority" },
|
|
434
|
-
dead: { action: "reject", reason: "native-push-no-resume-authority" },
|
|
435
|
-
indeterminate: { action: "reject", reason: "native-push-no-resume-authority" },
|
|
436
|
-
},
|
|
437
459
|
};
|
|
438
460
|
|
|
439
461
|
// The reasons the native-push resolver emits — a THIRD post-probe reject set, parallel
|
|
@@ -443,7 +465,6 @@ export const NATIVE_PUSH_DISPATCH_TABLE: Record<EntwurfIntent, Record<NativePush
|
|
|
443
465
|
export const NATIVE_PUSH_REJECT_REASONS = [
|
|
444
466
|
"native-push-target-dead",
|
|
445
467
|
"native-push-probe-indeterminate",
|
|
446
|
-
"native-push-no-resume-authority",
|
|
447
468
|
] as const satisfies readonly EntwurfV2RejectReason[];
|
|
448
469
|
|
|
449
470
|
/**
|
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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 →
|
|
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.
|
|
101
|
-
// resume
|
|
102
|
-
//
|
|
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
|
|
158
|
-
//
|
|
159
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
390
|
-
|
|
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 (
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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",
|