@junghanacs/entwurf 0.45.0 → 0.50.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 +3 -2
- package/BASELINE.md +110 -1
- package/CHANGELOG.md +169 -0
- package/CONTRIBUTING.md +1 -1
- package/ENTWURF-CLI.json +1229 -0
- package/ENTWURF.json +63 -15
- package/FAQ.md +4 -4
- package/README.md +124 -188
- package/VERIFY.md +2 -2
- package/docs/external-mcp-host.md +3 -3
- package/docs/setup-clean-host.md +37 -12
- package/mcp/entwurf-bridge/dist/ENTWURF.json +63 -15
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +19 -8
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +483 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +50 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +30 -15
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
- package/mcp/entwurf-bridge/src/index.ts +28 -8
- package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
- package/native/win32-x64/manifest.json +1 -1
- package/package.json +6 -3
- package/pi/pi-durable/bootstrap.mjs +179 -6
- package/pi/pi-durable/carrier/runtime.js +108 -9
- package/pi/pi-durable/carrier/session-name.js +11 -0
- package/pi/pi-durable/carrier/sessions.js +96 -20
- package/pi/pi-durable/overlay/upstream-pin.json +11 -5
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +710 -38
- package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
- package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
- package/pi-extensions/lib/herdr-resume-call.ts +810 -0
- package/pi-extensions/lib/meta-session.ts +13 -8
- package/pi-extensions/lib/mux-resume-call.ts +77 -4
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
- package/pi-extensions/lib/pi-durable-location.ts +446 -0
- package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
- package/pi-extensions/lib/resume-launch-identity.ts +31 -12
- package/pi-extensions/meta-bridge-pi-durable.ts +29 -15
- package/run.sh +411 -657
- package/scripts/check-claude-launch.ts +341 -0
- package/scripts/check-entwurf-v2-surface.ts +2 -2
- package/scripts/check-fresh-call-dispatch.ts +70 -9
- package/scripts/check-fresh-cut-gate.sh +23 -9
- package/scripts/check-gate-qualification.ts +18 -13
- package/scripts/check-herdr-activation.ts +301 -23
- package/scripts/check-herdr-fresh-call.ts +161 -25
- package/scripts/check-herdr-plugin-build.ts +173 -1
- package/scripts/check-herdr-plugin.ts +28 -2
- package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
- package/scripts/check-install-surface.ts +1236 -6
- package/scripts/check-meta-doctor-oracle.sh +1 -1
- package/scripts/check-meta-facts.ts +20 -1
- package/scripts/check-mux-resume-call.ts +28 -0
- package/scripts/check-peer-facts.ts +39 -2
- package/scripts/cli-help.mjs +222 -0
- package/scripts/dev-bin.sh +39 -2
- package/scripts/herdr-activation.mjs +28 -6
- package/scripts/herdr-operator-link.mjs +389 -0
- package/scripts/herdr-plugin-activate.mjs +5 -0
- package/scripts/herdr-plugin-deactivate.mjs +18 -5
- package/scripts/herdr-rails.mjs +43 -7
- package/scripts/herdr-runtime.mjs +27 -0
- package/scripts/meta-bridge-fresh-cut.ts +1 -27
- package/scripts/meta-bridge-prune.ts +1 -1
- package/scripts/meta-facts.ts +1 -1
- package/scripts/mutants/agent-guidance.json +4 -4
- package/scripts/mutants/claude-launch.json +142 -0
- package/scripts/mutants/cli-help-source.json +420 -0
- package/scripts/mutants/dev-gate-dispatch.json +109 -0
- package/scripts/mutants/fresh-call-dispatch.json +38 -2
- package/scripts/mutants/fresh-cut.json +15 -0
- package/scripts/mutants/herdr-activation.json +128 -0
- package/scripts/mutants/herdr-fresh-call.json +278 -5
- package/scripts/mutants/herdr-operator-link.json +318 -0
- package/scripts/mutants/herdr-plugin-build.json +55 -0
- package/scripts/mutants/herdr-plugin.json +17 -0
- package/scripts/mutants/herdr-resume-call.json +288 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
- package/scripts/mutants/meta-facts.json +15 -0
- package/scripts/mutants/mux-resume-call.json +63 -2
- package/scripts/mutants/peer-facts.json +15 -0
- package/scripts/mutants/pi-durable.json +979 -1
- package/scripts/mutants/source-install.json +35 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +255 -6
- package/scripts/peer-facts.ts +1 -1
- package/scripts/smoke-agy-install-state.sh +66 -0
- package/scripts/smoke-meta-prune.sh +23 -0
- package/scripts/smoke-omp-fresh-live.ts +12 -3
- package/scripts/smoke-omp-receive-live.ts +7 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
- package/team.json.example +39 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* entwurf-v2-visible-resume — stand a dormant pi or Claude Code citizen back up in a window
|
|
3
|
-
* operator can see, under the same garden id, and say only what was actually observed.
|
|
2
|
+
* entwurf-v2-visible-resume — stand a dormant pi, pi-durable or Claude Code citizen back up in a window
|
|
3
|
+
* (or herdr tab) the operator can see, under the same garden id, and say only what was actually observed.
|
|
4
4
|
*
|
|
5
5
|
* ── Two backends, one shape (#147 L2) ──
|
|
6
6
|
*
|
|
@@ -19,7 +19,17 @@
|
|
|
19
19
|
* timeout, a store write failure, a launch after the re-read) is invisible to this
|
|
20
20
|
* proof. Back = a NEW live owner, in the pane this call opened, re-armed the marker
|
|
21
21
|
* for the same session with a fresh binding the current record also carries (same
|
|
22
|
-
* createdAt). The old marker read again is never success.
|
|
22
|
+
* createdAt). The old marker read again is never success. "In the pane" is the owner
|
|
23
|
+
* pid equal to that pane's direct child: tmux's pane pid, or on herdr (#157 stage 4)
|
|
24
|
+
* the `pane.process_info` report of the pane ONE layout.apply created.
|
|
25
|
+
*
|
|
26
|
+
* pi-durable dormant = THIS process could reserve the session's directory D (#157 B3): the
|
|
27
|
+
* owner-fence claim's holder is proven dead, or it closed clean, and the record and
|
|
28
|
+
* its C4 location agree — decided by `reservePiDurableSession` inside its own
|
|
29
|
+
* record-arm then S_D holds, under the per-target lock this module already holds as
|
|
30
|
+
* the outer scope. The launched child adopts that one reservation from its closed
|
|
31
|
+
* context. Back = its adoption write: the claim carries this reservation's generation
|
|
32
|
+
* as its `adoptedFrom` — an adoption, never readiness, liveness or a turn.
|
|
23
33
|
*
|
|
24
34
|
* Every other backend is the leaf's named refusal. The argv is a fixed table per backend; the caller
|
|
25
35
|
* supplies only the target.
|
|
@@ -28,12 +38,20 @@
|
|
|
28
38
|
*
|
|
29
39
|
* surface (entwurf-control.ts · mcp bridge index.ts) composition root: supplies `launch`
|
|
30
40
|
* └─ entwurf-v2-visible-resume (this file) v2 leaves ONLY: lock, record, socket
|
|
31
|
-
* └─ deps.launch(argv, cwd) ── INJECTED SEAM ──→
|
|
41
|
+
* └─ deps.launch(argv, cwd) ── INJECTED SEAM ──→ fresh-call-dispatch (shared rail choice)
|
|
42
|
+
* ├→ mux-resume-call → placement leaf
|
|
43
|
+
* └→ herdr-resume-call (inside herdr: pi via agent
|
|
44
|
+
* start; pi-durable and Claude Code via ONE
|
|
45
|
+
* layout.apply)
|
|
32
46
|
*
|
|
33
47
|
* The seam is what keeps `docs/mux-launch-rail.md` §11 intact in both directions: v2 never
|
|
34
48
|
* imports mux, mux never imports entwurf core. This module cannot open a window; it can only
|
|
35
49
|
* ask whoever wired it to. That is deliberate — it means the delivery fence stays exactly where
|
|
36
|
-
* `check-mux-launch` scans for it.
|
|
50
|
+
* `check-mux-launch` scans for it. The herdr rail (#157 G1a) reaches it through the same seam and
|
|
51
|
+
* the same fence: this module imports no herdr module either, and learns which rail ran only from
|
|
52
|
+
* the `rail` its launch handle carries. A herdr launch is two mutations (tab create, then agent
|
|
53
|
+
* start), so the seam may also answer with a failure AFTER something was created; that is its own
|
|
54
|
+
* result, never a refusal that says no window was opened.
|
|
37
55
|
*
|
|
38
56
|
* ── Why a resume is structurally SMALLER than a fresh call on identity, and LARGER on the lock ──
|
|
39
57
|
*
|
|
@@ -63,8 +81,9 @@
|
|
|
63
81
|
*
|
|
64
82
|
* ── Two receipts, never merged ──
|
|
65
83
|
*
|
|
66
|
-
* `launch` says tmux made a window
|
|
67
|
-
* the citizen came back under the same id (pi:
|
|
84
|
+
* `launch` says tmux made a window (or, inside herdr, herdr made a tab) and was
|
|
85
|
+
* asked to start the runtime. `observation` says whether the citizen came back under the same id (pi:
|
|
86
|
+
* its socket; pi-durable: a child's adoption of the reservation; Claude Code: its receiver marker).
|
|
68
87
|
* Neither says the resumed citizen did anything. This verb submits no prompt. For pi a resume ran
|
|
69
88
|
* no turn (measured 2026-08-06, pi only — the transcript stayed byte-identical); for Claude Code
|
|
70
89
|
* the runtime's own activity after resume (its SessionStart hook re-arms the doorbell, and unread
|
|
@@ -72,11 +91,13 @@
|
|
|
72
91
|
* The socket-only description in the observation section above is the pi half; the Claude Code
|
|
73
92
|
* half reads the receiver marker and the record instead.
|
|
74
93
|
*/
|
|
94
|
+
import path from "node:path";
|
|
75
95
|
import { ACP_MODEL_NOT_CURATED_HINT, AcpModelNotCuratedError } from "./acp/model-ids.js";
|
|
76
96
|
import { ENTWURF_RESUME_REJECT_HINTS } from "./agent-guidance.js";
|
|
77
97
|
import { buildResumePiArgs } from "./entwurf-resume-args.js";
|
|
78
98
|
import { ENTWURF_V2_LOCK_DIR, acquireLock as realAcquireLock, releaseLock as realReleaseLock, } from "./entwurf-v2-lock.js";
|
|
79
|
-
import { classifyMarkerOwner, metaReceiverMarkerPath, parseMetaReceiverMarker, probePidExistence, processStartKey, readStoreRecordFile, } from "./meta-session.js";
|
|
99
|
+
import { classifyMarkerOwner, defaultMetaMailboxDir, defaultMetaReceiversDir, defaultMetaSendersDir, defaultMetaSessionsDir, metaReceiverMarkerPath, parseMetaReceiverMarker, probePidExistence, processStartKey, readStoreRecordFile, } from "./meta-session.js";
|
|
100
|
+
import { observePiDurableReservation, PiDurableOwnerFenceError, reservePiDurableSession, withdrawPiDurableReservation, } from "./pi-durable-owner-fence.js";
|
|
80
101
|
import { ResumeBackendUnsupportedError, resolveResumeLaunchIdentity, } from "./resume-launch-identity.js";
|
|
81
102
|
import { isValidSessionId } from "./session-id.js";
|
|
82
103
|
import { CONTROL_SOCKET_DIR, inspectTargetControlSocket, mapInspectionToLiveness, } from "./socket-discovery.js";
|
|
@@ -98,6 +119,19 @@ export function makeVisibleResumeDeps(launch, overrides = {}) {
|
|
|
98
119
|
probeSocket: (socketPath) => probeSocketLiveness(socketPath),
|
|
99
120
|
inspectReceiver: (gid) => inspectReceiverMarker(gid),
|
|
100
121
|
launch,
|
|
122
|
+
// The four roots THIS process resolves now, handed to the reserve as given: a split shape is
|
|
123
|
+
// the reserve's own named refusal, never merged here.
|
|
124
|
+
reservePiDurable: (gid) => reservePiDurableSession({
|
|
125
|
+
gardenId: gid,
|
|
126
|
+
roots: {
|
|
127
|
+
sessionsDir: defaultMetaSessionsDir(),
|
|
128
|
+
mailboxDir: defaultMetaMailboxDir(),
|
|
129
|
+
sendersDir: defaultMetaSendersDir(),
|
|
130
|
+
receiversDir: defaultMetaReceiversDir(),
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
withdrawPiDurable: (reservation) => withdrawPiDurableReservation(reservation),
|
|
134
|
+
observePiDurable: (reservation) => observePiDurableReservation(reservation),
|
|
101
135
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
102
136
|
now: () => Date.now(),
|
|
103
137
|
...overrides,
|
|
@@ -219,7 +253,7 @@ export async function visibleResume(targetGardenId, deps) {
|
|
|
219
253
|
return {
|
|
220
254
|
ok: false,
|
|
221
255
|
reason: err.reason,
|
|
222
|
-
detail: `${targetGardenId} is a ${err.backend} citizen; only pi and Claude Code citizens can be reopened ` +
|
|
256
|
+
detail: `${targetGardenId} is a ${err.backend} citizen; only pi, pi-durable and Claude Code citizens can be reopened ` +
|
|
223
257
|
`under the same garden id`,
|
|
224
258
|
};
|
|
225
259
|
}
|
|
@@ -227,6 +261,8 @@ export async function visibleResume(targetGardenId, deps) {
|
|
|
227
261
|
}
|
|
228
262
|
if (identity.backend === "claude-code")
|
|
229
263
|
return await resumeClaudeCode(targetGardenId, identity, deps);
|
|
264
|
+
if (identity.backend === "pi-durable")
|
|
265
|
+
return await resumePiDurable(targetGardenId, deps);
|
|
230
266
|
// Liveness preflight, still under the lock. A live citizen is addressed with
|
|
231
267
|
// `entwurf_v2 fire-and-forget`, not reopened — and refusing here is also what stops two
|
|
232
268
|
// windows existing for one garden id.
|
|
@@ -261,9 +297,14 @@ export async function visibleResume(targetGardenId, deps) {
|
|
|
261
297
|
provider: identity.provider,
|
|
262
298
|
model: identity.model,
|
|
263
299
|
});
|
|
264
|
-
const launched = deps.launch({ backend: "pi", cwd: identity.cwd, runtimeArgs });
|
|
265
|
-
if (!launched.ok)
|
|
266
|
-
return
|
|
300
|
+
const launched = await deps.launch({ backend: "pi", cwd: identity.cwd, runtimeArgs });
|
|
301
|
+
if (!launched.ok)
|
|
302
|
+
return launchNotLaunched(launched);
|
|
303
|
+
// Only a pi-durable launch carries a layout handle (#157 B3). Handed to a pi resume, it is a
|
|
304
|
+
// wiring defect, not an outcome: whatever was opened stays as it is and the lock is released.
|
|
305
|
+
if (launched.handle.rail === "herdr-layout" || launched.handle.rail === "herdr-layout-claude") {
|
|
306
|
+
throw new Error(`entwurf-v2-visible-resume: the launcher handed a pi resume of ${targetGardenId} a ${launched.handle.rail} handle — ` +
|
|
307
|
+
`only a pi-durable or Claude Code resume launches through layout.apply. Nothing was observed or closed.`);
|
|
267
308
|
}
|
|
268
309
|
const observation = await observeSameGidSocket(targetGardenId, mapped.socketPath, deps);
|
|
269
310
|
return {
|
|
@@ -276,6 +317,19 @@ export async function visibleResume(targetGardenId, deps) {
|
|
|
276
317
|
release();
|
|
277
318
|
}
|
|
278
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* A launch that did not become a launch, kept in its own shape. A refusal created nothing and is
|
|
322
|
+
* `launch-refused`. A failure that carries what it created (herdr: after the tab create) is
|
|
323
|
+
* `launch-failed`, with the vendor code, recovery and coordinates passed through untouched — folding
|
|
324
|
+
* it into a refusal is what would print "No window was opened" over a tab that may be on screen.
|
|
325
|
+
* Neither is retried, and neither is followed by an observation.
|
|
326
|
+
*/
|
|
327
|
+
function launchNotLaunched(launched, note = "") {
|
|
328
|
+
const detail = `${launched.reason} — ${launched.hint}${note}`;
|
|
329
|
+
return "failure" in launched
|
|
330
|
+
? { ok: false, reason: "launch-failed", detail, failure: launched.failure }
|
|
331
|
+
: { ok: false, reason: "launch-refused", detail };
|
|
332
|
+
}
|
|
279
333
|
/**
|
|
280
334
|
* The Claude Code half, still under the caller's lock. Dormancy needs a DEAD owner verdict on the
|
|
281
335
|
* receiver marker AND the binding: the record re-read AFTER that verdict must still name the same
|
|
@@ -357,26 +411,255 @@ async function resumeClaudeCode(targetGardenId, identity, deps) {
|
|
|
357
411
|
`after the dead owner's, and its process is not proven gone`,
|
|
358
412
|
};
|
|
359
413
|
}
|
|
360
|
-
const launched = deps.launch({
|
|
414
|
+
const launched = await deps.launch({
|
|
361
415
|
backend: "claude-code",
|
|
362
416
|
cwd: current.cwd,
|
|
363
417
|
runtimeArgs: ["--resume", current.nativeSessionId],
|
|
364
418
|
});
|
|
419
|
+
if (!launched.ok)
|
|
420
|
+
return launchNotLaunched(launched);
|
|
421
|
+
// The observation below joins the receiver marker's owner to the pane's DIRECT child: tmux's pane
|
|
422
|
+
// pid, or herdr's process report for the layout pane (#157 stage 4). A handle of any other rail
|
|
423
|
+
// carries no such pid, so it is a wiring defect, not an outcome: it fails loud rather than watching
|
|
424
|
+
// for a pid that does not exist. Whatever that launcher opened is left as it is; the lock is still
|
|
425
|
+
// released.
|
|
426
|
+
const handle = launched.handle;
|
|
427
|
+
if (handle.rail !== "tmux" && handle.rail !== "herdr-layout-claude") {
|
|
428
|
+
throw new Error(`entwurf-v2-visible-resume: the launcher handed a Claude Code resume of ${targetGardenId} a ${handle.rail} ` +
|
|
429
|
+
`handle — its observation joins the opened pane's direct child, which that rail does not carry. Nothing was ` +
|
|
430
|
+
`observed or closed.`);
|
|
431
|
+
}
|
|
432
|
+
const launch = { backend: "claude-code", targetGardenId, nativeSessionId: current.nativeSessionId, handle };
|
|
433
|
+
// herdr did not name the new pane's direct child: there is nothing to join, so no wait runs. The tab
|
|
434
|
+
// is open; nothing is retried or closed.
|
|
435
|
+
if (handle.rail === "herdr-layout-claude" && "unread" in handle.paneChild) {
|
|
436
|
+
return {
|
|
437
|
+
ok: true,
|
|
438
|
+
launch,
|
|
439
|
+
observation: { kind: "pane-process-unread", cause: handle.paneChild.unread, waitedMs: 0 },
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const panePid = handle.rail === "tmux" ? handle.panePid : handle.paneChild.pid;
|
|
443
|
+
const observation = await observeReceiverReattached(targetGardenId, current, { ownerPid: marker.ownerPid, ownerStartKey: marker.ownerStartKey, attachIncarnation: current.attachIncarnation }, panePid, deps);
|
|
444
|
+
return { ok: true, launch, observation };
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* #157 B3: the adopt launch for ONE reservation — the managed verb with its closed context
|
|
448
|
+
* `{v:1, gardenId, metaRoot, sourceGeneration}` (no session, cwd, profile, model, prompt, module,
|
|
449
|
+
* nonce or callback rides it), the reservation's cwd, and the fixed five-name projection of its raw
|
|
450
|
+
* profile and its meta root. Pure: it reads nothing and writes nothing.
|
|
451
|
+
*/
|
|
452
|
+
export function buildPiDurableAdoptLaunch(reservation) {
|
|
453
|
+
const { metaRoot } = reservation;
|
|
454
|
+
const context = {
|
|
455
|
+
v: 1,
|
|
456
|
+
gardenId: reservation.gardenId,
|
|
457
|
+
metaRoot,
|
|
458
|
+
sourceGeneration: reservation.generation,
|
|
459
|
+
};
|
|
460
|
+
return {
|
|
461
|
+
backend: "pi-durable",
|
|
462
|
+
cwd: reservation.cwd,
|
|
463
|
+
runtimeArgs: ["pi-durable", "--entwurf-adopt", JSON.stringify(context)],
|
|
464
|
+
piDurableAdoptEnv: {
|
|
465
|
+
PI_CODING_AGENT_DIR: reservation.profile,
|
|
466
|
+
ENTWURF_META_SESSIONS_DIR: path.join(metaRoot, "meta-sessions"),
|
|
467
|
+
ENTWURF_META_MAILBOX_DIR: path.join(metaRoot, "meta-mailbox"),
|
|
468
|
+
ENTWURF_META_SENDERS_DIR: path.join(metaRoot, "meta-senders"),
|
|
469
|
+
ENTWURF_META_RECEIVERS_DIR: path.join(metaRoot, "meta-receivers"),
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* A reserve refusal this verb answers by name; every other fence refusal keeps its own cause. A live holder is not
|
|
475
|
+
* an address (S3 joint review D4): a reservation's parent is a bridge waiting for a child, which entwurf_v2 cannot
|
|
476
|
+
* reach, and any other live holder is a process this verb did not judge contact-ready — so neither answer tells the
|
|
477
|
+
* caller to send. The branch reads the refusal's structured holder state, never its message.
|
|
478
|
+
*/
|
|
479
|
+
function piDurableReserveRefusal(targetGardenId, err) {
|
|
480
|
+
if (err.reason === "owner-live") {
|
|
481
|
+
if (err.holderState === "reserved") {
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
reason: "target-live",
|
|
485
|
+
detail: `${targetGardenId}'s pi-durable session is reserved for a child to adopt by a live process (${err.message}); ` +
|
|
486
|
+
`while that reservation stays unadopted under that live process a resume of ${targetGardenId} refuses on it; ` +
|
|
487
|
+
`nothing clears it automatically, a child that adopts it becomes its holder, and entwurf_v2 does not reach a reservation`,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
ok: false,
|
|
492
|
+
reason: "target-live",
|
|
493
|
+
detail: `${targetGardenId}'s pi-durable session is held by a live process (${err.message}); if that process is the ` +
|
|
494
|
+
`running citizen, entwurf_v2 reaches it only through the rail its own dispatch decides`,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
if (err.reason === "owner-uncertain" || err.reason === "owner-other-host") {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
reason: "target-indeterminate",
|
|
501
|
+
detail: `the holder of ${targetGardenId}'s pi-durable session could not be proven gone on this host (${err.message}), ` +
|
|
502
|
+
`and resuming a session that is still open would put two processes on one transcript`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
const messageOf = (err) => (err instanceof Error ? err.message : String(err));
|
|
508
|
+
/**
|
|
509
|
+
* A reserve whose write committed and whose lock release then failed (S3 joint review D3). It says only what that
|
|
510
|
+
* call wrote and what its holder implies; the release's own words say what it left. Nothing is withdrawn, retried
|
|
511
|
+
* or repaired here.
|
|
512
|
+
*/
|
|
513
|
+
function piDurableReservedThenFailed(targetGardenId, err, written) {
|
|
514
|
+
const residue = err.reason === "tx-release-failed"
|
|
515
|
+
? `; if the transaction file that release named remains, that process ending does not remove it, and it fails closed until an operator removes it`
|
|
516
|
+
: "";
|
|
517
|
+
return (`entwurf-v2-visible-resume: the reserve of ${targetGardenId} WROTE its reservation of ${written.resource} at ` +
|
|
518
|
+
`generation ${written.generation} for pid ${written.ownerPid}, then a lock release failed (${err.message}). ` +
|
|
519
|
+
`Nothing was launched and nothing withdrew that reservation: while it stays unadopted under live pid ` +
|
|
520
|
+
`${written.ownerPid}, a resume of ${targetGardenId} refuses, and nothing clears it automatically${residue}`);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* A withdrawal that failed (S3 joint review D1). Only what the failure itself records is stated: a write this call
|
|
524
|
+
* made, or a transaction file the release left; any other failure leaves the claim's state unestablished here.
|
|
525
|
+
*/
|
|
526
|
+
function piDurableWithdrawFailed(targetGardenId, held, reservation, err) {
|
|
527
|
+
const residue = `if that transaction file remains, this process ending does not remove it, and it fails closed until an operator removes it`;
|
|
528
|
+
if (err instanceof PiDurableOwnerFenceError && err.written !== undefined) {
|
|
529
|
+
return (` — this call wrote the withdrawal of ${held} (the claim back to the donor's ${err.written.state} row at ` +
|
|
530
|
+
`generation ${err.written.generation}), then releasing its transaction failed (${err.message}); ${residue}`);
|
|
531
|
+
}
|
|
532
|
+
if (err instanceof PiDurableOwnerFenceError && err.reason === "tx-release-failed") {
|
|
533
|
+
return (` — withdrawing ${held} FAILED at its transaction release (${err.message}); whether this call ` +
|
|
534
|
+
`wrote anything is not recorded, and ${residue}`);
|
|
535
|
+
}
|
|
536
|
+
return (` — withdrawing ${held} FAILED (${messageOf(err)}); what its claim holds now is not established here: while it ` +
|
|
537
|
+
`stays reserved, unadopted, under this live process (pid ${reservation.owner.pid}), a resume of ${targetGardenId} ` +
|
|
538
|
+
`refuses`);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* The pi-durable half (#157 B3), still under the caller's per-target lock, which stays the outer
|
|
542
|
+
* scope: the reserve takes record-arm then S_D itself and releases both before it returns, so before
|
|
543
|
+
* any launch. A reserve that refuses on eligibility writes nothing; a reserve whose lock release fails AFTER its
|
|
544
|
+
* write is named as such (its `written`), with the reservation it left. Neither opens anything.
|
|
545
|
+
*
|
|
546
|
+
* From the reservation on, this process holds D for one child, and what happens to that hold is
|
|
547
|
+
* decided by the launch outcome alone (L3-A1 §6), never by a guess:
|
|
548
|
+
*
|
|
549
|
+
* no launch request was made definitely none created → withdraw
|
|
550
|
+
* the seam refused (`ok:false`, no `failure`) the seam's contract: refused before anything was
|
|
551
|
+
* created (tmux: one of its named reasons; herdr:
|
|
552
|
+
* decided before the layout write) → withdraw
|
|
553
|
+
* a failure whose recovery is `none` that rail's own none-created certainty → withdraw
|
|
554
|
+
* a failure whose recovery is anything else something MAY exist → the reservation is KEPT
|
|
555
|
+
* the launcher threw unknown, whatever its position → KEPT
|
|
556
|
+
* a launch a child may adopt it → KEPT, then observed once
|
|
557
|
+
*
|
|
558
|
+
* A withdrawal that fails is reported with the refusal it followed and is never retried. A kept
|
|
559
|
+
* reservation is said so in the answer: it names this process as its holder, and while it stays unadopted
|
|
560
|
+
* under this live process a later resume of the same id refuses; nothing clears it automatically, and a
|
|
561
|
+
* child that adopts it becomes its holder.
|
|
562
|
+
*/
|
|
563
|
+
async function resumePiDurable(targetGardenId, deps) {
|
|
564
|
+
let reservation;
|
|
565
|
+
try {
|
|
566
|
+
reservation = await deps.reservePiDurable(targetGardenId);
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
if (err instanceof PiDurableOwnerFenceError) {
|
|
570
|
+
const refused = piDurableReserveRefusal(targetGardenId, err);
|
|
571
|
+
if (refused !== null)
|
|
572
|
+
return refused;
|
|
573
|
+
// A release that failed after the reservation was WRITTEN (S3 joint review D3): the reservation exists,
|
|
574
|
+
// so the answer names it and what it blocks, beside the release's own cause; nothing launches or withdraws.
|
|
575
|
+
if (err.written !== undefined)
|
|
576
|
+
throw new Error(piDurableReservedThenFailed(targetGardenId, err, err.written));
|
|
577
|
+
}
|
|
578
|
+
throw err;
|
|
579
|
+
}
|
|
580
|
+
const held = `${targetGardenId}'s reservation of ${reservation.resource} at generation ${reservation.generation}`;
|
|
581
|
+
const withdraw = async () => {
|
|
582
|
+
try {
|
|
583
|
+
await deps.withdrawPiDurable(reservation);
|
|
584
|
+
return ` — ${held} was withdrawn, since nothing was created`;
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
return piDurableWithdrawFailed(targetGardenId, held, reservation, err);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
const kept = ` — ${held} was KEPT, because something may exist that can still adopt it; it names this process ` +
|
|
591
|
+
`(pid ${reservation.owner.pid}) as its holder: while it stays unadopted under this live process a resume of ` +
|
|
592
|
+
`${targetGardenId} refuses, nothing clears it automatically, and a child that adopts it becomes its holder`;
|
|
593
|
+
let input;
|
|
594
|
+
try {
|
|
595
|
+
input = buildPiDurableAdoptLaunch(reservation);
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
throw new Error(`entwurf-v2-visible-resume: ${messageOf(err)}${await withdraw()}`);
|
|
599
|
+
}
|
|
600
|
+
let launched;
|
|
601
|
+
try {
|
|
602
|
+
launched = await deps.launch(input);
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
throw new Error(`entwurf-v2-visible-resume: the pi-durable launch of ${targetGardenId} threw: ${messageOf(err)}${kept}`);
|
|
606
|
+
}
|
|
365
607
|
if (!launched.ok) {
|
|
366
|
-
|
|
608
|
+
const noneCreated = !("failure" in launched) || launched.failure.recovery.outcome === "none";
|
|
609
|
+
return launchNotLaunched(launched, noneCreated ? await withdraw() : kept);
|
|
610
|
+
}
|
|
611
|
+
// A herdr agent-start handle belongs to the pi rail only: a pi-durable resume launches through
|
|
612
|
+
// tmux or ONE layout.apply. Handed one, it is a wiring defect; what was opened stays as it is.
|
|
613
|
+
if (launched.handle.rail === "herdr" || launched.handle.rail === "herdr-layout-claude") {
|
|
614
|
+
throw new Error(`entwurf-v2-visible-resume: the launcher handed a pi-durable resume of ${targetGardenId} a ` +
|
|
615
|
+
`${launched.handle.rail === "herdr" ? "herdr agent-start" : launched.handle.rail} handle — nothing was observed or closed${kept}`);
|
|
367
616
|
}
|
|
368
|
-
const observation = await
|
|
617
|
+
const observation = await observePiDurableAdoption(reservation, deps);
|
|
369
618
|
return {
|
|
370
619
|
ok: true,
|
|
371
620
|
launch: {
|
|
372
|
-
backend: "
|
|
621
|
+
backend: "pi-durable",
|
|
373
622
|
targetGardenId,
|
|
374
|
-
|
|
623
|
+
cwd: reservation.cwd,
|
|
624
|
+
reservation: {
|
|
625
|
+
generation: reservation.generation,
|
|
626
|
+
resource: reservation.resource,
|
|
627
|
+
ownerPid: reservation.owner.pid,
|
|
628
|
+
},
|
|
375
629
|
handle: launched.handle,
|
|
376
630
|
},
|
|
377
631
|
observation,
|
|
378
632
|
};
|
|
379
633
|
}
|
|
634
|
+
/**
|
|
635
|
+
* ONE bounded operation, the pi-durable twin of `observeSameGidSocket` (#157 B3): read the
|
|
636
|
+
* reservation's D claim until a child's adoption write carries its generation, the claim is lost or
|
|
637
|
+
* unreadable, or the same fixed wait ends. Every read is the fence's strict reader; nothing here
|
|
638
|
+
* writes, withdraws, retries or watches past the deadline. A read that throws past that reader stops
|
|
639
|
+
* the wait as `unobserved` with its cause: the window is open, and the launch receipt must survive.
|
|
640
|
+
*/
|
|
641
|
+
export async function observePiDurableAdoption(reservation, deps, timeoutMs = RESUME_OBSERVE_TIMEOUT_MS, intervalMs = RESUME_OBSERVE_INTERVAL_MS) {
|
|
642
|
+
const started = deps.now();
|
|
643
|
+
for (;;) {
|
|
644
|
+
let seen;
|
|
645
|
+
try {
|
|
646
|
+
seen = deps.observePiDurable(reservation);
|
|
647
|
+
}
|
|
648
|
+
catch (err) {
|
|
649
|
+
return { kind: "unobserved", reason: "observe-threw", detail: messageOf(err), waitedMs: deps.now() - started };
|
|
650
|
+
}
|
|
651
|
+
if (seen.class === "adopted")
|
|
652
|
+
return { kind: "adopted", waitedMs: deps.now() - started };
|
|
653
|
+
if (seen.class === "lost")
|
|
654
|
+
return { kind: "reservation-lost", waitedMs: deps.now() - started };
|
|
655
|
+
if (seen.class === "unobserved") {
|
|
656
|
+
return { kind: "unobserved", reason: seen.reason, detail: seen.detail, waitedMs: deps.now() - started };
|
|
657
|
+
}
|
|
658
|
+
if (deps.now() - started >= timeoutMs)
|
|
659
|
+
return { kind: "not-yet-adopted", waitedMs: deps.now() - started };
|
|
660
|
+
await deps.sleep(intervalMs);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
380
663
|
/**
|
|
381
664
|
* ONE bounded operation, the Claude Code twin of `observeSameGidSocket`: wait for a NEW live owner
|
|
382
665
|
* — in the pane this call opened (`panePid`), a join, not an address — to re-arm this garden id's
|
|
@@ -470,18 +753,29 @@ const REJECT_HINT = {
|
|
|
470
753
|
*/
|
|
471
754
|
export function renderVisibleResume(result, attribution = { actor: null }) {
|
|
472
755
|
if (!result.ok) {
|
|
756
|
+
if (result.reason === "launch-failed")
|
|
757
|
+
return renderLaunchFailed(result);
|
|
473
758
|
return {
|
|
474
759
|
text: `entwurf_resume_call rejected: ${result.reason} — ${REJECT_HINT[result.reason]}. ` +
|
|
475
760
|
`${result.detail}. No window was opened.`,
|
|
476
761
|
isError: true,
|
|
477
762
|
};
|
|
478
763
|
}
|
|
479
|
-
const h = result.launch.handle;
|
|
480
764
|
// Who asked is recorded, and it is not authority: the verb decides from the target's record and
|
|
481
765
|
// live rail facts alone, whoever the caller is.
|
|
482
766
|
const actor = attribution.actor === null
|
|
483
767
|
? ` actor: unattributed (the caller has no record-backed garden identity)\n`
|
|
484
768
|
: ` actor: ${attribution.actor} (record-backed caller; attribution, not authority)\n`;
|
|
769
|
+
// #157 B3: pi-durable has its own receipts (a reservation, an adoption), on either rail.
|
|
770
|
+
if (result.launch.backend === "pi-durable") {
|
|
771
|
+
return renderPiDurableResume(result.launch, result.observation, actor);
|
|
772
|
+
}
|
|
773
|
+
const h = result.launch.handle;
|
|
774
|
+
// The herdr rail has its own coordinates and its own words; the tmux text below stays as it was.
|
|
775
|
+
if (h.rail === "herdr")
|
|
776
|
+
return renderHerdrPiResume(result, h, actor);
|
|
777
|
+
if (h.rail === "herdr-layout-claude")
|
|
778
|
+
return renderHerdrClaudeResume(result, h, actor);
|
|
485
779
|
const window = ` runtime: ${h.runtimePath} (requested start cwd ${h.cwd})\n` +
|
|
486
780
|
` window: ${h.windowId} (index ${h.windowIndex}) in session ${h.sessionId}\n` +
|
|
487
781
|
` pane: ${h.paneId} pid ${h.panePid}\n`;
|
|
@@ -532,3 +826,174 @@ export function renderVisibleResume(result, attribution = { actor: null }) {
|
|
|
532
826
|
isError: false,
|
|
533
827
|
};
|
|
534
828
|
}
|
|
829
|
+
/**
|
|
830
|
+
* The pi-durable resume (#157 B3), on tmux or on herdr's one layout.apply. Three facts kept apart: the
|
|
831
|
+
* reservation this process holds, the LAUNCH that was asked to adopt it, and what the D claim said
|
|
832
|
+
* after one bounded read. `adopted` says a child wrote its adoption — not that it is ready, alive,
|
|
833
|
+
* attached, addressable or that anything was recalled; entwurf_peers is where its rail is read.
|
|
834
|
+
*/
|
|
835
|
+
function renderPiDurableResume(launch, observation, actor) {
|
|
836
|
+
const h = launch.handle;
|
|
837
|
+
const place = h.rail === "tmux" ? "window" : "tab";
|
|
838
|
+
const where = h.rail === "tmux"
|
|
839
|
+
? ` runtime: ${h.runtimePath} pi-durable --entwurf-adopt (requested start cwd ${h.cwd})\n` +
|
|
840
|
+
` window: ${h.windowId} (index ${h.windowIndex}) in session ${h.sessionId}\n` +
|
|
841
|
+
` pane: ${h.paneId} pid ${h.panePid}\n`
|
|
842
|
+
: ` runtime: ${h.runtime} pi-durable --entwurf-adopt (requested start cwd ${h.cwd})\n` +
|
|
843
|
+
` tab: ${h.herdrTabId} in workspace ${h.herdrWorkspaceId} — a NEW tab from ONE layout.apply, opened without taking focus\n` +
|
|
844
|
+
` pane: ${h.herdrPaneId} — herdr VIEW coordinates, not an address\n`;
|
|
845
|
+
const r = launch.reservation;
|
|
846
|
+
const heldBy = `this process (pid ${r.ownerPid})`;
|
|
847
|
+
const observed = observation.kind === "adopted"
|
|
848
|
+
? `OBSERVATION receipt: adopted — after ${observation.waitedMs}ms the claim of ${r.resource} carries generation ` +
|
|
849
|
+
`${r.generation} as its adoptedFrom: a child process wrote its adoption of this reservation. That is not ` +
|
|
850
|
+
`readiness, liveness, an attach, a turn or a recall — entwurf_peers shows whether its rail answers. This ` +
|
|
851
|
+
`call submitted no prompt.`
|
|
852
|
+
: observation.kind === "not-yet-adopted"
|
|
853
|
+
? `OBSERVATION receipt: not-yet-adopted — after ${observation.waitedMs}ms the claim of ${r.resource} was still ` +
|
|
854
|
+
`this reservation. The ${place} is open and visible; read it directly. The reservation stays held by ` +
|
|
855
|
+
`${heldBy}: a child there can still adopt it and become its holder; while it stays unadopted under this live ` +
|
|
856
|
+
`process a resume of ${launch.targetGardenId} refuses, and nothing clears it automatically. Nothing was ` +
|
|
857
|
+
`retried, withdrawn or killed.`
|
|
858
|
+
: observation.kind === "reservation-lost"
|
|
859
|
+
? `OBSERVATION receipt: reservation-lost — after ${observation.waitedMs}ms the claim of ${r.resource} ` +
|
|
860
|
+
`names neither this reservation nor an adoption of it (another owner, a reclaim or a withdrawal ` +
|
|
861
|
+
`replaced it). The ${place} is open; read it directly. Nothing was retried or killed.`
|
|
862
|
+
: `OBSERVATION receipt: unobserved — the claim of ${r.resource} could not be read after ` +
|
|
863
|
+
`${observation.waitedMs}ms (${observation.reason}: ${observation.detail}). The ${place} is open and ` +
|
|
864
|
+
`visible; nothing was retried, withdrawn or killed.`;
|
|
865
|
+
return {
|
|
866
|
+
text: `[entwurf resume call ↻${h.rail === "tmux" ? "" : " herdr"}]\n` +
|
|
867
|
+
` target: ${launch.targetGardenId}\n` +
|
|
868
|
+
` backend: pi-durable\n` +
|
|
869
|
+
` reserved: ${r.resource} at generation ${r.generation}, held by ${heldBy} for one child to adopt\n` +
|
|
870
|
+
where +
|
|
871
|
+
actor +
|
|
872
|
+
`\n` +
|
|
873
|
+
`LAUNCH receipt: ${h.rail === "tmux" ? "tmux created that window" : "herdr created that tab"} and was asked to ` +
|
|
874
|
+
`start the managed entwurf pi-durable with the closed adopt context of the reservation above, its raw ` +
|
|
875
|
+
`profile as PI_CODING_AGENT_DIR and its meta root's four ENTWURF_META_*_DIR children as its launch env, ` +
|
|
876
|
+
`in the record's cwd.\n` +
|
|
877
|
+
observed,
|
|
878
|
+
isError: false,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The pi resume on the herdr rail (#157 G1a). Same two receipts, kept apart the same way; the
|
|
883
|
+
* coordinates are herdr's VIEW of a tab, and herdr's own readiness or session report is not consulted
|
|
884
|
+
* — the same-gid control socket is the observation on both rails.
|
|
885
|
+
*/
|
|
886
|
+
function renderHerdrPiResume(result, h, actor) {
|
|
887
|
+
if (result.launch.backend !== "pi") {
|
|
888
|
+
throw new Error("entwurf-v2-visible-resume: a herdr launch handle arrived on a non-pi resume — only a pi resume renders through the herdr agent-start view");
|
|
889
|
+
}
|
|
890
|
+
const observation = result.observation;
|
|
891
|
+
const observed = observation.kind === "socket-alive";
|
|
892
|
+
return {
|
|
893
|
+
text: `[entwurf resume call ↻ herdr]\n` +
|
|
894
|
+
` target: ${result.launch.targetGardenId}\n` +
|
|
895
|
+
` session: ${result.launch.sessionFile}\n` +
|
|
896
|
+
` runtime: requested kind ${h.requestedKind} — herdr resolves the executable, we did not (requested start cwd ${h.cwd})\n` +
|
|
897
|
+
` tab: ${h.herdrTabId} in workspace ${h.herdrWorkspaceId} — a NEW tab beside the caller's, opened without taking focus\n` +
|
|
898
|
+
` pane: ${h.herdrPaneId} terminal ${h.herdrTerminalId} — herdr VIEW coordinates, not an address\n` +
|
|
899
|
+
` agent: ${h.herdrAgentName} (herdr's name for this launch, random per call)\n` +
|
|
900
|
+
actor +
|
|
901
|
+
`\n` +
|
|
902
|
+
`LAUNCH receipt: herdr created that tab and was asked to start pi on the transcript above.\n` +
|
|
903
|
+
(observed
|
|
904
|
+
? `OBSERVATION receipt: the control socket for ${result.launch.targetGardenId} answered after ` +
|
|
905
|
+
`${observation.waitedMs}ms — that citizen is addressable again, and entwurf_v2 fire-and-forget ` +
|
|
906
|
+
`now reaches it. No turn was run by this call. herdr's own agent status was not consulted.`
|
|
907
|
+
: `OBSERVATION receipt: resume-unobserved — the control socket did not answer within ` +
|
|
908
|
+
`${observation.waitedMs}ms. The tab is open and visible; read it directly. The lock was ` +
|
|
909
|
+
`released, nothing was retried, and nothing was closed.`),
|
|
910
|
+
isError: false,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* The Claude Code resume on the herdr rail (#157 stage 4). The same two receipts as on tmux, kept
|
|
915
|
+
* apart the same way. The tab and pane are herdr's VIEW; the pane's direct child is herdr's process
|
|
916
|
+
* report for the pane this call created — the pid the observation joins, never an address.
|
|
917
|
+
*/
|
|
918
|
+
function renderHerdrClaudeResume(result, h, actor) {
|
|
919
|
+
if (result.launch.backend !== "claude-code") {
|
|
920
|
+
throw new Error("entwurf-v2-visible-resume: a herdr Claude Code launch handle arrived on a non-Claude Code resume");
|
|
921
|
+
}
|
|
922
|
+
const target = result.launch.targetGardenId;
|
|
923
|
+
const observation = result.observation;
|
|
924
|
+
const child = "pid" in h.paneChild
|
|
925
|
+
? `pid ${h.paneChild.pid} (herdr's process report for that pane; a join, not an address)`
|
|
926
|
+
: `unread — ${h.paneChild.unread}`;
|
|
927
|
+
const left = "The tab is open and visible; read it directly. The lock was released, nothing was retried, and nothing was closed.";
|
|
928
|
+
return {
|
|
929
|
+
text: `[entwurf resume call ↻ herdr]\n` +
|
|
930
|
+
` target: ${target}\n` +
|
|
931
|
+
` backend: claude-code\n` +
|
|
932
|
+
` session: ${result.launch.nativeSessionId}\n` +
|
|
933
|
+
` runtime: ${h.runtime} --resume — herdr resolved the executable, we did not (requested start cwd ${h.cwd})\n` +
|
|
934
|
+
` tab: ${h.herdrTabId} in workspace ${h.herdrWorkspaceId} — a NEW tab from ONE layout.apply, opened without taking focus\n` +
|
|
935
|
+
` pane: ${h.herdrPaneId} — herdr VIEW coordinates, not an address\n` +
|
|
936
|
+
` child: ${child}\n` +
|
|
937
|
+
actor +
|
|
938
|
+
`\n` +
|
|
939
|
+
`LAUNCH receipt: herdr created that tab and was asked to start claude --resume on the session above.\n` +
|
|
940
|
+
(observation.kind === "receiver-armed"
|
|
941
|
+
? `OBSERVATION receipt: receiver-armed — a NEW live Claude Code process, the direct child of the pane this ` +
|
|
942
|
+
`call opened (pid ${observation.ownerPid}), re-armed the receiver marker of ${target} for the same session ` +
|
|
943
|
+
`after ${observation.waitedMs}ms, with a fresh arm binding the record also carries (same createdAt). ` +
|
|
944
|
+
`entwurf_v2 fire-and-forget now reaches its mailbox. This call submitted no prompt; the runtime's own ` +
|
|
945
|
+
`activity after resume (e.g. a doorbell on unread mail) is unmeasured.`
|
|
946
|
+
: observation.kind === "pane-process-unread"
|
|
947
|
+
? `OBSERVATION receipt: pane-process-unread — herdr did not report the direct child of the pane this ` +
|
|
948
|
+
`call opened (${observation.cause}), so no receiver marker could be joined to it and no wait ran. ${left}`
|
|
949
|
+
: observation.kind === "record-unreadable"
|
|
950
|
+
? `OBSERVATION receipt: record-unreadable — the record of ${target} could not be read after ` +
|
|
951
|
+
`${observation.waitedMs}ms: ${observation.cause}. ${left}`
|
|
952
|
+
: `OBSERVATION receipt: resume-unobserved — no new live receiver marker from the direct child of the pane ` +
|
|
953
|
+
`this call opened, bound to the record, appeared within ${observation.waitedMs}ms. ${left}`),
|
|
954
|
+
isError: false,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* A launch that failed after it created something. The header claims no more than the recovery
|
|
959
|
+
* knows — the fresh herdr rail learned that the hard way: `none` means herdr declined by name before
|
|
960
|
+
* making anything, `unknown` means no answer we can act on so a tab MAY exist, and anything else means
|
|
961
|
+
* the tab was created and the recovery says whether its pane was closed. No observation ran, so this
|
|
962
|
+
* says nothing about whether the citizen came back.
|
|
963
|
+
*/
|
|
964
|
+
function renderLaunchFailed(result) {
|
|
965
|
+
const f = result.failure;
|
|
966
|
+
const tabHeader = f.recovery.outcome === "none"
|
|
967
|
+
? "entwurf_resume_call failed at the herdr tab create step"
|
|
968
|
+
: f.recovery.outcome === "unknown"
|
|
969
|
+
? "entwurf_resume_call failed at the herdr tab create step with an UNKNOWN outcome"
|
|
970
|
+
: "entwurf_resume_call failed after the herdr tab was created";
|
|
971
|
+
// #157 B3: the pi-durable rail asks herdr for ONE layout.apply. Its classifier answers only none or
|
|
972
|
+
// unknown, so anything but none is UNKNOWN, and there is no tab create step to name.
|
|
973
|
+
const layout = f.rail === "herdr-layout";
|
|
974
|
+
const header = layout
|
|
975
|
+
? `entwurf_resume_call failed at the herdr layout.apply step${f.recovery.outcome === "none" ? "" : " with an UNKNOWN outcome"}`
|
|
976
|
+
: tabHeader;
|
|
977
|
+
const code = f.vendorCode === undefined ? "" : ` [herdr: ${f.vendorCode}]`;
|
|
978
|
+
const created = f.created === undefined
|
|
979
|
+
? ""
|
|
980
|
+
: ` created: tab ${f.created.herdrTabId} in workspace ${f.created.herdrWorkspaceId}, pane ${f.created.herdrPaneId} ` +
|
|
981
|
+
`terminal ${f.created.herdrTerminalId} — herdr VIEW coordinates, not an address\n`;
|
|
982
|
+
const recovery = f.recovery.outcome === "none"
|
|
983
|
+
? layout
|
|
984
|
+
? " recovery: none — the layout request was never written, or herdr declined it by name before making anything, so there is nothing to go look at\n"
|
|
985
|
+
: " recovery: none — herdr declined by name before making anything, so there is nothing to go look at\n"
|
|
986
|
+
: f.recovery.outcome === "unknown"
|
|
987
|
+
? " recovery: UNKNOWN — we never got an answer we can act on, so a tab MAY exist that this call cannot name. Nothing was closed; check herdr\n"
|
|
988
|
+
: f.recovery.outcome === "closed"
|
|
989
|
+
? ` recovery: closed ${f.recovery.paneId} (its terminal still matched the create receipt; the new tab went with it unless something else had joined it)\n`
|
|
990
|
+
: ` recovery: orphan-unreclaimed:${f.recovery.reason} (${f.recovery.paneId}) — this pane was NOT closed, on purpose\n`;
|
|
991
|
+
return {
|
|
992
|
+
text: `${header}: ${result.detail}.${code}\n` +
|
|
993
|
+
created +
|
|
994
|
+
recovery +
|
|
995
|
+
`No observation was attempted, so this says nothing about whether the citizen came back — entwurf_peers ` +
|
|
996
|
+
`shows whether its ${layout ? "rail" : "control socket"} answers. The lock was released and nothing was retried.`,
|
|
997
|
+
isError: true,
|
|
998
|
+
};
|
|
999
|
+
}
|