@junghanacs/entwurf 0.41.2 → 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 +4 -2
- package/BASELINE.md +110 -1
- package/CHANGELOG.md +328 -0
- package/CONTRIBUTING.md +1 -1
- package/ENTWURF-CLI.json +1229 -0
- package/ENTWURF.json +284 -0
- package/FAQ.md +4 -4
- package/README.md +125 -184
- 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 +284 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +46 -196
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +21 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +498 -29
- 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-fresh-call.js +23 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +68 -21
- 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/remote-pair-send.js +7 -4
- 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 +53 -60
- 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 +149 -327
- package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
- package/native/win32-x64/manifest.json +1 -1
- package/package.json +8 -4
- 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/agent-guidance.ts +341 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +25 -31
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +725 -54
- 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-fresh-call.ts +23 -26
- package/pi-extensions/lib/mux-resume-call.ts +95 -23
- 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/remote-pair-send.ts +7 -4
- package/pi-extensions/lib/resume-launch-identity.ts +31 -12
- package/pi-extensions/meta-bridge-pi-durable.ts +52 -79
- package/run.sh +417 -654
- package/scripts/build-bridge.sh +10 -0
- package/scripts/check-capability-bundle-reach.ts +86 -0
- package/scripts/check-claude-launch.ts +341 -0
- package/scripts/check-entwurf-self-address.ts +3 -3
- package/scripts/check-entwurf-v2-surface.ts +94 -98
- package/scripts/check-fresh-call-dispatch.ts +70 -9
- package/scripts/check-fresh-cut-gate.sh +23 -9
- package/scripts/check-gate-qualification.ts +24 -17
- 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 +48 -3
- 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 +160 -0
- package/scripts/mutants/bridge-boot-resume.json +6 -8
- package/scripts/mutants/capability-bundle-reach.json +124 -0
- package/scripts/mutants/claude-launch.json +142 -0
- package/scripts/mutants/cli-help-source.json +420 -0
- package/scripts/mutants/control-send-receipt.json +21 -4
- 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-fresh-call.json +14 -3
- package/scripts/mutants/mux-resume-call.json +89 -3
- package/scripts/mutants/peer-facts.json +15 -0
- package/scripts/mutants/pi-durable.json +996 -6
- package/scripts/mutants/remote-pair-send.json +21 -2
- package/scripts/mutants/self-address.json +10 -10
- package/scripts/mutants/source-install.json +35 -0
- package/scripts/mutants/v2-surface.json +50 -64
- package/scripts/mutants/v2-visible-resume.json +266 -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
|
|
@@ -73,7 +92,9 @@
|
|
|
73
92
|
* half reads the receiver marker and the record instead.
|
|
74
93
|
*/
|
|
75
94
|
|
|
95
|
+
import path from "node:path";
|
|
76
96
|
import { ACP_MODEL_NOT_CURATED_HINT, AcpModelNotCuratedError } from "./acp/model-ids.js";
|
|
97
|
+
import { ENTWURF_RESUME_REJECT_HINTS } from "./agent-guidance.ts";
|
|
77
98
|
import { buildResumePiArgs } from "./entwurf-resume-args.ts";
|
|
78
99
|
import {
|
|
79
100
|
type AcquireLockResult,
|
|
@@ -85,6 +106,10 @@ import {
|
|
|
85
106
|
} from "./entwurf-v2-lock.ts";
|
|
86
107
|
import {
|
|
87
108
|
classifyMarkerOwner,
|
|
109
|
+
defaultMetaMailboxDir,
|
|
110
|
+
defaultMetaReceiversDir,
|
|
111
|
+
defaultMetaSendersDir,
|
|
112
|
+
defaultMetaSessionsDir,
|
|
88
113
|
type MarkerOwnerVerdict,
|
|
89
114
|
metaReceiverMarkerPath,
|
|
90
115
|
parseMetaReceiverMarker,
|
|
@@ -92,10 +117,18 @@ import {
|
|
|
92
117
|
processStartKey,
|
|
93
118
|
readStoreRecordFile,
|
|
94
119
|
} from "./meta-session.ts";
|
|
120
|
+
import {
|
|
121
|
+
observePiDurableReservation,
|
|
122
|
+
PiDurableOwnerFenceError,
|
|
123
|
+
type PiDurableReservation,
|
|
124
|
+
type PiDurableReservationObservation,
|
|
125
|
+
type PiDurableWrittenClaim,
|
|
126
|
+
reservePiDurableSession,
|
|
127
|
+
withdrawPiDurableReservation,
|
|
128
|
+
} from "./pi-durable-owner-fence.ts";
|
|
95
129
|
import {
|
|
96
130
|
type ClaudeCodeLaunchIdentity,
|
|
97
131
|
type LaunchIdentity,
|
|
98
|
-
type ResumeBackend,
|
|
99
132
|
ResumeBackendUnsupportedError,
|
|
100
133
|
resolveResumeLaunchIdentity,
|
|
101
134
|
} from "./resume-launch-identity.ts";
|
|
@@ -125,7 +158,7 @@ export const RESUME_OBSERVE_INTERVAL_MS = 500;
|
|
|
125
158
|
* `target-backend-unsupported` IS in here, and the line between it and those throws is worth
|
|
126
159
|
* stating: asking to resume a Codex or agy citizen is not a defect at all. Nothing is corrupt, the
|
|
127
160
|
* record is exactly right, and the operator has simply reached past a capability boundary — only
|
|
128
|
-
* pi and Claude Code have a same-id resume. That deserves a named refusal a caller can act on, not
|
|
161
|
+
* pi, pi-durable and Claude Code have a same-id resume. That deserves a named refusal a caller can act on, not
|
|
129
162
|
* an error report about a store that is fine. The four `target-receiver-*` reasons are the Claude
|
|
130
163
|
* Code dormancy proof failing: nothing on disk proves the citizen is not running.
|
|
131
164
|
*/
|
|
@@ -143,9 +176,10 @@ export type VisibleResumeRejectReason =
|
|
|
143
176
|
| "target-address-conflict"
|
|
144
177
|
| "launch-refused";
|
|
145
178
|
|
|
146
|
-
/** What the injected launcher gives back. Structural on purpose: naming mux's types here
|
|
147
|
-
* be the import this split exists to avoid. */
|
|
148
|
-
export interface
|
|
179
|
+
/** What the injected launcher gives back. Structural on purpose: naming mux's or herdr's types here
|
|
180
|
+
* would be the import this split exists to avoid. `rail` says which one ran. */
|
|
181
|
+
export interface VisibleResumeTmuxLaunchHandle {
|
|
182
|
+
rail: "tmux";
|
|
149
183
|
serverPid: string;
|
|
150
184
|
sessionId: string;
|
|
151
185
|
windowId: string;
|
|
@@ -156,16 +190,118 @@ export interface VisibleResumeLaunchHandle {
|
|
|
156
190
|
cwd: string;
|
|
157
191
|
}
|
|
158
192
|
|
|
193
|
+
/** The herdr rail's handle (#157 G1a): what herdr was ASKED to start, the requested directory, and
|
|
194
|
+
* herdr's VIEW coordinates. No tmux field is invented for it — it has no server pid, window or
|
|
195
|
+
* resolved runtime path, and the pane pid the Claude Code observation joins on does not exist here. */
|
|
196
|
+
export interface VisibleResumeHerdrLaunchHandle {
|
|
197
|
+
rail: "herdr";
|
|
198
|
+
requestedKind: string;
|
|
199
|
+
cwd: string;
|
|
200
|
+
herdrAgentName: string;
|
|
201
|
+
herdrWorkspaceId: string;
|
|
202
|
+
herdrTabId: string;
|
|
203
|
+
herdrPaneId: string;
|
|
204
|
+
herdrTerminalId: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export type VisibleResumeLaunchHandle = VisibleResumeTmuxLaunchHandle | VisibleResumeHerdrLaunchHandle;
|
|
208
|
+
|
|
209
|
+
/** #157 B3: the herdr rail's pi-durable resume — ONE layout.apply, so herdr's layout reply is the whole
|
|
210
|
+
* receipt: the launcher asked for, the requested directory and herdr's VIEW coordinates. No agent name
|
|
211
|
+
* and no terminal id exist for it, which is why it carries its own rail tag: a pi resume handed one
|
|
212
|
+
* fails loud rather than rendering fields it never had. */
|
|
213
|
+
export interface VisibleResumeHerdrLayoutLaunchHandle {
|
|
214
|
+
rail: "herdr-layout";
|
|
215
|
+
runtime: string;
|
|
216
|
+
cwd: string;
|
|
217
|
+
herdrWorkspaceId: string;
|
|
218
|
+
herdrTabId: string;
|
|
219
|
+
herdrPaneId: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* #157 B3: the pi-durable adopt launch projection (L3-A1 §1) — EXACTLY five names: the SDK's own
|
|
224
|
+
* profile input carrying the reservation's raw profile, and the standard children of the
|
|
225
|
+
* reservation's ONE meta root. A fixed projection of those two coordinates, never an env dictionary.
|
|
226
|
+
*/
|
|
227
|
+
export interface PiDurableAdoptEnv {
|
|
228
|
+
PI_CODING_AGENT_DIR: string;
|
|
229
|
+
ENTWURF_META_SESSIONS_DIR: string;
|
|
230
|
+
ENTWURF_META_MAILBOX_DIR: string;
|
|
231
|
+
ENTWURF_META_SENDERS_DIR: string;
|
|
232
|
+
ENTWURF_META_RECEIVERS_DIR: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** What the launch seam is asked to open. pi and Claude Code carry the record's argv only; a
|
|
236
|
+
* pi-durable adoption also carries its fixed projection, and nothing else can. */
|
|
237
|
+
export type VisibleResumeLaunchInput =
|
|
238
|
+
| { backend: "pi" | "claude-code"; cwd: string; runtimeArgs: readonly string[] }
|
|
239
|
+
| { backend: "pi-durable"; cwd: string; runtimeArgs: readonly string[]; piDurableAdoptEnv: PiDurableAdoptEnv };
|
|
240
|
+
|
|
241
|
+
/** A launch that failed AFTER it created something (on herdr: after the tab create). The vendor's
|
|
242
|
+
* own code when it gave one, what is out there now, and the created coordinates when the create reply
|
|
243
|
+
* was read. Recovery is structural for the same reason the handles are. Only the herdr rail can fail
|
|
244
|
+
* after a mutation today (tmux opens its window in one step), and `rail` says so rather than leaving
|
|
245
|
+
* the renderer to assume it: `herdr-layout` is the pi-durable rail's ONE layout.apply (#157 B3), whose
|
|
246
|
+
* classifier answers only none or unknown and which has no tab create step of its own. */
|
|
247
|
+
export interface VisibleResumeLaunchFailure {
|
|
248
|
+
rail: "herdr" | "herdr-layout";
|
|
249
|
+
vendorCode?: string;
|
|
250
|
+
recovery:
|
|
251
|
+
| { outcome: "none" }
|
|
252
|
+
| { outcome: "unknown" }
|
|
253
|
+
| { outcome: "closed"; paneId: string; terminalId: string }
|
|
254
|
+
| { outcome: "orphan-unreclaimed"; paneId: string; reason: string };
|
|
255
|
+
created?: { herdrWorkspaceId: string; herdrTabId: string; herdrPaneId: string; herdrTerminalId: string };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* #157 stage 4: the herdr rail's Claude Code resume — ONE layout.apply, then ONE `pane.process_info`
|
|
260
|
+
* read of the pane it created. `paneChild` is herdr's report of that pane's DIRECT child (a layout
|
|
261
|
+
* argv pane spawns its program with no shell between, measured S4-CC-A H1), the twin of tmux's pane
|
|
262
|
+
* pid: a join for the observation, never an address. `runtime` is the name herdr was asked to run —
|
|
263
|
+
* herdr resolved it, we did not. Its own rail tag, so neither other resume can read it as its own.
|
|
264
|
+
*/
|
|
265
|
+
export interface VisibleResumeHerdrClaudeLaunchHandle {
|
|
266
|
+
rail: "herdr-layout-claude";
|
|
267
|
+
runtime: string;
|
|
268
|
+
cwd: string;
|
|
269
|
+
herdrWorkspaceId: string;
|
|
270
|
+
herdrTabId: string;
|
|
271
|
+
herdrPaneId: string;
|
|
272
|
+
paneChild: { pid: string } | { unread: string };
|
|
273
|
+
}
|
|
274
|
+
|
|
159
275
|
export type VisibleResumeLaunchOutcome =
|
|
160
|
-
| {
|
|
161
|
-
|
|
276
|
+
| {
|
|
277
|
+
ok: true;
|
|
278
|
+
handle: VisibleResumeLaunchHandle | VisibleResumeHerdrLayoutLaunchHandle | VisibleResumeHerdrClaudeLaunchHandle;
|
|
279
|
+
}
|
|
280
|
+
/** Refused before anything was created. */
|
|
281
|
+
| { ok: false; reason: string; hint: string }
|
|
282
|
+
| { ok: false; reason: string; hint: string; failure: VisibleResumeLaunchFailure };
|
|
162
283
|
|
|
163
|
-
/** Fact 1: tmux created a window
|
|
164
|
-
*
|
|
165
|
-
* observation of where the pane landed.
|
|
284
|
+
/** Fact 1: tmux created a window — or, inside herdr, herdr created a tab — and
|
|
285
|
+
* was asked to start the citizen's runtime there. `cwd` is the REQUESTED start directory (what the placement owner
|
|
286
|
+
* was told), the same kind of fact as `runtimePath` — not an observation of where the pane landed.
|
|
287
|
+
* The Claude Code receipt is tmux's window or herdr's layout tab: its observation joins that pane's
|
|
288
|
+
* direct child. */
|
|
166
289
|
export type ResumeLaunchReceipt =
|
|
167
290
|
| { backend: "pi"; targetGardenId: string; sessionFile: string; handle: VisibleResumeLaunchHandle }
|
|
168
|
-
| {
|
|
291
|
+
| {
|
|
292
|
+
backend: "claude-code";
|
|
293
|
+
targetGardenId: string;
|
|
294
|
+
nativeSessionId: string;
|
|
295
|
+
handle: VisibleResumeTmuxLaunchHandle | VisibleResumeHerdrClaudeLaunchHandle;
|
|
296
|
+
}
|
|
297
|
+
/** #157 B3: the reservation is part of the launch fact — the window or tab was opened FOR it. */
|
|
298
|
+
| {
|
|
299
|
+
backend: "pi-durable";
|
|
300
|
+
targetGardenId: string;
|
|
301
|
+
cwd: string;
|
|
302
|
+
reservation: { generation: string; resource: string; ownerPid: number };
|
|
303
|
+
handle: VisibleResumeTmuxLaunchHandle | VisibleResumeHerdrLayoutLaunchHandle;
|
|
304
|
+
};
|
|
169
305
|
|
|
170
306
|
/** Fact 2: the citizen's control socket came back under the SAME garden id, or it did not. This
|
|
171
307
|
* is the only fact in this module that says a citizen is back. */
|
|
@@ -183,7 +319,10 @@ export interface ResumeObservationReceipt {
|
|
|
183
319
|
export type ReceiverObservationReceipt =
|
|
184
320
|
| { kind: "receiver-armed"; ownerPid: number; waitedMs: number }
|
|
185
321
|
| { kind: "resume-unobserved"; waitedMs: number }
|
|
186
|
-
| { kind: "record-unreadable"; cause: string; waitedMs: number }
|
|
322
|
+
| { kind: "record-unreadable"; cause: string; waitedMs: number }
|
|
323
|
+
/** #157 stage 4, herdr only: herdr did not report the created pane's direct child, so there was no
|
|
324
|
+
* pid to join a marker to and no wait ran. The tab exists; nothing is closed. */
|
|
325
|
+
| { kind: "pane-process-unread"; cause: string; waitedMs: number };
|
|
187
326
|
|
|
188
327
|
/** What the receiver marker under a garden id says, read raw and judged by the shared owner rule. */
|
|
189
328
|
export type ReceiverInspection =
|
|
@@ -206,9 +345,29 @@ export type ReceiverInspection =
|
|
|
206
345
|
owner: MarkerOwnerVerdict;
|
|
207
346
|
};
|
|
208
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Fact 2 for pi-durable (#157 B3): what the reservation's D claim says, read by ONE bounded operation.
|
|
350
|
+
* `adopted` is an adoption WRITE by some child (the claim carries this reservation's generation as its
|
|
351
|
+
* `adoptedFrom`), not readiness, liveness, an attach or a turn; `not-yet-adopted` is still this
|
|
352
|
+
* reservation when the wait ended; `reservation-lost` is any other claim the strict reader accepted;
|
|
353
|
+
* `unobserved` is a claim that could not be read, with its named cause — never folded into lost.
|
|
354
|
+
*/
|
|
355
|
+
export type PiDurableAdoptionObservationReceipt =
|
|
356
|
+
| { kind: "adopted"; waitedMs: number }
|
|
357
|
+
| { kind: "not-yet-adopted"; waitedMs: number }
|
|
358
|
+
| { kind: "reservation-lost"; waitedMs: number }
|
|
359
|
+
| { kind: "unobserved"; reason: string; detail: string; waitedMs: number };
|
|
360
|
+
|
|
209
361
|
export type VisibleResumeResult =
|
|
210
|
-
| {
|
|
211
|
-
|
|
362
|
+
| {
|
|
363
|
+
ok: true;
|
|
364
|
+
launch: ResumeLaunchReceipt;
|
|
365
|
+
observation: ResumeObservationReceipt | ReceiverObservationReceipt | PiDurableAdoptionObservationReceipt;
|
|
366
|
+
}
|
|
367
|
+
| { ok: false; reason: VisibleResumeRejectReason; detail: string }
|
|
368
|
+
/** The launch failed after it created something. Not a refusal: something may be open, so this is
|
|
369
|
+
* never rendered as "No window was opened". No observation runs after it. */
|
|
370
|
+
| { ok: false; reason: "launch-failed"; detail: string; failure: VisibleResumeLaunchFailure };
|
|
212
371
|
|
|
213
372
|
export interface VisibleResumeDeps {
|
|
214
373
|
acquireLock: (gardenId: string) => AcquireLockResult;
|
|
@@ -218,12 +377,16 @@ export interface VisibleResumeDeps {
|
|
|
218
377
|
probeSocket: (socketPath: string) => Promise<SocketLiveness>;
|
|
219
378
|
/** The Claude Code dormancy proof and observation read this; pi never does. */
|
|
220
379
|
inspectReceiver: (gardenId: string) => ReceiverInspection;
|
|
221
|
-
/** The ONLY way this module reaches tmux. The surface supplies it
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
380
|
+
/** The ONLY way this module reaches tmux or herdr. The surface supplies it; the herdr rail is
|
|
381
|
+
* asynchronous (its CLI runner must not block the caller's event loop), so this is awaited. */
|
|
382
|
+
launch: (input: VisibleResumeLaunchInput) => VisibleResumeLaunchOutcome | Promise<VisibleResumeLaunchOutcome>;
|
|
383
|
+
/** #157 B3, pi-durable only: reserve D for one child, record-arm then S_D, both released before
|
|
384
|
+
* this returns. The per-target lock this module holds stays the outer scope and is never retaken. */
|
|
385
|
+
reservePiDurable: (gardenId: string) => Promise<PiDurableReservation>;
|
|
386
|
+
/** Withdraw THIS process's own reservation, only once its launch is definitely none-created. */
|
|
387
|
+
withdrawPiDurable: (reservation: PiDurableReservation) => Promise<void>;
|
|
388
|
+
/** What the reservation's D claim says now: a read, never a watch. */
|
|
389
|
+
observePiDurable: (reservation: PiDurableReservation) => PiDurableReservationObservation;
|
|
227
390
|
sleep: (ms: number) => Promise<void>;
|
|
228
391
|
now: () => number;
|
|
229
392
|
}
|
|
@@ -242,6 +405,20 @@ export function makeVisibleResumeDeps(
|
|
|
242
405
|
probeSocket: (socketPath) => probeSocketLiveness(socketPath),
|
|
243
406
|
inspectReceiver: (gid) => inspectReceiverMarker(gid),
|
|
244
407
|
launch,
|
|
408
|
+
// The four roots THIS process resolves now, handed to the reserve as given: a split shape is
|
|
409
|
+
// the reserve's own named refusal, never merged here.
|
|
410
|
+
reservePiDurable: (gid) =>
|
|
411
|
+
reservePiDurableSession({
|
|
412
|
+
gardenId: gid,
|
|
413
|
+
roots: {
|
|
414
|
+
sessionsDir: defaultMetaSessionsDir(),
|
|
415
|
+
mailboxDir: defaultMetaMailboxDir(),
|
|
416
|
+
sendersDir: defaultMetaSendersDir(),
|
|
417
|
+
receiversDir: defaultMetaReceiversDir(),
|
|
418
|
+
},
|
|
419
|
+
}),
|
|
420
|
+
withdrawPiDurable: (reservation) => withdrawPiDurableReservation(reservation),
|
|
421
|
+
observePiDurable: (reservation) => observePiDurableReservation(reservation),
|
|
245
422
|
sleep: (ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
|
|
246
423
|
now: () => Date.now(),
|
|
247
424
|
...overrides,
|
|
@@ -365,7 +542,7 @@ export async function visibleResume(targetGardenId: string, deps: VisibleResumeD
|
|
|
365
542
|
ok: false,
|
|
366
543
|
reason: err.reason,
|
|
367
544
|
detail:
|
|
368
|
-
`${targetGardenId} is a ${err.backend} citizen; only pi and Claude Code citizens can be reopened ` +
|
|
545
|
+
`${targetGardenId} is a ${err.backend} citizen; only pi, pi-durable and Claude Code citizens can be reopened ` +
|
|
369
546
|
`under the same garden id`,
|
|
370
547
|
};
|
|
371
548
|
}
|
|
@@ -373,6 +550,7 @@ export async function visibleResume(targetGardenId: string, deps: VisibleResumeD
|
|
|
373
550
|
}
|
|
374
551
|
|
|
375
552
|
if (identity.backend === "claude-code") return await resumeClaudeCode(targetGardenId, identity, deps);
|
|
553
|
+
if (identity.backend === "pi-durable") return await resumePiDurable(targetGardenId, deps);
|
|
376
554
|
|
|
377
555
|
// Liveness preflight, still under the lock. A live citizen is addressed with
|
|
378
556
|
// `entwurf_v2 fire-and-forget`, not reopened — and refusing here is also what stops two
|
|
@@ -411,9 +589,15 @@ export async function visibleResume(targetGardenId: string, deps: VisibleResumeD
|
|
|
411
589
|
provider: identity.provider,
|
|
412
590
|
model: identity.model,
|
|
413
591
|
});
|
|
414
|
-
const launched = deps.launch({ backend: "pi", cwd: identity.cwd, runtimeArgs });
|
|
415
|
-
if (!launched.ok)
|
|
416
|
-
|
|
592
|
+
const launched = await deps.launch({ backend: "pi", cwd: identity.cwd, runtimeArgs });
|
|
593
|
+
if (!launched.ok) return launchNotLaunched(launched);
|
|
594
|
+
// Only a pi-durable launch carries a layout handle (#157 B3). Handed to a pi resume, it is a
|
|
595
|
+
// wiring defect, not an outcome: whatever was opened stays as it is and the lock is released.
|
|
596
|
+
if (launched.handle.rail === "herdr-layout" || launched.handle.rail === "herdr-layout-claude") {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`entwurf-v2-visible-resume: the launcher handed a pi resume of ${targetGardenId} a ${launched.handle.rail} handle — ` +
|
|
599
|
+
`only a pi-durable or Claude Code resume launches through layout.apply. Nothing was observed or closed.`,
|
|
600
|
+
);
|
|
417
601
|
}
|
|
418
602
|
|
|
419
603
|
const observation = await observeSameGidSocket(targetGardenId, mapped.socketPath, deps);
|
|
@@ -427,6 +611,23 @@ export async function visibleResume(targetGardenId: string, deps: VisibleResumeD
|
|
|
427
611
|
}
|
|
428
612
|
}
|
|
429
613
|
|
|
614
|
+
/**
|
|
615
|
+
* A launch that did not become a launch, kept in its own shape. A refusal created nothing and is
|
|
616
|
+
* `launch-refused`. A failure that carries what it created (herdr: after the tab create) is
|
|
617
|
+
* `launch-failed`, with the vendor code, recovery and coordinates passed through untouched — folding
|
|
618
|
+
* it into a refusal is what would print "No window was opened" over a tab that may be on screen.
|
|
619
|
+
* Neither is retried, and neither is followed by an observation.
|
|
620
|
+
*/
|
|
621
|
+
function launchNotLaunched(
|
|
622
|
+
launched: Extract<VisibleResumeLaunchOutcome, { ok: false }>,
|
|
623
|
+
note = "",
|
|
624
|
+
): VisibleResumeResult {
|
|
625
|
+
const detail = `${launched.reason} — ${launched.hint}${note}`;
|
|
626
|
+
return "failure" in launched
|
|
627
|
+
? { ok: false, reason: "launch-failed", detail, failure: launched.failure }
|
|
628
|
+
: { ok: false, reason: "launch-refused", detail };
|
|
629
|
+
}
|
|
630
|
+
|
|
430
631
|
/**
|
|
431
632
|
* The Claude Code half, still under the caller's lock. Dormancy needs a DEAD owner verdict on the
|
|
432
633
|
* receiver marker AND the binding: the record re-read AFTER that verdict must still name the same
|
|
@@ -522,33 +723,294 @@ async function resumeClaudeCode(
|
|
|
522
723
|
};
|
|
523
724
|
}
|
|
524
725
|
|
|
525
|
-
const launched = deps.launch({
|
|
726
|
+
const launched = await deps.launch({
|
|
526
727
|
backend: "claude-code",
|
|
527
728
|
cwd: current.cwd,
|
|
528
729
|
runtimeArgs: ["--resume", current.nativeSessionId],
|
|
529
730
|
});
|
|
530
|
-
if (!launched.ok)
|
|
531
|
-
|
|
731
|
+
if (!launched.ok) return launchNotLaunched(launched);
|
|
732
|
+
// The observation below joins the receiver marker's owner to the pane's DIRECT child: tmux's pane
|
|
733
|
+
// pid, or herdr's process report for the layout pane (#157 stage 4). A handle of any other rail
|
|
734
|
+
// carries no such pid, so it is a wiring defect, not an outcome: it fails loud rather than watching
|
|
735
|
+
// for a pid that does not exist. Whatever that launcher opened is left as it is; the lock is still
|
|
736
|
+
// released.
|
|
737
|
+
const handle = launched.handle;
|
|
738
|
+
if (handle.rail !== "tmux" && handle.rail !== "herdr-layout-claude") {
|
|
739
|
+
throw new Error(
|
|
740
|
+
`entwurf-v2-visible-resume: the launcher handed a Claude Code resume of ${targetGardenId} a ${handle.rail} ` +
|
|
741
|
+
`handle — its observation joins the opened pane's direct child, which that rail does not carry. Nothing was ` +
|
|
742
|
+
`observed or closed.`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
const launch = { backend: "claude-code" as const, targetGardenId, nativeSessionId: current.nativeSessionId, handle };
|
|
746
|
+
// herdr did not name the new pane's direct child: there is nothing to join, so no wait runs. The tab
|
|
747
|
+
// is open; nothing is retried or closed.
|
|
748
|
+
if (handle.rail === "herdr-layout-claude" && "unread" in handle.paneChild) {
|
|
749
|
+
return {
|
|
750
|
+
ok: true,
|
|
751
|
+
launch,
|
|
752
|
+
observation: { kind: "pane-process-unread", cause: handle.paneChild.unread, waitedMs: 0 },
|
|
753
|
+
};
|
|
532
754
|
}
|
|
755
|
+
const panePid = handle.rail === "tmux" ? handle.panePid : (handle.paneChild as { pid: string }).pid;
|
|
533
756
|
const observation = await observeReceiverReattached(
|
|
534
757
|
targetGardenId,
|
|
535
758
|
current,
|
|
536
759
|
{ ownerPid: marker.ownerPid, ownerStartKey: marker.ownerStartKey, attachIncarnation: current.attachIncarnation },
|
|
537
|
-
|
|
760
|
+
panePid,
|
|
538
761
|
deps,
|
|
539
762
|
);
|
|
763
|
+
return { ok: true, launch, observation };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* #157 B3: the adopt launch for ONE reservation — the managed verb with its closed context
|
|
768
|
+
* `{v:1, gardenId, metaRoot, sourceGeneration}` (no session, cwd, profile, model, prompt, module,
|
|
769
|
+
* nonce or callback rides it), the reservation's cwd, and the fixed five-name projection of its raw
|
|
770
|
+
* profile and its meta root. Pure: it reads nothing and writes nothing.
|
|
771
|
+
*/
|
|
772
|
+
export function buildPiDurableAdoptLaunch(
|
|
773
|
+
reservation: Pick<PiDurableReservation, "gardenId" | "metaRoot" | "generation" | "profile" | "cwd">,
|
|
774
|
+
): Extract<VisibleResumeLaunchInput, { backend: "pi-durable" }> {
|
|
775
|
+
const { metaRoot } = reservation;
|
|
776
|
+
const context = {
|
|
777
|
+
v: 1,
|
|
778
|
+
gardenId: reservation.gardenId,
|
|
779
|
+
metaRoot,
|
|
780
|
+
sourceGeneration: reservation.generation,
|
|
781
|
+
};
|
|
782
|
+
return {
|
|
783
|
+
backend: "pi-durable",
|
|
784
|
+
cwd: reservation.cwd,
|
|
785
|
+
runtimeArgs: ["pi-durable", "--entwurf-adopt", JSON.stringify(context)],
|
|
786
|
+
piDurableAdoptEnv: {
|
|
787
|
+
PI_CODING_AGENT_DIR: reservation.profile,
|
|
788
|
+
ENTWURF_META_SESSIONS_DIR: path.join(metaRoot, "meta-sessions"),
|
|
789
|
+
ENTWURF_META_MAILBOX_DIR: path.join(metaRoot, "meta-mailbox"),
|
|
790
|
+
ENTWURF_META_SENDERS_DIR: path.join(metaRoot, "meta-senders"),
|
|
791
|
+
ENTWURF_META_RECEIVERS_DIR: path.join(metaRoot, "meta-receivers"),
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* A reserve refusal this verb answers by name; every other fence refusal keeps its own cause. A live holder is not
|
|
798
|
+
* an address (S3 joint review D4): a reservation's parent is a bridge waiting for a child, which entwurf_v2 cannot
|
|
799
|
+
* reach, and any other live holder is a process this verb did not judge contact-ready — so neither answer tells the
|
|
800
|
+
* caller to send. The branch reads the refusal's structured holder state, never its message.
|
|
801
|
+
*/
|
|
802
|
+
function piDurableReserveRefusal(targetGardenId: string, err: PiDurableOwnerFenceError): VisibleResumeResult | null {
|
|
803
|
+
if (err.reason === "owner-live") {
|
|
804
|
+
if (err.holderState === "reserved") {
|
|
805
|
+
return {
|
|
806
|
+
ok: false,
|
|
807
|
+
reason: "target-live",
|
|
808
|
+
detail:
|
|
809
|
+
`${targetGardenId}'s pi-durable session is reserved for a child to adopt by a live process (${err.message}); ` +
|
|
810
|
+
`while that reservation stays unadopted under that live process a resume of ${targetGardenId} refuses on it; ` +
|
|
811
|
+
`nothing clears it automatically, a child that adopts it becomes its holder, and entwurf_v2 does not reach a reservation`,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
ok: false,
|
|
816
|
+
reason: "target-live",
|
|
817
|
+
detail:
|
|
818
|
+
`${targetGardenId}'s pi-durable session is held by a live process (${err.message}); if that process is the ` +
|
|
819
|
+
`running citizen, entwurf_v2 reaches it only through the rail its own dispatch decides`,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (err.reason === "owner-uncertain" || err.reason === "owner-other-host") {
|
|
823
|
+
return {
|
|
824
|
+
ok: false,
|
|
825
|
+
reason: "target-indeterminate",
|
|
826
|
+
detail:
|
|
827
|
+
`the holder of ${targetGardenId}'s pi-durable session could not be proven gone on this host (${err.message}), ` +
|
|
828
|
+
`and resuming a session that is still open would put two processes on one transcript`,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const messageOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* A reserve whose write committed and whose lock release then failed (S3 joint review D3). It says only what that
|
|
838
|
+
* call wrote and what its holder implies; the release's own words say what it left. Nothing is withdrawn, retried
|
|
839
|
+
* or repaired here.
|
|
840
|
+
*/
|
|
841
|
+
function piDurableReservedThenFailed(
|
|
842
|
+
targetGardenId: string,
|
|
843
|
+
err: PiDurableOwnerFenceError,
|
|
844
|
+
written: PiDurableWrittenClaim,
|
|
845
|
+
): string {
|
|
846
|
+
const residue =
|
|
847
|
+
err.reason === "tx-release-failed"
|
|
848
|
+
? `; if the transaction file that release named remains, that process ending does not remove it, and it fails closed until an operator removes it`
|
|
849
|
+
: "";
|
|
850
|
+
return (
|
|
851
|
+
`entwurf-v2-visible-resume: the reserve of ${targetGardenId} WROTE its reservation of ${written.resource} at ` +
|
|
852
|
+
`generation ${written.generation} for pid ${written.ownerPid}, then a lock release failed (${err.message}). ` +
|
|
853
|
+
`Nothing was launched and nothing withdrew that reservation: while it stays unadopted under live pid ` +
|
|
854
|
+
`${written.ownerPid}, a resume of ${targetGardenId} refuses, and nothing clears it automatically${residue}`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* A withdrawal that failed (S3 joint review D1). Only what the failure itself records is stated: a write this call
|
|
860
|
+
* made, or a transaction file the release left; any other failure leaves the claim's state unestablished here.
|
|
861
|
+
*/
|
|
862
|
+
function piDurableWithdrawFailed(
|
|
863
|
+
targetGardenId: string,
|
|
864
|
+
held: string,
|
|
865
|
+
reservation: PiDurableReservation,
|
|
866
|
+
err: unknown,
|
|
867
|
+
): string {
|
|
868
|
+
const residue = `if that transaction file remains, this process ending does not remove it, and it fails closed until an operator removes it`;
|
|
869
|
+
if (err instanceof PiDurableOwnerFenceError && err.written !== undefined) {
|
|
870
|
+
return (
|
|
871
|
+
` — this call wrote the withdrawal of ${held} (the claim back to the donor's ${err.written.state} row at ` +
|
|
872
|
+
`generation ${err.written.generation}), then releasing its transaction failed (${err.message}); ${residue}`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (err instanceof PiDurableOwnerFenceError && err.reason === "tx-release-failed") {
|
|
876
|
+
return (
|
|
877
|
+
` — withdrawing ${held} FAILED at its transaction release (${err.message}); whether this call ` +
|
|
878
|
+
`wrote anything is not recorded, and ${residue}`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return (
|
|
882
|
+
` — withdrawing ${held} FAILED (${messageOf(err)}); what its claim holds now is not established here: while it ` +
|
|
883
|
+
`stays reserved, unadopted, under this live process (pid ${reservation.owner.pid}), a resume of ${targetGardenId} ` +
|
|
884
|
+
`refuses`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* The pi-durable half (#157 B3), still under the caller's per-target lock, which stays the outer
|
|
890
|
+
* scope: the reserve takes record-arm then S_D itself and releases both before it returns, so before
|
|
891
|
+
* any launch. A reserve that refuses on eligibility writes nothing; a reserve whose lock release fails AFTER its
|
|
892
|
+
* write is named as such (its `written`), with the reservation it left. Neither opens anything.
|
|
893
|
+
*
|
|
894
|
+
* From the reservation on, this process holds D for one child, and what happens to that hold is
|
|
895
|
+
* decided by the launch outcome alone (L3-A1 §6), never by a guess:
|
|
896
|
+
*
|
|
897
|
+
* no launch request was made definitely none created → withdraw
|
|
898
|
+
* the seam refused (`ok:false`, no `failure`) the seam's contract: refused before anything was
|
|
899
|
+
* created (tmux: one of its named reasons; herdr:
|
|
900
|
+
* decided before the layout write) → withdraw
|
|
901
|
+
* a failure whose recovery is `none` that rail's own none-created certainty → withdraw
|
|
902
|
+
* a failure whose recovery is anything else something MAY exist → the reservation is KEPT
|
|
903
|
+
* the launcher threw unknown, whatever its position → KEPT
|
|
904
|
+
* a launch a child may adopt it → KEPT, then observed once
|
|
905
|
+
*
|
|
906
|
+
* A withdrawal that fails is reported with the refusal it followed and is never retried. A kept
|
|
907
|
+
* reservation is said so in the answer: it names this process as its holder, and while it stays unadopted
|
|
908
|
+
* under this live process a later resume of the same id refuses; nothing clears it automatically, and a
|
|
909
|
+
* child that adopts it becomes its holder.
|
|
910
|
+
*/
|
|
911
|
+
async function resumePiDurable(targetGardenId: string, deps: VisibleResumeDeps): Promise<VisibleResumeResult> {
|
|
912
|
+
let reservation: PiDurableReservation;
|
|
913
|
+
try {
|
|
914
|
+
reservation = await deps.reservePiDurable(targetGardenId);
|
|
915
|
+
} catch (err) {
|
|
916
|
+
if (err instanceof PiDurableOwnerFenceError) {
|
|
917
|
+
const refused = piDurableReserveRefusal(targetGardenId, err);
|
|
918
|
+
if (refused !== null) return refused;
|
|
919
|
+
// A release that failed after the reservation was WRITTEN (S3 joint review D3): the reservation exists,
|
|
920
|
+
// so the answer names it and what it blocks, beside the release's own cause; nothing launches or withdraws.
|
|
921
|
+
if (err.written !== undefined) throw new Error(piDurableReservedThenFailed(targetGardenId, err, err.written));
|
|
922
|
+
}
|
|
923
|
+
throw err;
|
|
924
|
+
}
|
|
925
|
+
const held = `${targetGardenId}'s reservation of ${reservation.resource} at generation ${reservation.generation}`;
|
|
926
|
+
const withdraw = async (): Promise<string> => {
|
|
927
|
+
try {
|
|
928
|
+
await deps.withdrawPiDurable(reservation);
|
|
929
|
+
return ` — ${held} was withdrawn, since nothing was created`;
|
|
930
|
+
} catch (err) {
|
|
931
|
+
return piDurableWithdrawFailed(targetGardenId, held, reservation, err);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
const kept =
|
|
935
|
+
` — ${held} was KEPT, because something may exist that can still adopt it; it names this process ` +
|
|
936
|
+
`(pid ${reservation.owner.pid}) as its holder: while it stays unadopted under this live process a resume of ` +
|
|
937
|
+
`${targetGardenId} refuses, nothing clears it automatically, and a child that adopts it becomes its holder`;
|
|
938
|
+
|
|
939
|
+
let input: ReturnType<typeof buildPiDurableAdoptLaunch>;
|
|
940
|
+
try {
|
|
941
|
+
input = buildPiDurableAdoptLaunch(reservation);
|
|
942
|
+
} catch (err) {
|
|
943
|
+
throw new Error(`entwurf-v2-visible-resume: ${messageOf(err)}${await withdraw()}`);
|
|
944
|
+
}
|
|
945
|
+
let launched: VisibleResumeLaunchOutcome;
|
|
946
|
+
try {
|
|
947
|
+
launched = await deps.launch(input);
|
|
948
|
+
} catch (err) {
|
|
949
|
+
throw new Error(
|
|
950
|
+
`entwurf-v2-visible-resume: the pi-durable launch of ${targetGardenId} threw: ${messageOf(err)}${kept}`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (!launched.ok) {
|
|
954
|
+
const noneCreated = !("failure" in launched) || launched.failure.recovery.outcome === "none";
|
|
955
|
+
return launchNotLaunched(launched, noneCreated ? await withdraw() : kept);
|
|
956
|
+
}
|
|
957
|
+
// A herdr agent-start handle belongs to the pi rail only: a pi-durable resume launches through
|
|
958
|
+
// tmux or ONE layout.apply. Handed one, it is a wiring defect; what was opened stays as it is.
|
|
959
|
+
if (launched.handle.rail === "herdr" || launched.handle.rail === "herdr-layout-claude") {
|
|
960
|
+
throw new Error(
|
|
961
|
+
`entwurf-v2-visible-resume: the launcher handed a pi-durable resume of ${targetGardenId} a ` +
|
|
962
|
+
`${launched.handle.rail === "herdr" ? "herdr agent-start" : launched.handle.rail} handle — nothing was observed or closed${kept}`,
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
const observation = await observePiDurableAdoption(reservation, deps);
|
|
540
966
|
return {
|
|
541
967
|
ok: true,
|
|
542
968
|
launch: {
|
|
543
|
-
backend: "
|
|
969
|
+
backend: "pi-durable",
|
|
544
970
|
targetGardenId,
|
|
545
|
-
|
|
971
|
+
cwd: reservation.cwd,
|
|
972
|
+
reservation: {
|
|
973
|
+
generation: reservation.generation,
|
|
974
|
+
resource: reservation.resource,
|
|
975
|
+
ownerPid: reservation.owner.pid,
|
|
976
|
+
},
|
|
546
977
|
handle: launched.handle,
|
|
547
978
|
},
|
|
548
979
|
observation,
|
|
549
980
|
};
|
|
550
981
|
}
|
|
551
982
|
|
|
983
|
+
/**
|
|
984
|
+
* ONE bounded operation, the pi-durable twin of `observeSameGidSocket` (#157 B3): read the
|
|
985
|
+
* reservation's D claim until a child's adoption write carries its generation, the claim is lost or
|
|
986
|
+
* unreadable, or the same fixed wait ends. Every read is the fence's strict reader; nothing here
|
|
987
|
+
* writes, withdraws, retries or watches past the deadline. A read that throws past that reader stops
|
|
988
|
+
* the wait as `unobserved` with its cause: the window is open, and the launch receipt must survive.
|
|
989
|
+
*/
|
|
990
|
+
export async function observePiDurableAdoption(
|
|
991
|
+
reservation: PiDurableReservation,
|
|
992
|
+
deps: Pick<VisibleResumeDeps, "observePiDurable" | "sleep" | "now">,
|
|
993
|
+
timeoutMs: number = RESUME_OBSERVE_TIMEOUT_MS,
|
|
994
|
+
intervalMs: number = RESUME_OBSERVE_INTERVAL_MS,
|
|
995
|
+
): Promise<PiDurableAdoptionObservationReceipt> {
|
|
996
|
+
const started = deps.now();
|
|
997
|
+
for (;;) {
|
|
998
|
+
let seen: PiDurableReservationObservation;
|
|
999
|
+
try {
|
|
1000
|
+
seen = deps.observePiDurable(reservation);
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
return { kind: "unobserved", reason: "observe-threw", detail: messageOf(err), waitedMs: deps.now() - started };
|
|
1003
|
+
}
|
|
1004
|
+
if (seen.class === "adopted") return { kind: "adopted", waitedMs: deps.now() - started };
|
|
1005
|
+
if (seen.class === "lost") return { kind: "reservation-lost", waitedMs: deps.now() - started };
|
|
1006
|
+
if (seen.class === "unobserved") {
|
|
1007
|
+
return { kind: "unobserved", reason: seen.reason, detail: seen.detail, waitedMs: deps.now() - started };
|
|
1008
|
+
}
|
|
1009
|
+
if (deps.now() - started >= timeoutMs) return { kind: "not-yet-adopted", waitedMs: deps.now() - started };
|
|
1010
|
+
await deps.sleep(intervalMs);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
552
1014
|
/**
|
|
553
1015
|
* ONE bounded operation, the Claude Code twin of `observeSameGidSocket`: wait for a NEW live owner
|
|
554
1016
|
* — in the pane this call opened (`panePid`), a join, not an address — to re-arm this garden id's
|
|
@@ -633,24 +1095,22 @@ export async function observeSameGidSocket(
|
|
|
633
1095
|
}
|
|
634
1096
|
}
|
|
635
1097
|
|
|
1098
|
+
// The static record/liveness hint text is authored in root ENTWURF.json (#154 C1-C); the reasons,
|
|
1099
|
+
// this total table and the renderer stay here. acp-model-not-curated keeps the shared ACP constant,
|
|
1100
|
+
// and the launch-stage mux hints folded into `detail` stay behind the mux import fence.
|
|
636
1101
|
const REJECT_HINT: Record<VisibleResumeRejectReason, string> = {
|
|
637
|
-
"target-invalid": "
|
|
638
|
-
"target-backend-unsupported":
|
|
639
|
-
"that citizen's backend has no same-id resume (only pi and Claude Code do) — reach it with entwurf_v2, or open a fresh sibling",
|
|
1102
|
+
"target-invalid": ENTWURF_RESUME_REJECT_HINTS["target-invalid"],
|
|
1103
|
+
"target-backend-unsupported": ENTWURF_RESUME_REJECT_HINTS["target-backend-unsupported"],
|
|
640
1104
|
"acp-model-not-curated": ACP_MODEL_NOT_CURATED_HINT,
|
|
641
|
-
"target-locked": "
|
|
642
|
-
"target-live": "
|
|
643
|
-
"target-indeterminate":
|
|
644
|
-
|
|
645
|
-
"target-receiver-
|
|
646
|
-
|
|
647
|
-
"target-receiver-
|
|
648
|
-
"target-
|
|
649
|
-
|
|
650
|
-
"target-receiver-unbound":
|
|
651
|
-
"the dead receiver owner is not bound to the record's last arm (none observed, a later attach, or the record moved), so a Claude Code process may still hold the session — if its window is closed, reopen it yourself",
|
|
652
|
-
"target-address-conflict": "that citizen's control-socket path is corrupt",
|
|
653
|
-
"launch-refused": "tmux was never asked to open a window",
|
|
1105
|
+
"target-locked": ENTWURF_RESUME_REJECT_HINTS["target-locked"],
|
|
1106
|
+
"target-live": ENTWURF_RESUME_REJECT_HINTS["target-live"],
|
|
1107
|
+
"target-indeterminate": ENTWURF_RESUME_REJECT_HINTS["target-indeterminate"],
|
|
1108
|
+
"target-receiver-absent": ENTWURF_RESUME_REJECT_HINTS["target-receiver-absent"],
|
|
1109
|
+
"target-receiver-corrupt": ENTWURF_RESUME_REJECT_HINTS["target-receiver-corrupt"],
|
|
1110
|
+
"target-receiver-mismatch": ENTWURF_RESUME_REJECT_HINTS["target-receiver-mismatch"],
|
|
1111
|
+
"target-receiver-unbound": ENTWURF_RESUME_REJECT_HINTS["target-receiver-unbound"],
|
|
1112
|
+
"target-address-conflict": ENTWURF_RESUME_REJECT_HINTS["target-address-conflict"],
|
|
1113
|
+
"launch-refused": ENTWURF_RESUME_REJECT_HINTS["launch-refused"],
|
|
654
1114
|
};
|
|
655
1115
|
|
|
656
1116
|
/**
|
|
@@ -667,6 +1127,7 @@ export function renderVisibleResume(
|
|
|
667
1127
|
attribution: { actor: string | null } = { actor: null },
|
|
668
1128
|
): { text: string; isError: boolean } {
|
|
669
1129
|
if (!result.ok) {
|
|
1130
|
+
if (result.reason === "launch-failed") return renderLaunchFailed(result);
|
|
670
1131
|
return {
|
|
671
1132
|
text:
|
|
672
1133
|
`entwurf_resume_call rejected: ${result.reason} — ${REJECT_HINT[result.reason]}. ` +
|
|
@@ -674,13 +1135,20 @@ export function renderVisibleResume(
|
|
|
674
1135
|
isError: true,
|
|
675
1136
|
};
|
|
676
1137
|
}
|
|
677
|
-
const h = result.launch.handle;
|
|
678
1138
|
// Who asked is recorded, and it is not authority: the verb decides from the target's record and
|
|
679
1139
|
// live rail facts alone, whoever the caller is.
|
|
680
1140
|
const actor =
|
|
681
1141
|
attribution.actor === null
|
|
682
1142
|
? ` actor: unattributed (the caller has no record-backed garden identity)\n`
|
|
683
1143
|
: ` actor: ${attribution.actor} (record-backed caller; attribution, not authority)\n`;
|
|
1144
|
+
// #157 B3: pi-durable has its own receipts (a reservation, an adoption), on either rail.
|
|
1145
|
+
if (result.launch.backend === "pi-durable") {
|
|
1146
|
+
return renderPiDurableResume(result.launch, result.observation as PiDurableAdoptionObservationReceipt, actor);
|
|
1147
|
+
}
|
|
1148
|
+
const h = result.launch.handle;
|
|
1149
|
+
// The herdr rail has its own coordinates and its own words; the tmux text below stays as it was.
|
|
1150
|
+
if (h.rail === "herdr") return renderHerdrPiResume(result, h, actor);
|
|
1151
|
+
if (h.rail === "herdr-layout-claude") return renderHerdrClaudeResume(result, h, actor);
|
|
684
1152
|
const window =
|
|
685
1153
|
` runtime: ${h.runtimePath} (requested start cwd ${h.cwd})\n` +
|
|
686
1154
|
` window: ${h.windowId} (index ${h.windowIndex}) in session ${h.sessionId}\n` +
|
|
@@ -734,3 +1202,206 @@ export function renderVisibleResume(
|
|
|
734
1202
|
isError: false,
|
|
735
1203
|
};
|
|
736
1204
|
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* The pi-durable resume (#157 B3), on tmux or on herdr's one layout.apply. Three facts kept apart: the
|
|
1208
|
+
* reservation this process holds, the LAUNCH that was asked to adopt it, and what the D claim said
|
|
1209
|
+
* after one bounded read. `adopted` says a child wrote its adoption — not that it is ready, alive,
|
|
1210
|
+
* attached, addressable or that anything was recalled; entwurf_peers is where its rail is read.
|
|
1211
|
+
*/
|
|
1212
|
+
function renderPiDurableResume(
|
|
1213
|
+
launch: Extract<ResumeLaunchReceipt, { backend: "pi-durable" }>,
|
|
1214
|
+
observation: PiDurableAdoptionObservationReceipt,
|
|
1215
|
+
actor: string,
|
|
1216
|
+
): { text: string; isError: boolean } {
|
|
1217
|
+
const h = launch.handle;
|
|
1218
|
+
const place = h.rail === "tmux" ? "window" : "tab";
|
|
1219
|
+
const where =
|
|
1220
|
+
h.rail === "tmux"
|
|
1221
|
+
? ` runtime: ${h.runtimePath} pi-durable --entwurf-adopt (requested start cwd ${h.cwd})\n` +
|
|
1222
|
+
` window: ${h.windowId} (index ${h.windowIndex}) in session ${h.sessionId}\n` +
|
|
1223
|
+
` pane: ${h.paneId} pid ${h.panePid}\n`
|
|
1224
|
+
: ` runtime: ${h.runtime} pi-durable --entwurf-adopt (requested start cwd ${h.cwd})\n` +
|
|
1225
|
+
` tab: ${h.herdrTabId} in workspace ${h.herdrWorkspaceId} — a NEW tab from ONE layout.apply, opened without taking focus\n` +
|
|
1226
|
+
` pane: ${h.herdrPaneId} — herdr VIEW coordinates, not an address\n`;
|
|
1227
|
+
const r = launch.reservation;
|
|
1228
|
+
const heldBy = `this process (pid ${r.ownerPid})`;
|
|
1229
|
+
const observed =
|
|
1230
|
+
observation.kind === "adopted"
|
|
1231
|
+
? `OBSERVATION receipt: adopted — after ${observation.waitedMs}ms the claim of ${r.resource} carries generation ` +
|
|
1232
|
+
`${r.generation} as its adoptedFrom: a child process wrote its adoption of this reservation. That is not ` +
|
|
1233
|
+
`readiness, liveness, an attach, a turn or a recall — entwurf_peers shows whether its rail answers. This ` +
|
|
1234
|
+
`call submitted no prompt.`
|
|
1235
|
+
: observation.kind === "not-yet-adopted"
|
|
1236
|
+
? `OBSERVATION receipt: not-yet-adopted — after ${observation.waitedMs}ms the claim of ${r.resource} was still ` +
|
|
1237
|
+
`this reservation. The ${place} is open and visible; read it directly. The reservation stays held by ` +
|
|
1238
|
+
`${heldBy}: a child there can still adopt it and become its holder; while it stays unadopted under this live ` +
|
|
1239
|
+
`process a resume of ${launch.targetGardenId} refuses, and nothing clears it automatically. Nothing was ` +
|
|
1240
|
+
`retried, withdrawn or killed.`
|
|
1241
|
+
: observation.kind === "reservation-lost"
|
|
1242
|
+
? `OBSERVATION receipt: reservation-lost — after ${observation.waitedMs}ms the claim of ${r.resource} ` +
|
|
1243
|
+
`names neither this reservation nor an adoption of it (another owner, a reclaim or a withdrawal ` +
|
|
1244
|
+
`replaced it). The ${place} is open; read it directly. Nothing was retried or killed.`
|
|
1245
|
+
: `OBSERVATION receipt: unobserved — the claim of ${r.resource} could not be read after ` +
|
|
1246
|
+
`${observation.waitedMs}ms (${observation.reason}: ${observation.detail}). The ${place} is open and ` +
|
|
1247
|
+
`visible; nothing was retried, withdrawn or killed.`;
|
|
1248
|
+
return {
|
|
1249
|
+
text:
|
|
1250
|
+
`[entwurf resume call ↻${h.rail === "tmux" ? "" : " herdr"}]\n` +
|
|
1251
|
+
` target: ${launch.targetGardenId}\n` +
|
|
1252
|
+
` backend: pi-durable\n` +
|
|
1253
|
+
` reserved: ${r.resource} at generation ${r.generation}, held by ${heldBy} for one child to adopt\n` +
|
|
1254
|
+
where +
|
|
1255
|
+
actor +
|
|
1256
|
+
`\n` +
|
|
1257
|
+
`LAUNCH receipt: ${h.rail === "tmux" ? "tmux created that window" : "herdr created that tab"} and was asked to ` +
|
|
1258
|
+
`start the managed entwurf pi-durable with the closed adopt context of the reservation above, its raw ` +
|
|
1259
|
+
`profile as PI_CODING_AGENT_DIR and its meta root's four ENTWURF_META_*_DIR children as its launch env, ` +
|
|
1260
|
+
`in the record's cwd.\n` +
|
|
1261
|
+
observed,
|
|
1262
|
+
isError: false,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* The pi resume on the herdr rail (#157 G1a). Same two receipts, kept apart the same way; the
|
|
1268
|
+
* coordinates are herdr's VIEW of a tab, and herdr's own readiness or session report is not consulted
|
|
1269
|
+
* — the same-gid control socket is the observation on both rails.
|
|
1270
|
+
*/
|
|
1271
|
+
function renderHerdrPiResume(
|
|
1272
|
+
result: Extract<VisibleResumeResult, { ok: true }>,
|
|
1273
|
+
h: VisibleResumeHerdrLaunchHandle,
|
|
1274
|
+
actor: string,
|
|
1275
|
+
): { text: string; isError: boolean } {
|
|
1276
|
+
if (result.launch.backend !== "pi") {
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
"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",
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
const observation = result.observation as ResumeObservationReceipt;
|
|
1282
|
+
const observed = observation.kind === "socket-alive";
|
|
1283
|
+
return {
|
|
1284
|
+
text:
|
|
1285
|
+
`[entwurf resume call ↻ herdr]\n` +
|
|
1286
|
+
` target: ${result.launch.targetGardenId}\n` +
|
|
1287
|
+
` session: ${result.launch.sessionFile}\n` +
|
|
1288
|
+
` runtime: requested kind ${h.requestedKind} — herdr resolves the executable, we did not (requested start cwd ${h.cwd})\n` +
|
|
1289
|
+
` tab: ${h.herdrTabId} in workspace ${h.herdrWorkspaceId} — a NEW tab beside the caller's, opened without taking focus\n` +
|
|
1290
|
+
` pane: ${h.herdrPaneId} terminal ${h.herdrTerminalId} — herdr VIEW coordinates, not an address\n` +
|
|
1291
|
+
` agent: ${h.herdrAgentName} (herdr's name for this launch, random per call)\n` +
|
|
1292
|
+
actor +
|
|
1293
|
+
`\n` +
|
|
1294
|
+
`LAUNCH receipt: herdr created that tab and was asked to start pi on the transcript above.\n` +
|
|
1295
|
+
(observed
|
|
1296
|
+
? `OBSERVATION receipt: the control socket for ${result.launch.targetGardenId} answered after ` +
|
|
1297
|
+
`${observation.waitedMs}ms — that citizen is addressable again, and entwurf_v2 fire-and-forget ` +
|
|
1298
|
+
`now reaches it. No turn was run by this call. herdr's own agent status was not consulted.`
|
|
1299
|
+
: `OBSERVATION receipt: resume-unobserved — the control socket did not answer within ` +
|
|
1300
|
+
`${observation.waitedMs}ms. The tab is open and visible; read it directly. The lock was ` +
|
|
1301
|
+
`released, nothing was retried, and nothing was closed.`),
|
|
1302
|
+
isError: false,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* The Claude Code resume on the herdr rail (#157 stage 4). The same two receipts as on tmux, kept
|
|
1308
|
+
* apart the same way. The tab and pane are herdr's VIEW; the pane's direct child is herdr's process
|
|
1309
|
+
* report for the pane this call created — the pid the observation joins, never an address.
|
|
1310
|
+
*/
|
|
1311
|
+
function renderHerdrClaudeResume(
|
|
1312
|
+
result: Extract<VisibleResumeResult, { ok: true }>,
|
|
1313
|
+
h: VisibleResumeHerdrClaudeLaunchHandle,
|
|
1314
|
+
actor: string,
|
|
1315
|
+
): { text: string; isError: boolean } {
|
|
1316
|
+
if (result.launch.backend !== "claude-code") {
|
|
1317
|
+
throw new Error("entwurf-v2-visible-resume: a herdr Claude Code launch handle arrived on a non-Claude Code resume");
|
|
1318
|
+
}
|
|
1319
|
+
const target = result.launch.targetGardenId;
|
|
1320
|
+
const observation = result.observation as ReceiverObservationReceipt;
|
|
1321
|
+
const child =
|
|
1322
|
+
"pid" in h.paneChild
|
|
1323
|
+
? `pid ${h.paneChild.pid} (herdr's process report for that pane; a join, not an address)`
|
|
1324
|
+
: `unread — ${h.paneChild.unread}`;
|
|
1325
|
+
const left =
|
|
1326
|
+
"The tab is open and visible; read it directly. The lock was released, nothing was retried, and nothing was closed.";
|
|
1327
|
+
return {
|
|
1328
|
+
text:
|
|
1329
|
+
`[entwurf resume call ↻ herdr]\n` +
|
|
1330
|
+
` target: ${target}\n` +
|
|
1331
|
+
` backend: claude-code\n` +
|
|
1332
|
+
` session: ${result.launch.nativeSessionId}\n` +
|
|
1333
|
+
` runtime: ${h.runtime} --resume — herdr resolved the executable, we did not (requested start cwd ${h.cwd})\n` +
|
|
1334
|
+
` tab: ${h.herdrTabId} in workspace ${h.herdrWorkspaceId} — a NEW tab from ONE layout.apply, opened without taking focus\n` +
|
|
1335
|
+
` pane: ${h.herdrPaneId} — herdr VIEW coordinates, not an address\n` +
|
|
1336
|
+
` child: ${child}\n` +
|
|
1337
|
+
actor +
|
|
1338
|
+
`\n` +
|
|
1339
|
+
`LAUNCH receipt: herdr created that tab and was asked to start claude --resume on the session above.\n` +
|
|
1340
|
+
(observation.kind === "receiver-armed"
|
|
1341
|
+
? `OBSERVATION receipt: receiver-armed — a NEW live Claude Code process, the direct child of the pane this ` +
|
|
1342
|
+
`call opened (pid ${observation.ownerPid}), re-armed the receiver marker of ${target} for the same session ` +
|
|
1343
|
+
`after ${observation.waitedMs}ms, with a fresh arm binding the record also carries (same createdAt). ` +
|
|
1344
|
+
`entwurf_v2 fire-and-forget now reaches its mailbox. This call submitted no prompt; the runtime's own ` +
|
|
1345
|
+
`activity after resume (e.g. a doorbell on unread mail) is unmeasured.`
|
|
1346
|
+
: observation.kind === "pane-process-unread"
|
|
1347
|
+
? `OBSERVATION receipt: pane-process-unread — herdr did not report the direct child of the pane this ` +
|
|
1348
|
+
`call opened (${observation.cause}), so no receiver marker could be joined to it and no wait ran. ${left}`
|
|
1349
|
+
: observation.kind === "record-unreadable"
|
|
1350
|
+
? `OBSERVATION receipt: record-unreadable — the record of ${target} could not be read after ` +
|
|
1351
|
+
`${observation.waitedMs}ms: ${observation.cause}. ${left}`
|
|
1352
|
+
: `OBSERVATION receipt: resume-unobserved — no new live receiver marker from the direct child of the pane ` +
|
|
1353
|
+
`this call opened, bound to the record, appeared within ${observation.waitedMs}ms. ${left}`),
|
|
1354
|
+
isError: false,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* A launch that failed after it created something. The header claims no more than the recovery
|
|
1360
|
+
* knows — the fresh herdr rail learned that the hard way: `none` means herdr declined by name before
|
|
1361
|
+
* making anything, `unknown` means no answer we can act on so a tab MAY exist, and anything else means
|
|
1362
|
+
* the tab was created and the recovery says whether its pane was closed. No observation ran, so this
|
|
1363
|
+
* says nothing about whether the citizen came back.
|
|
1364
|
+
*/
|
|
1365
|
+
function renderLaunchFailed(result: Extract<VisibleResumeResult, { reason: "launch-failed" }>): {
|
|
1366
|
+
text: string;
|
|
1367
|
+
isError: boolean;
|
|
1368
|
+
} {
|
|
1369
|
+
const f = result.failure;
|
|
1370
|
+
const tabHeader =
|
|
1371
|
+
f.recovery.outcome === "none"
|
|
1372
|
+
? "entwurf_resume_call failed at the herdr tab create step"
|
|
1373
|
+
: f.recovery.outcome === "unknown"
|
|
1374
|
+
? "entwurf_resume_call failed at the herdr tab create step with an UNKNOWN outcome"
|
|
1375
|
+
: "entwurf_resume_call failed after the herdr tab was created";
|
|
1376
|
+
// #157 B3: the pi-durable rail asks herdr for ONE layout.apply. Its classifier answers only none or
|
|
1377
|
+
// unknown, so anything but none is UNKNOWN, and there is no tab create step to name.
|
|
1378
|
+
const layout = f.rail === "herdr-layout";
|
|
1379
|
+
const header = layout
|
|
1380
|
+
? `entwurf_resume_call failed at the herdr layout.apply step${f.recovery.outcome === "none" ? "" : " with an UNKNOWN outcome"}`
|
|
1381
|
+
: tabHeader;
|
|
1382
|
+
const code = f.vendorCode === undefined ? "" : ` [herdr: ${f.vendorCode}]`;
|
|
1383
|
+
const created =
|
|
1384
|
+
f.created === undefined
|
|
1385
|
+
? ""
|
|
1386
|
+
: ` created: tab ${f.created.herdrTabId} in workspace ${f.created.herdrWorkspaceId}, pane ${f.created.herdrPaneId} ` +
|
|
1387
|
+
`terminal ${f.created.herdrTerminalId} — herdr VIEW coordinates, not an address\n`;
|
|
1388
|
+
const recovery =
|
|
1389
|
+
f.recovery.outcome === "none"
|
|
1390
|
+
? layout
|
|
1391
|
+
? " 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"
|
|
1392
|
+
: " recovery: none — herdr declined by name before making anything, so there is nothing to go look at\n"
|
|
1393
|
+
: f.recovery.outcome === "unknown"
|
|
1394
|
+
? " 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"
|
|
1395
|
+
: f.recovery.outcome === "closed"
|
|
1396
|
+
? ` 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`
|
|
1397
|
+
: ` recovery: orphan-unreclaimed:${f.recovery.reason} (${f.recovery.paneId}) — this pane was NOT closed, on purpose\n`;
|
|
1398
|
+
return {
|
|
1399
|
+
text:
|
|
1400
|
+
`${header}: ${result.detail}.${code}\n` +
|
|
1401
|
+
created +
|
|
1402
|
+
recovery +
|
|
1403
|
+
`No observation was attempted, so this says nothing about whether the citizen came back — entwurf_peers ` +
|
|
1404
|
+
`shows whether its ${layout ? "rail" : "control socket"} answers. The lock was released and nothing was retried.`,
|
|
1405
|
+
isError: true,
|
|
1406
|
+
};
|
|
1407
|
+
}
|