@junghanacs/entwurf 0.13.1 → 0.14.1
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 +59 -15
- package/BASELINE.md +3 -3
- package/CHANGELOG.md +74 -0
- package/CONTRIBUTING.md +13 -9
- package/DELIVERY.md +7 -6
- package/README.md +37 -28
- package/VERIFY.md +22 -14
- 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 +9 -4
- package/docs/external-mcp-host.md +4 -5
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +155 -28
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +8 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/classify-tmux-cwd.js +47 -0
- 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 +300 -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 +141 -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 +173 -28
- 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 +22 -11
- package/pi-extensions/entwurf-control.ts +227 -40
- package/pi-extensions/lib/acp/backend.ts +71 -12
- package/pi-extensions/lib/acp/overlay.ts +8 -6
- package/pi-extensions/lib/classify-tmux-cwd.ts +50 -0
- 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 +381 -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 +189 -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/run.sh +471 -201
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-overlay.ts +13 -3
- 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 +157 -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 +99 -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 +283 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +84 -76
- package/scripts/check-release-gate-outcomes.ts +127 -7
- 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/mutation-qualify.ts +109 -3
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-bridge-state.py +75 -1
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-overlay.json +17 -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/meta-retire.json +47 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +229 -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 +0 -1037
- package/scripts/mutants/release-gate.json +35 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +58 -18
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-agy-native-push-live.ts +6 -17
- package/scripts/smoke-entwurf-chain-live.ts +11 -3
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +169 -3
- 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 -275
- 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 -467
|
@@ -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
|
|
12
|
-
* `releaseLock
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* canonical socket path; `indeterminate` (EACCES/unknown) fails loud rather than silently
|
|
29
29
|
* claiming "no conflict". The decider's later `inspectSocket` probe is a SEPARATE step.
|
|
30
30
|
*/
|
|
31
|
-
import {
|
|
31
|
+
import { sendRpcCommand as realSendRpc, } from "./entwurf-control-rpc.js";
|
|
32
32
|
import { mailboxConversationalDeliverable, receiverMarkerMatchesIdentity, } from "./entwurf-deliverability.js";
|
|
33
33
|
import { isOutOfSocketDomainGardenIdConflict } from "./entwurf-facts.js";
|
|
34
34
|
import { isLivenessSupported } from "./entwurf-v2-contract.js";
|
|
@@ -38,35 +38,10 @@ import { makeProductionSendViaMailbox } from "./entwurf-v2-mailbox.js";
|
|
|
38
38
|
import { makeNativePushSend } from "./entwurf-v2-native-push.js";
|
|
39
39
|
import { executeControlSocketSend, } from "./entwurf-v2-send.js";
|
|
40
40
|
import { resolveDeadControlSendFallback } from "./entwurf-v2-send-fallback.js";
|
|
41
|
-
import { executeSpawnBgResume } from "./entwurf-v2-spawn.js";
|
|
42
|
-
import { makeProductionSpawnBgResumeDeps } from "./entwurf-v2-spawn-production.js";
|
|
43
41
|
import { defaultMetaMailboxDir, defaultMetaSessionsDir, enqueueMetaMessage, metaCapabilityFor, metaRecordExistsByGardenId, readAddressableMetaIdentity, readMetaReceiverMarker, } from "./meta-session.js";
|
|
44
42
|
import { resolveNativePushAdapter as realResolveNativePushAdapter, } from "./native-push/adapter.js";
|
|
45
43
|
import { CONTROL_SOCKET_DIR, controlSocketPath, inspectControlSocketPath, inspectTargetControlSocket, isRecordLessSocketCandidate, } from "./socket-discovery.js";
|
|
46
44
|
import { classifyConnectError, probeSocketLiveness } from "./socket-probe.js";
|
|
47
|
-
/**
|
|
48
|
-
* 0.12.1 B-2: the production `preflight` seam default. preflight value-imports
|
|
49
|
-
* `@earendil-works/pi-coding-agent` (ProjectTrustStore), so importing it eagerly
|
|
50
|
-
* would pull pi into the harness-neutral MCP bridge's boot closure. This wrapper
|
|
51
|
-
* defers that to a lazy `await import()` reached ONLY on the owned-outcome resume
|
|
52
|
-
* branch (the decider awaits it). peers/self/list/mailbox-deliver therefore boot
|
|
53
|
-
* with no pi package present.
|
|
54
|
-
*
|
|
55
|
-
* WHAT THIS DEFERRAL DOES NOT DO — corrected 0.12.8. The old sentence framed the
|
|
56
|
-
* pi-less case as an edge ("a pi-less environment that DOES hit a spawn-bg resume
|
|
57
|
-
* surfaces an honest module-not-found"). In a published consumer tree that is not an
|
|
58
|
-
* edge, it is the DEFAULT: `@earendil-works/pi-coding-agent` is an optional peer that
|
|
59
|
-
* a neutral `npm install` does not resolve (entwurf-preflight.ts:51), so this import
|
|
60
|
-
* throws for every installed user and the owned-outcome lane has never lived in any
|
|
61
|
-
* published version. The deferral still buys what it claims — a pi-free boot — and
|
|
62
|
-
* "module-not-found" is still honest, but it is a permanent floor on the install path,
|
|
63
|
-
* not a rare condition. Reviving that lane (declared dep / pi CLI subprocess / PATH
|
|
64
|
-
* resolution) is a separate decision; nothing here should read as if it works today.
|
|
65
|
-
*/
|
|
66
|
-
async function lazyProductionPreflight(input) {
|
|
67
|
-
const { preflight } = await import("./entwurf-preflight.js");
|
|
68
|
-
return preflight(input);
|
|
69
|
-
}
|
|
70
45
|
/** Map a record-side socket inspection to the singleton (socketGids, symlinkedGids) the
|
|
71
46
|
* `isOutOfSocketDomainGardenIdConflict` predicate consumes. `indeterminate` fails LOUD (QB2): an
|
|
72
47
|
* unprovable conflict must NOT be folded to "no conflict" — that would silently allow an
|
|
@@ -107,12 +82,10 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
107
82
|
releaseLock: s.releaseLock ?? realReleaseLock,
|
|
108
83
|
inspectSocket: s.inspectSocket ?? inspectTargetControlSocket,
|
|
109
84
|
probeSocket: s.probeSocket ?? probeSocketLiveness,
|
|
110
|
-
preflight: s.preflight ?? lazyProductionPreflight,
|
|
111
85
|
classifyConnect: s.classifyConnect ?? classifyConnectError,
|
|
112
86
|
sendRpc: s.sendRpc ?? realSendRpc,
|
|
113
87
|
enqueue: s.enqueue ?? enqueueMetaMessage,
|
|
114
88
|
resolveNativePushAdapter: s.resolveNativePushAdapter ?? realResolveNativePushAdapter,
|
|
115
|
-
spawnOverrides: s.spawnOverrides ?? {},
|
|
116
89
|
};
|
|
117
90
|
// ── ONE lock domain (Q2/QB3): a single lockDir-bound release for ALL hands ─
|
|
118
91
|
const acquire = (gid) => io.acquireLock(gid, { dir: lockDir });
|
|
@@ -185,7 +158,6 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
185
158
|
releaseLock: release,
|
|
186
159
|
inspectSocket,
|
|
187
160
|
probeSocket,
|
|
188
|
-
preflightForCwd: (cwd) => io.preflight({ cwd, agentDir: opts.agentDir, prefixRoots: opts.prefixRoots }),
|
|
189
161
|
mailboxDeliverabilityFor,
|
|
190
162
|
// 봉인 4: resolve the native-push adapter for this backend + probe the conversation.
|
|
191
163
|
// Only reached on a nativePushSupported backend (the decider gates it), so the resolver
|
|
@@ -193,7 +165,6 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
193
165
|
nativePushProbe: (identity) => Promise.resolve(io.resolveNativePushAdapter(identity.backend).probe(identity.nativeSessionId)),
|
|
194
166
|
mailboxDir,
|
|
195
167
|
sessionsDir,
|
|
196
|
-
observeTimeoutMs: opts.observeTimeoutMs,
|
|
197
168
|
};
|
|
198
169
|
// ── control-send hand deps (5c-2): the dead-fallback shares resolveTarget /
|
|
199
170
|
// inspect / probe / dirs with the decider; the mailbox enqueue is the SAME
|
|
@@ -224,25 +195,6 @@ export function makeProductionEntwurfV2Deps(opts) {
|
|
|
224
195
|
// ── executor: the three transport hands, each pre-bound ───────────────────
|
|
225
196
|
const executor = {
|
|
226
197
|
sendControl: (plan, lock) => executeControlSocketSend(plan, lock, controlSendDeps),
|
|
227
|
-
resumeSpawnBg: (plan, lock) => {
|
|
228
|
-
const spawnPlan = plan;
|
|
229
|
-
// Caller-edge preservation (#50 C3): the dormant rail delivers the SAME
|
|
230
|
-
// structured <sender_info> the live socket rail synthesizes at its
|
|
231
|
-
// receiver — one formatter (entwurf-control-rpc SSOT), appended after the
|
|
232
|
-
// task text exactly as a live delivery would render it. Without this, a
|
|
233
|
-
// resumed citizen woke with an anonymous prompt while every other rail
|
|
234
|
-
// carried the sender envelope.
|
|
235
|
-
const sender = opts.senderProvider();
|
|
236
|
-
const prompt = sender ? spawnPlan.prompt + formatSenderInfoBlock(sender, spawnPlan.wantsReply) : spawnPlan.prompt;
|
|
237
|
-
return executeSpawnBgResume({ ...spawnPlan, prompt }, lock,
|
|
238
|
-
// QB3: inject the shared lockDir-bound `release` — never the spawn factory's
|
|
239
|
-
// default releaseFn (which would release into the DEFAULT lock dir).
|
|
240
|
-
makeProductionSpawnBgResumeDeps({
|
|
241
|
-
...io.spawnOverrides,
|
|
242
|
-
killGraceMs: opts.killGraceMs ?? io.spawnOverrides.killGraceMs,
|
|
243
|
-
releaseFn: release,
|
|
244
|
-
}));
|
|
245
|
-
},
|
|
246
198
|
sendMailbox: (plan, _lock) => sendViaMailbox(plan, _lock),
|
|
247
199
|
// native-push (봉인 4): the SAME injected adapter resolver drives the executor send,
|
|
248
200
|
// 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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
18
|
-
* - no-lock — meta-mailbox (?7
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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 —
|
|
@@ -33,7 +34,7 @@ export function initialReleaseState() {
|
|
|
33
34
|
return { released: false };
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
|
-
* An in-domain execute (control-socket
|
|
37
|
+
* An in-domain execute (control-socket) must hold a lock whose gardenId
|
|
37
38
|
* IS the plan's target. Both halves are 5b decider-contract invariants, not runtime
|
|
38
39
|
* conditions: a null lock or a lock paired with the WRONG plan are the same grade of
|
|
39
40
|
* mis-wiring (a later release would free nothing, or free a DIFFERENT gid's lock), so
|
|
@@ -51,7 +52,7 @@ function assertInDomainLock(plan, lock) {
|
|
|
51
52
|
/**
|
|
52
53
|
* Derive the release policy from the plan, cross-checking the lock invariants the 5b
|
|
53
54
|
* decider guarantees: meta-mailbox ⇒ lock null (?7); in-domain (control-socket /
|
|
54
|
-
*
|
|
55
|
+
* control-socket) ⇒ lock non-null AND lock.gardenId === plan.targetGardenId. A mismatch is
|
|
55
56
|
* a decider contract violation, not a runtime condition — it throws so a mis-wired
|
|
56
57
|
* hand fails loud instead of silently dropping or mis-releasing a lock.
|
|
57
58
|
*/
|
|
@@ -74,17 +75,15 @@ export function decideReleasePolicy(plan, lock) {
|
|
|
74
75
|
case "control-socket":
|
|
75
76
|
assertInDomainLock(plan, lock);
|
|
76
77
|
return { kind: "release-after-send-final" };
|
|
77
|
-
case "spawn-bg":
|
|
78
|
-
assertInDomainLock(plan, lock);
|
|
79
|
-
return { kind: "release-after-spawn-observation" };
|
|
80
78
|
}
|
|
81
79
|
}
|
|
82
80
|
/**
|
|
83
81
|
* Fold one execution event into the release decision. `shouldRelease` is true
|
|
84
82
|
* EXACTLY ONCE — on the first event that satisfies the policy's release condition;
|
|
85
83
|
* every later event (after `state.released`) returns false. The single-release
|
|
86
|
-
* guarantee is what
|
|
87
|
-
*
|
|
84
|
+
* guarantee is what let the retired spawn watcher race two observations without a
|
|
85
|
+
* double release, and it is why the control-socket hand can feed a final outcome more
|
|
86
|
+
* than once without paying for it twice.
|
|
88
87
|
*/
|
|
89
88
|
export function reduceRelease(policy, state, event) {
|
|
90
89
|
if (state.released) {
|
|
@@ -99,19 +98,5 @@ export function reduceRelease(policy, state, event) {
|
|
|
99
98
|
case "release-after-send-final":
|
|
100
99
|
// Release once the send reaches a final outcome; hold before that.
|
|
101
100
|
return event.kind === "send-final" ? release() : hold();
|
|
102
|
-
case "release-after-spawn-observation":
|
|
103
|
-
// Fable 3: spawn-started is NOT a release event. Release on the first
|
|
104
|
-
// observed transition (socket-alive / child-exited, any code), or on a
|
|
105
|
-
// failed start (no child to watch).
|
|
106
|
-
switch (event.kind) {
|
|
107
|
-
case "socket-alive":
|
|
108
|
-
case "child-exited":
|
|
109
|
-
case "spawn-start-failed":
|
|
110
|
-
return release();
|
|
111
|
-
case "spawn-started":
|
|
112
|
-
case "send-final":
|
|
113
|
-
case "mailbox-enqueued":
|
|
114
|
-
return hold();
|
|
115
|
-
}
|
|
116
101
|
}
|
|
117
102
|
}
|
|
@@ -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` / `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
@@ -71,16 +69,6 @@ export async function executeDispatch(decision, deps) {
|
|
|
71
69
|
return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
|
-
case "spawn-bg": {
|
|
75
|
-
try {
|
|
76
|
-
// `lock-retained` is a RETURNED result (fail-closed), not a throw — it rides `executed`.
|
|
77
|
-
const result = await deps.resumeSpawnBg(plan, lock);
|
|
78
|
-
return { kind: "executed", receipt, transport, outcome: { transport: "spawn-bg", result } };
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
72
|
case "meta-mailbox": {
|
|
85
73
|
try {
|
|
86
74
|
// lock is null here (?7) — passed verbatim; the production adapter ignores it.
|
|
@@ -18,17 +18,18 @@
|
|
|
18
18
|
* must read the same row. That pin is also what makes "no resume promotion" structural,
|
|
19
19
|
* not just a 5c-2a guard: the `fire-and-forget` row has NO resume cell —
|
|
20
20
|
* live → send / control-socket (the retry)
|
|
21
|
-
* dormant(dead) → reject (dormant-fire-forget-unsupported) ←
|
|
21
|
+
* dormant(dead) → reject (dormant-fire-forget-unsupported) ← nothing is launched
|
|
22
22
|
* indeterminate → reject (indeterminate-no-spawn)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Since the visible-first cut there is no resume cell ANYWHERE — the intent that owned one
|
|
24
|
+
* is gone from the contract — so a dead re-resolve can only retry (alive), reject
|
|
25
|
+
* (dead/indeterminate pi), or — on an UNSUPPORTED backend, via the separate mailbox
|
|
26
|
+
* mini-table — enqueue to a deliverable citizen. It can never start a process.
|
|
27
27
|
*
|
|
28
|
-
* The N2 asymmetry (frozen in entwurf-v2-contract)
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* The N2 asymmetry (frozen in entwurf-v2-contract) is a RECEIVER question, not a backend
|
|
29
|
+
* privilege: a mailbox is offered only where an active receiver actually drains it. An
|
|
30
|
+
* `unsupported` citizen gets one exactly while `mailboxDeliverable` holds, and is rejected as
|
|
31
|
+
* `mailbox-undeliverable` the moment it does not. An in-domain `dormant` pi is a CONFIRMED
|
|
32
|
+
* not-running session, so nothing would ever drain that enqueue — a silent pileup → reject.
|
|
32
33
|
*/
|
|
33
34
|
import { isLivenessSupported, resolveDispatch } from "./entwurf-v2-contract.js";
|
|
34
35
|
import { defaultMetaMailboxDir, defaultMetaSessionsDir } from "./meta-session.js";
|
|
@@ -104,8 +105,8 @@ export async function resolveDeadControlSendFallback(plan, lock, deps) {
|
|
|
104
105
|
return { kind: "reject", reason: receipt.reason };
|
|
105
106
|
}
|
|
106
107
|
// fire-and-forget + live is the ONLY in-domain allow cell → control-socket send. A
|
|
107
|
-
//
|
|
108
|
-
//
|
|
108
|
+
// meta-mailbox transport here would be a frozen-table drift, so fail loud rather than
|
|
109
|
+
// mis-route a SEND fallback onto another rail.
|
|
109
110
|
if (receipt.transport !== "control-socket") {
|
|
110
111
|
throw new Error(`entwurf-v2-send-fallback: in-domain fire-and-forget re-resolve yielded unexpected transport (${receipt.transport}) — table drift.`);
|
|
111
112
|
}
|
|
@@ -45,7 +45,7 @@ export async function executeControlSocketSend(plan, lock, deps) {
|
|
|
45
45
|
const policy = decideReleasePolicy(plan, lock);
|
|
46
46
|
const held = lock;
|
|
47
47
|
// Lock-leak backstop: once the lock is held, ANY throw out of the drive — a
|
|
48
|
-
// contract-violation guard
|
|
48
|
+
// contract-violation guard, the mis-route assert, even a buggy
|
|
49
49
|
// dep that throws where it should return — must still release the lock before it
|
|
50
50
|
// propagates. A leaked lock pins the gid forever (5a's worst failure). So convert any
|
|
51
51
|
// such throw into a `failed` final outcome, run finalizeRelease (which releases on
|
|
@@ -127,14 +127,9 @@ async function driveDeadFallback(plan, lock, deps) {
|
|
|
127
127
|
catch (err) {
|
|
128
128
|
return { outcome: "failed", error: err };
|
|
129
129
|
}
|
|
130
|
-
case "spawn-bg":
|
|
131
|
-
// A SEND fallback must never re-resolve into a spawn — that is a decider
|
|
132
|
-
// contract violation (a send and a spawn are different actions), so fail loud
|
|
133
|
-
// rather than silently mis-execute.
|
|
134
|
-
throw new Error("entwurf-v2-send: re-resolve returned a spawn-bg plan for a send fallback (contract violation).");
|
|
135
130
|
case "native-push":
|
|
136
131
|
// The dead-control-socket fallback re-resolves only the pi socket domain
|
|
137
|
-
// (control-socket / mailbox
|
|
132
|
+
// (control-socket / mailbox); it never routes the native-push rail. A
|
|
138
133
|
// native-push rePlan here is structurally impossible — fail loud.
|
|
139
134
|
throw new Error("entwurf-v2-send: re-resolve returned a native-push plan for a send fallback (contract violation).");
|
|
140
135
|
}
|
|
@@ -14,33 +14,13 @@
|
|
|
14
14
|
* from mode/action — that would blur the F1 ownership contract).
|
|
15
15
|
* - `renderEntwurfV2Result` — the result union → `{ text, isError }`, surfacing the carry-overs
|
|
16
16
|
* the surface must NOT drop: a reject's reason+diagnostic, a control `rejectReason` (N3), a
|
|
17
|
-
*
|
|
17
|
+
* and the N1 delivered+lock-dirty `execution-failed`.
|
|
18
18
|
* - `runAndRenderEntwurfV2FromSurface` — assemble production deps + run + render, so the root
|
|
19
19
|
* surface never needs to name the `EntwurfV2RunResult` union (it only sees `{text,isError}`).
|
|
20
20
|
*/
|
|
21
|
-
import * as path from "node:path";
|
|
22
21
|
import { makeProductionEntwurfV2Deps } from "./entwurf-v2-production.js";
|
|
23
22
|
import { runEntwurfV2 } from "./entwurf-v2-runner.js";
|
|
24
23
|
import { FRESH_CUT_PRESCRIPTION } from "./meta-session.js";
|
|
25
|
-
/** The operator-policy SSOT for v2 dispatch's preflight prefix-auto-approve roots (5d-4b).
|
|
26
|
-
* ONE shared env var feeds BOTH surfaces (pi-native + MCP) — a pi session and an MCP child
|
|
27
|
-
* both inherit it, so there is no per-surface config fork. `prefixRoots` is operator policy,
|
|
28
|
-
* not session-local UX, so it is an env var, not a pi flag. */
|
|
29
|
-
export const ENTWURF_PREFIX_ROOTS_ENV = "ENTWURF_PREFIX_ROOTS";
|
|
30
|
-
/** Parse `ENTWURF_PREFIX_ROOTS` into the preflight's `prefixRoots`. `path.delimiter`-
|
|
31
|
-
* separated (`:` on Linux/macOS); entries are trimmed, empty segments dropped. Unset / empty
|
|
32
|
-
* / delimiters-only ⇒ `[]` (no prefix promotion — frozen decision 7, no package default).
|
|
33
|
-
* It does NOT throw on a nonexistent/typo path: `preflight`'s normalize keeps an absolute
|
|
34
|
-
* fallback, so a bad root simply never matches (a typo must not broaden approve, and must not
|
|
35
|
-
* turn every owned-outcome dispatch into a loud failure). `~` is left for preflight to expand. */
|
|
36
|
-
export function parseEntwurfPrefixRootsEnv(raw = process.env[ENTWURF_PREFIX_ROOTS_ENV]) {
|
|
37
|
-
if (!raw)
|
|
38
|
-
return [];
|
|
39
|
-
return raw
|
|
40
|
-
.split(path.delimiter)
|
|
41
|
-
.map((s) => s.trim())
|
|
42
|
-
.filter((s) => s.length > 0);
|
|
43
|
-
}
|
|
44
24
|
/** Surface params → the runner's `DispatchInput`. `wants_reply`→`wantsReply`; `intent` and
|
|
45
25
|
* `message` pass through; `mode`/`wantsReply` are left undefined when absent so the decider's
|
|
46
26
|
* own defaults (follow_up / false) apply — no double-default. */
|
|
@@ -55,27 +35,36 @@ export function toDispatchInput(params) {
|
|
|
55
35
|
}
|
|
56
36
|
/**
|
|
57
37
|
* Detour B (B-a) — actionable rendering of an honest reject. The decider is UNCHANGED:
|
|
58
|
-
* a reject stays a reject (Hard Rule 3)
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* reading as "delivery impossible". Returns undefined for rejects with no useful next step.
|
|
38
|
+
* a reject stays a reject (Hard Rule 3). This only appends a one-line "what to do instead"
|
|
39
|
+
* to the reject TEXT, so an honest reject stops reading as "delivery impossible". Returns
|
|
40
|
+
* undefined for rejects with no useful next step.
|
|
62
41
|
*/
|
|
63
42
|
export function actionableRejectHint(reason) {
|
|
64
43
|
switch (reason) {
|
|
65
|
-
case "
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
44
|
+
case "dormant-fire-forget-unsupported":
|
|
45
|
+
// The cell that carries the whole cost of the visible-first cut. Naming the real
|
|
46
|
+
// state — the citizen exists and is not running — matters more than ever now that
|
|
47
|
+
// there is no verb behind it: a caller that reads "reject" as "wrong id" goes
|
|
48
|
+
// looking in the wrong place.
|
|
49
|
+
return ("this citizen's record is intact but its session is not running, and delivery has no way to wake it. " +
|
|
50
|
+
"The resume that used to answer here launched a hidden background child and was withdrawn under the " +
|
|
51
|
+
"visible-first rule; the visible same-id resume is the separate lifecycle verb entwurf_resume_call. " +
|
|
52
|
+
"Reopen the citizen with entwurf_resume_call {target} (pi targets only; it runs no turn), then dispatch again.");
|
|
53
|
+
case "indeterminate-no-spawn":
|
|
54
|
+
// The socket probe did not settle, so liveness is UNKNOWN — an unestablished probe
|
|
55
|
+
// is not a measured death, and dispatching into it could double-deliver. Say
|
|
56
|
+
// "the PROBE was inconclusive", never "the socket answered inconclusively":
|
|
57
|
+
// `indeterminate` also covers a probe that got no answer at all, was refused by
|
|
58
|
+
// permissions, or timed out, and "answered" claims a reply that may never have
|
|
59
|
+
// existed. The wire id is FROZEN and still spells "-no-spawn"; it names the rule
|
|
60
|
+
// (never dispatch into an indeterminate target), not a capability that still
|
|
61
|
+
// exists. So the hint's job is to say plainly what did NOT happen: nothing
|
|
62
|
+
// delivered, nothing started.
|
|
63
|
+
return ("the control-socket probe was inconclusive, so the target's liveness is UNKNOWN — this is not a " +
|
|
64
|
+
"measured death. NOTHING was delivered and NO process was started (the reason id keeps its frozen " +
|
|
65
|
+
"'-no-spawn' wire spelling from an era when one could be; entwurf_v2 starts nothing on any rail). " +
|
|
66
|
+
"Re-run entwurf_peers to re-probe; if it stays indeterminate, check for a stale socket file left at " +
|
|
67
|
+
"that garden id by a session that died without cleaning up.");
|
|
79
68
|
case "native-push-target-dead":
|
|
80
69
|
// The adapter probe found no live host process for the conversation.
|
|
81
70
|
return "native-push conversation is not live (no host process found). Re-open the conversation, then retry — there is nothing to inject into.";
|
|
@@ -97,7 +86,7 @@ export function actionableRejectHint(reason) {
|
|
|
97
86
|
/** Render the outcome-rich result to `{ text, isError }`. A reject or a thrown/failed/dirty
|
|
98
87
|
* delivery is `isError:true`; a sent/fallback-sent/enqueued/observed delivery is `isError:false`.
|
|
99
88
|
* A control in-band `rejected` is a non-delivery (isError:true) and carries N3 `rejectReason`
|
|
100
|
-
* when present
|
|
89
|
+
* when present. */
|
|
101
90
|
export function renderEntwurfV2Result(result) {
|
|
102
91
|
switch (result.kind) {
|
|
103
92
|
case "rejected": {
|
|
@@ -125,27 +114,6 @@ export function renderEntwurfV2Result(result) {
|
|
|
125
114
|
isError: !delivered,
|
|
126
115
|
};
|
|
127
116
|
}
|
|
128
|
-
if (o.transport === "spawn-bg") {
|
|
129
|
-
const res = o.result;
|
|
130
|
-
if (res.kind === "lock-retained") {
|
|
131
|
-
const d = res.diagnostic;
|
|
132
|
-
return {
|
|
133
|
-
text: `entwurf_v2 spawn-bg LOCK RETAINED (${res.reason}) — lock NOT released, operator must clear:` +
|
|
134
|
-
`\n target: ${d.targetGardenId}` +
|
|
135
|
-
`\n lockPath: ${d.lockPath}` +
|
|
136
|
-
`\n expectedSocketPath: ${d.expectedSocketPath}` +
|
|
137
|
-
`\n observeTimeoutMs: ${d.observeTimeoutMs}, killGraceMs: ${d.killGraceMs}` +
|
|
138
|
-
(res.error ? `\n error: ${res.error}` : ""),
|
|
139
|
-
isError: true,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
if (res.kind === "spawn-start-failed") {
|
|
143
|
-
return { text: `entwurf_v2 spawn-bg failed to start: ${res.error}`, isError: true };
|
|
144
|
-
}
|
|
145
|
-
const pid = "pid" in res && res.pid !== undefined ? ` (pid ${res.pid})` : "";
|
|
146
|
-
const exit = res.kind === "child-exited" ? ` exitCode=${res.exitCode}` : "";
|
|
147
|
-
return { text: `entwurf_v2 spawn-bg → ${res.kind}${pid}${exit}, lock released`, isError: false };
|
|
148
|
-
}
|
|
149
117
|
if (o.transport === "native-push") {
|
|
150
118
|
// direct-inject succeeded; note if the 1-shot re-probe retry fired.
|
|
151
119
|
return {
|
|
@@ -179,12 +147,7 @@ export function renderEntwurfV2Result(result) {
|
|
|
179
147
|
* fence types stay behind this one entry point.
|
|
180
148
|
*/
|
|
181
149
|
export async function runAndRenderEntwurfV2FromSurface(params, opts) {
|
|
182
|
-
const prodOpts = {
|
|
183
|
-
senderProvider: opts.senderProvider,
|
|
184
|
-
agentDir: opts.agentDir,
|
|
185
|
-
// Explicit opts win (test / future surface override); otherwise the shared env SSOT.
|
|
186
|
-
prefixRoots: opts.prefixRoots ?? parseEntwurfPrefixRootsEnv(),
|
|
187
|
-
};
|
|
150
|
+
const prodOpts = { senderProvider: opts.senderProvider };
|
|
188
151
|
const result = await runEntwurfV2(toDispatchInput(params), makeProductionEntwurfV2Deps(prodOpts));
|
|
189
152
|
return renderEntwurfV2Result(result);
|
|
190
153
|
}
|