@junghanacs/entwurf 0.30.1 → 0.31.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 +1 -0
- package/CHANGELOG.md +112 -0
- package/DELIVERY.md +1 -0
- package/README.md +47 -7
- package/VERIFY.md +17 -1
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +104 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +30 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +288 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
- package/mcp/entwurf-bridge/src/index.ts +7 -7
- package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
- package/package.json +13 -10
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/pi-durable/bootstrap.mjs +281 -0
- package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
- package/pi/pi-durable/overlay/upstream-pin.json +16 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
- package/pi-extensions/lib/fresh-call-composition.ts +42 -7
- package/pi-extensions/lib/meta-sender-identity.ts +7 -1
- package/pi-extensions/lib/meta-session.ts +28 -2
- package/pi-extensions/lib/mux-fresh-call.ts +34 -2
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
- package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
- package/run.sh +200 -33
- package/scripts/check-fresh-call-dispatch.ts +18 -0
- package/scripts/check-gate-qualification.ts +2 -1
- package/scripts/check-pi-durable-contact.ts +834 -0
- package/scripts/check-pi-durable-receive.ts +1116 -0
- package/scripts/check-pi-durable-send.ts +1661 -0
- package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
- package/scripts/lib/pi-durable-r1.ts +100 -0
- package/scripts/lib/pi-durable-reap.ts +27 -0
- package/scripts/lib/pi-durable-receive.ts +137 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/copilot-birth.json +2 -2
- package/scripts/mutants/meta-hook-session-switch.json +4 -2
- package/scripts/mutants/mux-fresh-call.json +69 -3
- package/scripts/mutants/omp-birth.json +2 -2
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +390 -0
- package/scripts/mutants/pi-floor.json +2 -2
- package/scripts/pi-durable-contact-host.mjs +105 -0
- package/scripts/pi-durable-receive-host.mjs +127 -0
- package/scripts/pi-durable-send-host.mjs +213 -0
- package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
- package/scripts/smoke-setup-verdict.sh +21 -2
- package/scripts/tsconfig.json +1 -0
|
@@ -25,11 +25,12 @@
|
|
|
25
25
|
import { randomBytes } from "node:crypto";
|
|
26
26
|
|
|
27
27
|
/** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
|
|
28
|
-
* not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C,
|
|
29
|
-
* by #95 after its system birth and app-server rails were measured
|
|
28
|
+
* not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, `codex`
|
|
29
|
+
* by #95 after its system birth and app-server rails were measured, and `pi-durable` by #129
|
|
30
|
+
* (the operator-provided durable app through the managed `entwurf pi-durable`). The set is joined to the
|
|
30
31
|
* citizen backends by `check-harness-admission-parity`: a harness that mints records but is
|
|
31
32
|
* missing HERE is not an unwired convenience, it is a release blocker. */
|
|
32
|
-
export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "codex"] as const;
|
|
33
|
+
export const FRESH_CALL_BACKENDS = ["pi", "claude-code", "copilot", "omp", "codex", "pi-durable"] as const;
|
|
33
34
|
export type FreshCallBackend = (typeof FRESH_CALL_BACKENDS)[number];
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -68,6 +69,11 @@ export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
|
|
|
68
69
|
// Codex's tool process is the operator app-server, not the pane: the no-arg
|
|
69
70
|
// verb refuses there by name. First action stays the delivery verb with args.
|
|
70
71
|
codex: "mcp__entwurf_bridge__entwurf_v2",
|
|
72
|
+
// The durable contact registers the bridge verbs as its OWN tools under their bare names, and the
|
|
73
|
+
// app offers a tool to the model by its registered name (`durable/harness/registry.ts:99`
|
|
74
|
+
// @cd32f77). Measured for `entwurf_v2` on the S+native-H wire (receipt 4012c066); the callback
|
|
75
|
+
// spelling follows the same rule and is owed its first LIVE observation.
|
|
76
|
+
"pi-durable": "entwurf_callback",
|
|
71
77
|
};
|
|
72
78
|
|
|
73
79
|
/** Where a sibling SENDS the task result. Always the delivery verb, never the birth callback. */
|
|
@@ -77,6 +83,7 @@ export const FRESH_CALL_DELIVERY_TOOL: Record<FreshCallBackend, string> = {
|
|
|
77
83
|
copilot: "entwurf-bridge-entwurf_v2",
|
|
78
84
|
omp: "mcp__entwurf_bridge_entwurf_v",
|
|
79
85
|
codex: "mcp__entwurf_bridge__entwurf_v2",
|
|
86
|
+
"pi-durable": "entwurf_v2",
|
|
80
87
|
};
|
|
81
88
|
|
|
82
89
|
/**
|
|
@@ -99,6 +106,7 @@ export const FRESH_CALL_PEERS_TOOL: Record<FreshCallBackend, string> = {
|
|
|
99
106
|
copilot: "entwurf-bridge-entwurf_peers",
|
|
100
107
|
omp: "mcp__entwurf_bridge_entwurf_peers",
|
|
101
108
|
codex: "mcp__entwurf_bridge__entwurf_peers",
|
|
109
|
+
"pi-durable": "entwurf_peers",
|
|
102
110
|
};
|
|
103
111
|
|
|
104
112
|
/**
|
|
@@ -139,13 +147,16 @@ export const FRESH_CALL_TOOL_LOAD_HINT: Record<FreshCallBackend, readonly string
|
|
|
139
147
|
copilot: [],
|
|
140
148
|
omp: [],
|
|
141
149
|
codex: [],
|
|
150
|
+
"pi-durable": [],
|
|
142
151
|
};
|
|
143
152
|
|
|
144
153
|
/**
|
|
145
|
-
* What a launch has to say, in the two shapes the
|
|
146
|
-
* handed
|
|
147
|
-
*
|
|
148
|
-
*
|
|
154
|
+
* What a launch has to say, in the two shapes the six backends need. pi, claude-code, copilot
|
|
155
|
+
* and codex are handed the first-turn PROMPT as an argument; pi-durable is handed that same
|
|
156
|
+
* PROMPT inside the closed `{v,task}` payload on the bootstrap flag, which its packaged bootstrap
|
|
157
|
+
* decodes; omp is handed its own bootstrap PAYLOAD, which its installed extension unpacks. Both
|
|
158
|
+
* are always built, because building one is cheap and a backend switch must never be able to
|
|
159
|
+
* reach a field that was not composed.
|
|
149
160
|
*/
|
|
150
161
|
export interface FreshCallComposition {
|
|
151
162
|
prompt: string;
|
|
@@ -339,6 +350,30 @@ export function composeBackendArgs(
|
|
|
339
350
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
340
351
|
composition.prompt,
|
|
341
352
|
];
|
|
353
|
+
case "pi-durable": {
|
|
354
|
+
// The managed verb (`entwurf pi-durable`) carries the explicit model as pi's own
|
|
355
|
+
// `--provider`/`--model` pair, split at the FIRST `/` (the preflight already refused a model
|
|
356
|
+
// with no provider), the explicit width, and the WHOLE framing as the closed `{v,task}`
|
|
357
|
+
// payload — the durable bootstrap submits it to the root once, after the birth. The
|
|
358
|
+
// caller's address rides the env pair, never this payload.
|
|
359
|
+
const at = model.indexOf("/");
|
|
360
|
+
if (at <= 0 || at === model.length - 1) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`fresh-call composition: pi-durable model ${JSON.stringify(model)} is not <provider>/<model id>`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return [
|
|
366
|
+
"pi-durable",
|
|
367
|
+
"--provider",
|
|
368
|
+
model.slice(0, at),
|
|
369
|
+
"--model",
|
|
370
|
+
model.slice(at + 1),
|
|
371
|
+
"--width",
|
|
372
|
+
"task-wide",
|
|
373
|
+
`--${OMP_BOOTSTRAP_FLAG}`,
|
|
374
|
+
JSON.stringify({ v: OMP_BOOTSTRAP_VERSION, task: composition.prompt }),
|
|
375
|
+
];
|
|
376
|
+
}
|
|
342
377
|
}
|
|
343
378
|
}
|
|
344
379
|
|
|
@@ -73,7 +73,13 @@ import { type NativePushAdapter, resolveNativePushAdapter } from "./native-push/
|
|
|
73
73
|
* exactly the #46 defect on agy, and copilot joined the list only once its own hook wrote one
|
|
74
74
|
* (#82 RAIL 5b). Membership says a marker may EXIST, never that a reply can land: the reply
|
|
75
75
|
* rail is chosen from `nativePushSupported` at the bridge, not from this list. */
|
|
76
|
-
export const META_SENDER_BACKENDS: readonly MetaBackend[] = [
|
|
76
|
+
export const META_SENDER_BACKENDS: readonly MetaBackend[] = [
|
|
77
|
+
"claude-code",
|
|
78
|
+
"antigravity",
|
|
79
|
+
"copilot",
|
|
80
|
+
"omp",
|
|
81
|
+
"pi-durable",
|
|
82
|
+
];
|
|
77
83
|
|
|
78
84
|
/** A marker that passed BOTH guards, together with the record that vouches for it. */
|
|
79
85
|
export interface TrustedMetaSender {
|
|
@@ -81,7 +81,7 @@ export class MetaRecordError extends Error {
|
|
|
81
81
|
* every write, including its own. `./run.sh doctor-meta-bridge` already flags a
|
|
82
82
|
* stale deployed writer — run it after the change.
|
|
83
83
|
*/
|
|
84
|
-
export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp"] as const;
|
|
84
|
+
export const META_BACKENDS = ["claude-code", "antigravity", "codex", "copilot", "omp", "pi-durable"] as const;
|
|
85
85
|
export type MetaBackend = (typeof META_BACKENDS)[number];
|
|
86
86
|
|
|
87
87
|
/**
|
|
@@ -222,6 +222,24 @@ export const META_BACKEND_DESCRIPTORS: Record<MetaBackend, MetaBackendDescriptor
|
|
|
222
222
|
deliveryLevel: "D6",
|
|
223
223
|
nativeIdLabel: "sessionId",
|
|
224
224
|
},
|
|
225
|
+
// The long-running durable app (#129). Graded only as far as its RECORDED receipts reach.
|
|
226
|
+
// sessionId the durable session directory name `<13-digit ms>-<uuid>` that
|
|
227
|
+
// `selectSession` creates and `--continue` reopens
|
|
228
|
+
// (`experimental/durable/sessions.ts:18-55` @cd32f77), surfaced as
|
|
229
|
+
// `view.current().session.id`. Not `pi.provider.sessionId`, which a
|
|
230
|
+
// fork changes.
|
|
231
|
+
// D2 receiver armed: the contact mints the record, names the host as sender, and arms
|
|
232
|
+
// a root doorbell (the receiver marker is owned by the host pid as `pi-durable-host`,
|
|
233
|
+
// which the owner join admits). The addressed-enqueue → idle-wake → root-notice → drain
|
|
234
|
+
// chain has ONE recorded receipt, on the scripted-model + native-harness lab cell
|
|
235
|
+
// (`check-pi-durable-receive`, S+H, V not run) — lab evidence, not a vendor-turn D4–D6
|
|
236
|
+
// acceptance, and no sibling-isolation (D3) or LIVE fresh receipt exists yet.
|
|
237
|
+
"pi-durable": {
|
|
238
|
+
backend: "pi-durable",
|
|
239
|
+
wakeMode: "self-fetch",
|
|
240
|
+
deliveryLevel: "D2",
|
|
241
|
+
nativeIdLabel: "sessionId",
|
|
242
|
+
},
|
|
225
243
|
};
|
|
226
244
|
|
|
227
245
|
// ---------------------------------------------------------------------------
|
|
@@ -305,7 +323,15 @@ function isoNow(now: Date): string {
|
|
|
305
323
|
export const META_SCHEMA_VERSION_V3 = 3 as const;
|
|
306
324
|
|
|
307
325
|
/** Every backend admitted by the one V3 record-citizen schema. */
|
|
308
|
-
export const META_CITIZEN_BACKENDS = [
|
|
326
|
+
export const META_CITIZEN_BACKENDS = [
|
|
327
|
+
"claude-code",
|
|
328
|
+
"antigravity",
|
|
329
|
+
"codex",
|
|
330
|
+
"copilot",
|
|
331
|
+
"omp",
|
|
332
|
+
"pi-durable",
|
|
333
|
+
"pi",
|
|
334
|
+
] as const;
|
|
309
335
|
export type MetaCitizenBackend = (typeof META_CITIZEN_BACKENDS)[number];
|
|
310
336
|
|
|
311
337
|
/**
|
|
@@ -143,6 +143,12 @@ import {
|
|
|
143
143
|
} from "./mux-placement.ts";
|
|
144
144
|
import { resolveCodexDefaultSocketPath } from "./native-push/codex-ws-client.ts";
|
|
145
145
|
import { OMP_PREFLIGHT_HINT, type OmpPreflightRejectReason, ompFreshPreflight } from "./omp-fresh-preflight.ts";
|
|
146
|
+
import {
|
|
147
|
+
PI_DURABLE_PREFLIGHT_HINT,
|
|
148
|
+
type PiDurablePreflightRejectReason,
|
|
149
|
+
piDurableFreshPreflight,
|
|
150
|
+
} from "./pi-durable-fresh-preflight.ts";
|
|
151
|
+
import { checkPiDurableRuntime, PI_DURABLE_RUNTIME_HINT, piDurablePackageLayout } from "./pi-durable-runtime.ts";
|
|
146
152
|
import { classifyTmuxSessionName, resolveTmuxSessionId, type TmuxSessionRejectReason } from "./resolve-tmux-session.ts";
|
|
147
153
|
|
|
148
154
|
/**
|
|
@@ -238,6 +244,10 @@ export const FRESH_CALL_RUNTIME: Record<FreshCallBackend, string> = {
|
|
|
238
244
|
copilot: "entwurf",
|
|
239
245
|
omp: "omp",
|
|
240
246
|
codex: "codex",
|
|
247
|
+
// #129: like copilot, a MANAGED verb and never the bare app. The durable app is source-only
|
|
248
|
+
// and runs under the operator-provided runtime's own resolver; `entwurf pi-durable` is the one
|
|
249
|
+
// fixed invocation that verifies that runtime against the pin and execs the packaged bootstrap.
|
|
250
|
+
"pi-durable": "entwurf",
|
|
241
251
|
};
|
|
242
252
|
|
|
243
253
|
/** A launch that was refused, or a placement that could not be established. Every value is a
|
|
@@ -251,6 +261,7 @@ export type FreshCallRejectReason =
|
|
|
251
261
|
| TmuxSessionRejectReason
|
|
252
262
|
| CopilotPreflightRejectReason
|
|
253
263
|
| OmpPreflightRejectReason
|
|
264
|
+
| PiDurablePreflightRejectReason
|
|
254
265
|
| CodexPreflightRejectReason
|
|
255
266
|
| CodexCallerPreflightRejectReason
|
|
256
267
|
| CodexCallerSeatRejectReason
|
|
@@ -350,9 +361,9 @@ export type FreshCallResult = { ok: true; receipt: FreshCallReceipt } | { ok: fa
|
|
|
350
361
|
* for. That is sufficient and not a compromise: every reader of the carrier trims and tests
|
|
351
362
|
* truthiness (`index.ts:212-217`), so empty and absent are the same answer by construction.
|
|
352
363
|
*
|
|
353
|
-
* It is applied to all
|
|
364
|
+
* It is applied to all six backends because the leak is a property of tmux, not of a vendor. A
|
|
354
365
|
* scrub only on the backend whose measurement surfaced it would encode the claim that the other
|
|
355
|
-
*
|
|
366
|
+
* five are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
|
|
356
367
|
* authoritative when the process that owns it exported it ITSELF, and a fresh `pi` sibling does
|
|
357
368
|
* exactly that after this argv has run. This is a fixed seam and deliberately NOT a general env
|
|
358
369
|
* carrier — an arbitrary `-e` passthrough would hand callers the environment-shaping power this
|
|
@@ -506,6 +517,25 @@ export function freshCall(
|
|
|
506
517
|
const missing = ompFreshPreflight(env);
|
|
507
518
|
if (missing) return { ok: false, reason: missing };
|
|
508
519
|
}
|
|
520
|
+
if (params.backend === "pi-durable") {
|
|
521
|
+
// The framed first input is computed here only to MEASURE it against the bootstrap payload
|
|
522
|
+
// cap; the same pure function composes it again below. The runtime is located and verified
|
|
523
|
+
// with THIS process's env, before anything is placed. That is the bridge's env, not
|
|
524
|
+
// necessarily the window's: a new-window pane inherits the tmux SERVER's environment
|
|
525
|
+
// (measured, see SCRUBBED_INHERITED_ENV), and the window runs whatever `entwurf` PATH names.
|
|
526
|
+
// The managed verb re-verifies with its own env before it execs anything, so the window never
|
|
527
|
+
// runs an unverified runtime — but when HOME/XDG_DATA_HOME or the package closure differ, this
|
|
528
|
+
// verdict and the window's can disagree, and the window is left showing its own refusal.
|
|
529
|
+
const layout = piDurablePackageLayout();
|
|
530
|
+
const runtime = layout === null ? null : checkPiDurableRuntime(env, layout);
|
|
531
|
+
const missing = piDurableFreshPreflight({
|
|
532
|
+
model,
|
|
533
|
+
firstInput: buildFreshCallPrompt({ backend: params.backend, task, callerGardenId, nonce }),
|
|
534
|
+
layout,
|
|
535
|
+
runtime: runtime === null || runtime.ok ? null : runtime.reason,
|
|
536
|
+
});
|
|
537
|
+
if (missing) return { ok: false, reason: missing };
|
|
538
|
+
}
|
|
509
539
|
const inspected = inspectPlacement(env);
|
|
510
540
|
if (!inspected.ok) return { ok: false, reason: inspected.reason };
|
|
511
541
|
const placement = inspected.placement;
|
|
@@ -626,6 +656,8 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
|
|
|
626
656
|
// the sentence an operator reads cannot drift away from the predicate that produced it.
|
|
627
657
|
...COPILOT_PREFLIGHT_HINT,
|
|
628
658
|
...OMP_PREFLIGHT_HINT,
|
|
659
|
+
...PI_DURABLE_RUNTIME_HINT,
|
|
660
|
+
...PI_DURABLE_PREFLIGHT_HINT,
|
|
629
661
|
...CODEX_PREFLIGHT_HINT,
|
|
630
662
|
...CODEX_CALLER_PREFLIGHT_HINT,
|
|
631
663
|
...CODEX_CALLER_SEAT_HINT,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-durable-fresh-preflight — the pre-mutation check a pi-durable fresh call needs (#129 W, L6;
|
|
3
|
+
* `docs/adding-a-harness.md` step 9 clauses 2, 3 and 5).
|
|
4
|
+
*
|
|
5
|
+
* Same question as `omp-fresh-preflight.ts`, at the same moment: before `mux-fresh-call` mutates the
|
|
6
|
+
* operator's tmux session, is everything the fresh contract needs actually in place? A refusal HERE,
|
|
7
|
+
* before placement, leaves no window behind. A pass here does not promise the window's own pass: the
|
|
8
|
+
* managed verb re-verifies inside the window, whose env is the tmux SERVER's and whose `entwurf` is
|
|
9
|
+
* whatever PATH names, so under env or package-closure skew it can still refuse after the window
|
|
10
|
+
* exists — and that refusal is left showing in it.
|
|
11
|
+
*
|
|
12
|
+
* ── The inputs, first ──
|
|
13
|
+
*
|
|
14
|
+
* model the fresh-call model grammar already admits `/`, and a durable launch needs the
|
|
15
|
+
* provider spelled out (the bootstrap refuses a model resolved by inference), so
|
|
16
|
+
* the model here is `<provider>/<model id>` split at the FIRST `/` — a catalog id
|
|
17
|
+
* that itself contains `/` keeps it. A model with no provider part is refused.
|
|
18
|
+
* first input the whole framing (callback first, then the task) rides the bootstrap's closed
|
|
19
|
+
* `{v:2,task}` payload, whose decoder caps `task` at 16000 characters. A framed
|
|
20
|
+
* task over that cap would open a window whose bootstrap refuses its own argv.
|
|
21
|
+
*
|
|
22
|
+
* ── Then the runtime, then the four units ──
|
|
23
|
+
*
|
|
24
|
+
* runtime operator-provided at the fixed locator and verified against the pin
|
|
25
|
+
* (`pi-durable-runtime.ts`); passed in as that check's verdict.
|
|
26
|
+
* birth the compiled contact in this package's closure — it mints the record.
|
|
27
|
+
* MCP hand the compiled entwurf-bridge entry in the same closure — the contact spawns it.
|
|
28
|
+
* receive the compiled owner join in the same closure admits `pi-durable-host`; without
|
|
29
|
+
* it every dispatch to the citizen is the honest `mailbox-undeliverable`.
|
|
30
|
+
* visible identity the packaged bootstrap — the one that hands the TUI the view whose conversation
|
|
31
|
+
* labels carry the garden id.
|
|
32
|
+
*
|
|
33
|
+
* ── What this deliberately does NOT claim ──
|
|
34
|
+
*
|
|
35
|
+
* Filesystem truth about ONE package closure and ONE runtime directory. It does not prove the durable
|
|
36
|
+
* app opens, the bridge connects, the doorbell arms or a garden id renders — that is runtime truth for
|
|
37
|
+
* the clause 7 LIVE receipt. No spawn, no network, no mutation; every path arrives as an argument, so
|
|
38
|
+
* this leaf imports nothing but types.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
42
|
+
import type { PiDurablePackageLayout, PiDurableRuntimeRejectReason } from "./pi-durable-runtime.ts";
|
|
43
|
+
|
|
44
|
+
/** The bootstrap decoder's task cap (`meta-bridge-omp.ts` OMP_BOOTSTRAP_TASK_MAX_CHARS); the beside
|
|
45
|
+
* test holds the two equal. */
|
|
46
|
+
export const PI_DURABLE_FIRST_INPUT_MAX_CHARS = 16000;
|
|
47
|
+
|
|
48
|
+
export type PiDurablePreflightRejectReason =
|
|
49
|
+
| "pi-durable-model-not-provider-qualified"
|
|
50
|
+
| "pi-durable-first-input-too-long"
|
|
51
|
+
| PiDurableRuntimeRejectReason
|
|
52
|
+
| "pi-durable-birth-unit-missing"
|
|
53
|
+
| "pi-durable-mcp-hand-missing"
|
|
54
|
+
| "pi-durable-receive-unit-missing"
|
|
55
|
+
| "pi-durable-visible-identity-missing";
|
|
56
|
+
|
|
57
|
+
/** `<provider>/<model id>` at the FIRST `/`, or `null` when either side is empty. */
|
|
58
|
+
export function splitPiDurableModel(model: string): { provider: string; model: string } | null {
|
|
59
|
+
const at = model.indexOf("/");
|
|
60
|
+
if (at <= 0 || at === model.length - 1) return null;
|
|
61
|
+
return { provider: model.slice(0, at), model: model.slice(at + 1) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The compiled owner join, read from the emitted bytes the bridge actually loads. */
|
|
65
|
+
function ownerJoinAdmitsDurableHost(file: string): boolean {
|
|
66
|
+
try {
|
|
67
|
+
const join = /SENDER_JOINED_RECEIVER_OWNER_KINDS\s*=\s*(\[[^\]]*\])/.exec(readFileSync(file, "utf8"))?.[1];
|
|
68
|
+
return join?.includes('"pi-durable-host"') ?? false;
|
|
69
|
+
} catch {
|
|
70
|
+
// Bounded environment probe: an unreadable file is a missing unit.
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The FIRST missing thing, in repair order, or `null` when a fresh launch may proceed. */
|
|
76
|
+
export function piDurableFreshPreflight(params: {
|
|
77
|
+
readonly model: string;
|
|
78
|
+
readonly firstInput: string;
|
|
79
|
+
readonly layout: PiDurablePackageLayout | null;
|
|
80
|
+
/** The verdict of `checkPiDurableRuntime` for the caller's env: `null` = verified. */
|
|
81
|
+
readonly runtime: PiDurableRuntimeRejectReason | null;
|
|
82
|
+
}): PiDurablePreflightRejectReason | null {
|
|
83
|
+
if (splitPiDurableModel(params.model) === null) return "pi-durable-model-not-provider-qualified";
|
|
84
|
+
if (params.firstInput.length > PI_DURABLE_FIRST_INPUT_MAX_CHARS) return "pi-durable-first-input-too-long";
|
|
85
|
+
if (params.layout === null) return "pi-durable-package-incomplete";
|
|
86
|
+
if (params.runtime !== null) return params.runtime;
|
|
87
|
+
const layout = params.layout;
|
|
88
|
+
if (!existsSync(layout.adapter)) return "pi-durable-birth-unit-missing";
|
|
89
|
+
if (!existsSync(layout.bridgeEntry)) return "pi-durable-mcp-hand-missing";
|
|
90
|
+
if (!ownerJoinAdmitsDurableHost(layout.deliverability)) return "pi-durable-receive-unit-missing";
|
|
91
|
+
if (!existsSync(layout.bootstrap)) return "pi-durable-visible-identity-missing";
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Repair text beside the predicate that decides it. The runtime reasons keep theirs on the runtime
|
|
96
|
+
* leaf; `mux-fresh-call` spreads both tables. */
|
|
97
|
+
export const PI_DURABLE_PREFLIGHT_HINT: Record<
|
|
98
|
+
Exclude<PiDurablePreflightRejectReason, PiDurableRuntimeRejectReason>,
|
|
99
|
+
string
|
|
100
|
+
> = {
|
|
101
|
+
"pi-durable-model-not-provider-qualified":
|
|
102
|
+
"a pi-durable model is `<provider>/<model id>` — the durable bootstrap resolves no provider by inference",
|
|
103
|
+
"pi-durable-first-input-too-long":
|
|
104
|
+
"the framed first input exceeds the 16000-character bootstrap payload cap — shorten the task",
|
|
105
|
+
"pi-durable-birth-unit-missing":
|
|
106
|
+
"this Entwurf package has no compiled pi-durable contact (mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js), so the sibling would mint no record — reinstall or rebuild Entwurf",
|
|
107
|
+
"pi-durable-mcp-hand-missing":
|
|
108
|
+
"this Entwurf package has no compiled entwurf-bridge entry, so the sibling's callback tool would not exist — reinstall or rebuild Entwurf",
|
|
109
|
+
"pi-durable-receive-unit-missing":
|
|
110
|
+
"this Entwurf package's compiled owner join does not admit pi-durable-host, so nothing could ever be delivered to the sibling — reinstall or rebuild Entwurf",
|
|
111
|
+
"pi-durable-visible-identity-missing":
|
|
112
|
+
"this Entwurf package has no packaged pi-durable bootstrap (pi/pi-durable/bootstrap.mjs), so no TUI would carry the garden id — reinstall Entwurf",
|
|
113
|
+
};
|