@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
|
@@ -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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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.
|
|
@@ -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
|
|
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 —
|
|
@@ -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). `
|
|
45
|
-
//
|
|
46
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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
|
|
127
|
-
*
|
|
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` / `
|
|
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
|
|
@@ -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
|
|
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
|
-
* `
|
|
70
|
-
*
|
|
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.
|
|
@@ -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
|
|
|
34
35
|
import type { MailboxDeliverabilityResult } from "./entwurf-deliverability.ts";
|
|
@@ -143,8 +144,8 @@ export async function resolveDeadControlSendFallback(
|
|
|
143
144
|
return { kind: "reject", reason: receipt.reason };
|
|
144
145
|
}
|
|
145
146
|
// fire-and-forget + live is the ONLY in-domain allow cell → control-socket send. A
|
|
146
|
-
//
|
|
147
|
-
//
|
|
147
|
+
// meta-mailbox transport here would be a frozen-table drift, so fail loud rather than
|
|
148
|
+
// mis-route a SEND fallback onto another rail.
|
|
148
149
|
if (receipt.transport !== "control-socket") {
|
|
149
150
|
throw new Error(
|
|
150
151
|
`entwurf-v2-send-fallback: in-domain fire-and-forget re-resolve yielded unexpected transport (${receipt.transport}) — table drift.`,
|
|
@@ -127,7 +127,7 @@ export async function executeControlSocketSend(
|
|
|
127
127
|
const held = lock as LockClaim;
|
|
128
128
|
|
|
129
129
|
// Lock-leak backstop: once the lock is held, ANY throw out of the drive — a
|
|
130
|
-
// contract-violation guard
|
|
130
|
+
// contract-violation guard, the mis-route assert, even a buggy
|
|
131
131
|
// dep that throws where it should return — must still release the lock before it
|
|
132
132
|
// propagates. A leaked lock pins the gid forever (5a's worst failure). So convert any
|
|
133
133
|
// such throw into a `failed` final outcome, run finalizeRelease (which releases on
|
|
@@ -212,14 +212,9 @@ async function driveDeadFallback(
|
|
|
212
212
|
} catch (err) {
|
|
213
213
|
return { outcome: "failed", error: err };
|
|
214
214
|
}
|
|
215
|
-
case "spawn-bg":
|
|
216
|
-
// A SEND fallback must never re-resolve into a spawn — that is a decider
|
|
217
|
-
// contract violation (a send and a spawn are different actions), so fail loud
|
|
218
|
-
// rather than silently mis-execute.
|
|
219
|
-
throw new Error("entwurf-v2-send: re-resolve returned a spawn-bg plan for a send fallback (contract violation).");
|
|
220
215
|
case "native-push":
|
|
221
216
|
// The dead-control-socket fallback re-resolves only the pi socket domain
|
|
222
|
-
// (control-socket / mailbox
|
|
217
|
+
// (control-socket / mailbox); it never routes the native-push rail. A
|
|
223
218
|
// native-push rePlan here is structurally impossible — fail loud.
|
|
224
219
|
throw new Error(
|
|
225
220
|
"entwurf-v2-send: re-resolve returned a native-push plan for a send fallback (contract violation).",
|