@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
|
|
@@ -73,6 +92,7 @@
|
|
|
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";
|
|
77
97
|
import { ENTWURF_RESUME_REJECT_HINTS } from "./agent-guidance.ts";
|
|
78
98
|
import { buildResumePiArgs } from "./entwurf-resume-args.ts";
|
|
@@ -86,6 +106,10 @@ import {
|
|
|
86
106
|
} from "./entwurf-v2-lock.ts";
|
|
87
107
|
import {
|
|
88
108
|
classifyMarkerOwner,
|
|
109
|
+
defaultMetaMailboxDir,
|
|
110
|
+
defaultMetaReceiversDir,
|
|
111
|
+
defaultMetaSendersDir,
|
|
112
|
+
defaultMetaSessionsDir,
|
|
89
113
|
type MarkerOwnerVerdict,
|
|
90
114
|
metaReceiverMarkerPath,
|
|
91
115
|
parseMetaReceiverMarker,
|
|
@@ -93,10 +117,18 @@ import {
|
|
|
93
117
|
processStartKey,
|
|
94
118
|
readStoreRecordFile,
|
|
95
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";
|
|
96
129
|
import {
|
|
97
130
|
type ClaudeCodeLaunchIdentity,
|
|
98
131
|
type LaunchIdentity,
|
|
99
|
-
type ResumeBackend,
|
|
100
132
|
ResumeBackendUnsupportedError,
|
|
101
133
|
resolveResumeLaunchIdentity,
|
|
102
134
|
} from "./resume-launch-identity.ts";
|
|
@@ -126,7 +158,7 @@ export const RESUME_OBSERVE_INTERVAL_MS = 500;
|
|
|
126
158
|
* `target-backend-unsupported` IS in here, and the line between it and those throws is worth
|
|
127
159
|
* stating: asking to resume a Codex or agy citizen is not a defect at all. Nothing is corrupt, the
|
|
128
160
|
* record is exactly right, and the operator has simply reached past a capability boundary — only
|
|
129
|
-
* 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
|
|
130
162
|
* an error report about a store that is fine. The four `target-receiver-*` reasons are the Claude
|
|
131
163
|
* Code dormancy proof failing: nothing on disk proves the citizen is not running.
|
|
132
164
|
*/
|
|
@@ -144,9 +176,10 @@ export type VisibleResumeRejectReason =
|
|
|
144
176
|
| "target-address-conflict"
|
|
145
177
|
| "launch-refused";
|
|
146
178
|
|
|
147
|
-
/** What the injected launcher gives back. Structural on purpose: naming mux's types here
|
|
148
|
-
* be the import this split exists to avoid. */
|
|
149
|
-
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";
|
|
150
183
|
serverPid: string;
|
|
151
184
|
sessionId: string;
|
|
152
185
|
windowId: string;
|
|
@@ -157,16 +190,118 @@ export interface VisibleResumeLaunchHandle {
|
|
|
157
190
|
cwd: string;
|
|
158
191
|
}
|
|
159
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
|
+
|
|
160
275
|
export type VisibleResumeLaunchOutcome =
|
|
161
|
-
| {
|
|
162
|
-
|
|
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 };
|
|
163
283
|
|
|
164
|
-
/** Fact 1: tmux created a window
|
|
165
|
-
*
|
|
166
|
-
* 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. */
|
|
167
289
|
export type ResumeLaunchReceipt =
|
|
168
290
|
| { backend: "pi"; targetGardenId: string; sessionFile: string; handle: VisibleResumeLaunchHandle }
|
|
169
|
-
| {
|
|
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
|
+
};
|
|
170
305
|
|
|
171
306
|
/** Fact 2: the citizen's control socket came back under the SAME garden id, or it did not. This
|
|
172
307
|
* is the only fact in this module that says a citizen is back. */
|
|
@@ -184,7 +319,10 @@ export interface ResumeObservationReceipt {
|
|
|
184
319
|
export type ReceiverObservationReceipt =
|
|
185
320
|
| { kind: "receiver-armed"; ownerPid: number; waitedMs: number }
|
|
186
321
|
| { kind: "resume-unobserved"; waitedMs: number }
|
|
187
|
-
| { 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 };
|
|
188
326
|
|
|
189
327
|
/** What the receiver marker under a garden id says, read raw and judged by the shared owner rule. */
|
|
190
328
|
export type ReceiverInspection =
|
|
@@ -207,9 +345,29 @@ export type ReceiverInspection =
|
|
|
207
345
|
owner: MarkerOwnerVerdict;
|
|
208
346
|
};
|
|
209
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
|
+
|
|
210
361
|
export type VisibleResumeResult =
|
|
211
|
-
| {
|
|
212
|
-
|
|
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 };
|
|
213
371
|
|
|
214
372
|
export interface VisibleResumeDeps {
|
|
215
373
|
acquireLock: (gardenId: string) => AcquireLockResult;
|
|
@@ -219,12 +377,16 @@ export interface VisibleResumeDeps {
|
|
|
219
377
|
probeSocket: (socketPath: string) => Promise<SocketLiveness>;
|
|
220
378
|
/** The Claude Code dormancy proof and observation read this; pi never does. */
|
|
221
379
|
inspectReceiver: (gardenId: string) => ReceiverInspection;
|
|
222
|
-
/** The ONLY way this module reaches tmux. The surface supplies it
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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;
|
|
228
390
|
sleep: (ms: number) => Promise<void>;
|
|
229
391
|
now: () => number;
|
|
230
392
|
}
|
|
@@ -243,6 +405,20 @@ export function makeVisibleResumeDeps(
|
|
|
243
405
|
probeSocket: (socketPath) => probeSocketLiveness(socketPath),
|
|
244
406
|
inspectReceiver: (gid) => inspectReceiverMarker(gid),
|
|
245
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),
|
|
246
422
|
sleep: (ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
|
|
247
423
|
now: () => Date.now(),
|
|
248
424
|
...overrides,
|
|
@@ -366,7 +542,7 @@ export async function visibleResume(targetGardenId: string, deps: VisibleResumeD
|
|
|
366
542
|
ok: false,
|
|
367
543
|
reason: err.reason,
|
|
368
544
|
detail:
|
|
369
|
-
`${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 ` +
|
|
370
546
|
`under the same garden id`,
|
|
371
547
|
};
|
|
372
548
|
}
|
|
@@ -374,6 +550,7 @@ export async function visibleResume(targetGardenId: string, deps: VisibleResumeD
|
|
|
374
550
|
}
|
|
375
551
|
|
|
376
552
|
if (identity.backend === "claude-code") return await resumeClaudeCode(targetGardenId, identity, deps);
|
|
553
|
+
if (identity.backend === "pi-durable") return await resumePiDurable(targetGardenId, deps);
|
|
377
554
|
|
|
378
555
|
// Liveness preflight, still under the lock. A live citizen is addressed with
|
|
379
556
|
// `entwurf_v2 fire-and-forget`, not reopened — and refusing here is also what stops two
|
|
@@ -412,9 +589,15 @@ export async function visibleResume(targetGardenId: string, deps: VisibleResumeD
|
|
|
412
589
|
provider: identity.provider,
|
|
413
590
|
model: identity.model,
|
|
414
591
|
});
|
|
415
|
-
const launched = deps.launch({ backend: "pi", cwd: identity.cwd, runtimeArgs });
|
|
416
|
-
if (!launched.ok)
|
|
417
|
-
|
|
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
|
+
);
|
|
418
601
|
}
|
|
419
602
|
|
|
420
603
|
const observation = await observeSameGidSocket(targetGardenId, mapped.socketPath, deps);
|
|
@@ -428,6 +611,23 @@ export async function visibleResume(targetGardenId: string, deps: VisibleResumeD
|
|
|
428
611
|
}
|
|
429
612
|
}
|
|
430
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
|
+
|
|
431
631
|
/**
|
|
432
632
|
* The Claude Code half, still under the caller's lock. Dormancy needs a DEAD owner verdict on the
|
|
433
633
|
* receiver marker AND the binding: the record re-read AFTER that verdict must still name the same
|
|
@@ -523,33 +723,294 @@ async function resumeClaudeCode(
|
|
|
523
723
|
};
|
|
524
724
|
}
|
|
525
725
|
|
|
526
|
-
const launched = deps.launch({
|
|
726
|
+
const launched = await deps.launch({
|
|
527
727
|
backend: "claude-code",
|
|
528
728
|
cwd: current.cwd,
|
|
529
729
|
runtimeArgs: ["--resume", current.nativeSessionId],
|
|
530
730
|
});
|
|
531
|
-
if (!launched.ok)
|
|
532
|
-
|
|
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
|
+
};
|
|
533
754
|
}
|
|
755
|
+
const panePid = handle.rail === "tmux" ? handle.panePid : (handle.paneChild as { pid: string }).pid;
|
|
534
756
|
const observation = await observeReceiverReattached(
|
|
535
757
|
targetGardenId,
|
|
536
758
|
current,
|
|
537
759
|
{ ownerPid: marker.ownerPid, ownerStartKey: marker.ownerStartKey, attachIncarnation: current.attachIncarnation },
|
|
538
|
-
|
|
760
|
+
panePid,
|
|
539
761
|
deps,
|
|
540
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);
|
|
541
966
|
return {
|
|
542
967
|
ok: true,
|
|
543
968
|
launch: {
|
|
544
|
-
backend: "
|
|
969
|
+
backend: "pi-durable",
|
|
545
970
|
targetGardenId,
|
|
546
|
-
|
|
971
|
+
cwd: reservation.cwd,
|
|
972
|
+
reservation: {
|
|
973
|
+
generation: reservation.generation,
|
|
974
|
+
resource: reservation.resource,
|
|
975
|
+
ownerPid: reservation.owner.pid,
|
|
976
|
+
},
|
|
547
977
|
handle: launched.handle,
|
|
548
978
|
},
|
|
549
979
|
observation,
|
|
550
980
|
};
|
|
551
981
|
}
|
|
552
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
|
+
|
|
553
1014
|
/**
|
|
554
1015
|
* ONE bounded operation, the Claude Code twin of `observeSameGidSocket`: wait for a NEW live owner
|
|
555
1016
|
* — in the pane this call opened (`panePid`), a join, not an address — to re-arm this garden id's
|
|
@@ -666,6 +1127,7 @@ export function renderVisibleResume(
|
|
|
666
1127
|
attribution: { actor: string | null } = { actor: null },
|
|
667
1128
|
): { text: string; isError: boolean } {
|
|
668
1129
|
if (!result.ok) {
|
|
1130
|
+
if (result.reason === "launch-failed") return renderLaunchFailed(result);
|
|
669
1131
|
return {
|
|
670
1132
|
text:
|
|
671
1133
|
`entwurf_resume_call rejected: ${result.reason} — ${REJECT_HINT[result.reason]}. ` +
|
|
@@ -673,13 +1135,20 @@ export function renderVisibleResume(
|
|
|
673
1135
|
isError: true,
|
|
674
1136
|
};
|
|
675
1137
|
}
|
|
676
|
-
const h = result.launch.handle;
|
|
677
1138
|
// Who asked is recorded, and it is not authority: the verb decides from the target's record and
|
|
678
1139
|
// live rail facts alone, whoever the caller is.
|
|
679
1140
|
const actor =
|
|
680
1141
|
attribution.actor === null
|
|
681
1142
|
? ` actor: unattributed (the caller has no record-backed garden identity)\n`
|
|
682
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);
|
|
683
1152
|
const window =
|
|
684
1153
|
` runtime: ${h.runtimePath} (requested start cwd ${h.cwd})\n` +
|
|
685
1154
|
` window: ${h.windowId} (index ${h.windowIndex}) in session ${h.sessionId}\n` +
|
|
@@ -733,3 +1202,206 @@ export function renderVisibleResume(
|
|
|
733
1202
|
isError: false,
|
|
734
1203
|
};
|
|
735
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
|
+
}
|