@north-light/crouter 0.3.249 → 0.3.251
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/README.md +1 -1
- package/dist/api/__tests__/integration/client.test.js +28 -7
- package/dist/api/client.d.ts +1 -1
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +3 -0
- package/dist/api/dto/messages.d.ts +4 -0
- package/dist/api/dto/nodes.d.ts +4 -0
- package/dist/api/dto/reports.d.ts +7 -2
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -0
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +4 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +4 -3
- package/dist/clients/attach/session/chrome-refresh.d.ts +3 -0
- package/dist/clients/attach/session/chrome-refresh.js +1 -0
- package/dist/clients/attach/viewer.js +532 -532
- package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.d.ts +1 -0
- package/dist/clients/inbox/__tests__/integration/stale-row-activation.test.js +53 -0
- package/dist/clients/inbox/controller.js +32 -5
- package/dist/clients/inbox/tui/ansi.d.ts +3 -1
- package/dist/clients/inbox/tui/ansi.js +14 -10
- package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +7 -7
- package/dist/commands/api-client.d.ts +2 -2
- package/dist/commands/api-client.js +5 -5
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/node/create.js +3 -1
- package/dist/commands/node/inspect.js +1 -0
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +6 -4
- package/dist/commands/node/wait.js +1 -1
- package/dist/commands/push.js +36 -37
- package/dist/commands/sys/daemon.js +21 -8
- package/dist/commands/sys/setup-core.js +2 -2
- package/dist/core/__tests__/broker-stream-watchdog-floor.test.d.ts +1 -0
- package/dist/core/__tests__/broker-stream-watchdog-floor.test.js +84 -0
- package/dist/core/__tests__/broker-turn-admission.test.d.ts +1 -0
- package/dist/core/__tests__/broker-turn-admission.test.js +44 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +100 -1
- package/dist/core/__tests__/child-death-wake.test.js +11 -7
- package/dist/core/__tests__/collectible.test.d.ts +1 -0
- package/dist/core/__tests__/collectible.test.js +88 -0
- package/dist/core/__tests__/context-intro.test.js +4 -41
- package/dist/core/__tests__/daemon-boot.test.js +30 -14
- package/dist/core/__tests__/daemon-wedge.test.js +25 -19
- package/dist/core/__tests__/fixtures/fake-engine.js +20 -2
- package/dist/core/__tests__/human-deliver.test.js +5 -5
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +65 -4
- package/dist/core/__tests__/integration/human-deliver-e2e.test.js +3 -3
- package/dist/core/__tests__/integration/revive.test.js +48 -0
- package/dist/core/__tests__/integration/spawn-root.test.js +36 -1
- package/dist/core/__tests__/integration/worktree-land.test.js +82 -3
- package/dist/core/__tests__/integration/worktree-reap.test.js +44 -6
- package/dist/core/__tests__/migration.test.js +30 -0
- package/dist/core/__tests__/passive-subscription.test.js +39 -1
- package/dist/core/__tests__/prune.test.js +12 -3
- package/dist/core/__tests__/relaunch-root.test.js +1 -1
- package/dist/core/__tests__/respawn-throttle.test.js +16 -146
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +23 -11
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +81 -2
- package/dist/core/__tests__/seam/dormancy-release.test.js +1 -1
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +63 -0
- package/dist/core/__tests__/seam/self-close-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/seam/self-close-teardown.test.js +49 -0
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +18 -10
- package/dist/core/canvas/canvas.d.ts +9 -15
- package/dist/core/canvas/canvas.js +41 -47
- package/dist/core/canvas/crons.d.ts +5 -8
- package/dist/core/canvas/crons.js +5 -8
- package/dist/core/canvas/history.d.ts +5 -0
- package/dist/core/canvas/history.js +12 -1
- package/dist/core/canvas/migrations.js +58 -1
- package/dist/core/canvas/pid.d.ts +1 -1
- package/dist/core/canvas/pid.js +5 -2
- package/dist/core/canvas/types.d.ts +4 -0
- package/dist/core/feed/feed.d.ts +4 -0
- package/dist/core/feed/feed.js +13 -8
- package/dist/core/feed/inbox.d.ts +2 -1
- package/dist/core/feed/inbox.js +22 -3
- package/dist/core/git.d.ts +3 -0
- package/dist/core/git.js +3 -1
- package/dist/core/human/feedback-companion.js +1 -0
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/companion.js +1 -0
- package/dist/core/review/realize.js +1 -0
- package/dist/core/runtime/broker/auth-reload.d.ts +2 -0
- package/dist/core/runtime/broker/auth-reload.js +9 -3
- package/dist/core/runtime/broker/engine-drive.d.ts +2 -0
- package/dist/core/runtime/broker/engine-drive.js +39 -23
- package/dist/core/runtime/broker/event-projection.d.ts +4 -0
- package/dist/core/runtime/broker/event-projection.js +43 -23
- package/dist/core/runtime/broker/fault-retry.d.ts +9 -0
- package/dist/core/runtime/broker/fault-retry.js +94 -8
- package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
- package/dist/core/runtime/broker/frame-dispatch.js +4 -1
- package/dist/core/runtime/broker/held-deferred-inbox.d.ts +9 -0
- package/dist/core/runtime/broker/held-deferred-inbox.js +8 -0
- package/dist/core/runtime/broker/inbox.js +4 -0
- package/dist/core/runtime/broker/rebind.js +18 -0
- package/dist/core/runtime/broker/turn-admission.d.ts +11 -0
- package/dist/core/runtime/broker/turn-admission.js +31 -0
- package/dist/core/runtime/broker/turn-ignition.d.ts +2 -0
- package/dist/core/runtime/broker/turn-ignition.js +25 -15
- package/dist/core/runtime/broker.js +21 -2
- package/dist/core/runtime/close.d.ts +11 -9
- package/dist/core/runtime/close.js +23 -19
- package/dist/core/runtime/fault.d.ts +21 -0
- package/dist/core/runtime/fault.js +129 -19
- package/dist/core/runtime/fleet.d.ts +0 -6
- package/dist/core/runtime/host.d.ts +7 -1
- package/dist/core/runtime/host.js +20 -2
- package/dist/core/runtime/nodes.d.ts +5 -1
- package/dist/core/runtime/nodes.js +24 -1
- package/dist/core/runtime/placement.d.ts +6 -6
- package/dist/core/runtime/placement.js +15 -8
- package/dist/core/runtime/reset.js +2 -2
- package/dist/core/runtime/revive.js +40 -4
- package/dist/core/runtime/spawn.d.ts +3 -0
- package/dist/core/runtime/spawn.js +1 -0
- package/dist/core/runtime/warm-pool.d.ts +4 -0
- package/dist/core/runtime/warm-pool.js +2 -0
- package/dist/core/worktree.d.ts +4 -3
- package/dist/core/worktree.js +73 -27
- package/dist/daemon/__tests__/integration/api-startup-readiness.test.d.ts +1 -0
- package/dist/daemon/__tests__/integration/api-startup-readiness.test.js +81 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +38 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +1 -0
- package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +13 -2
- package/dist/daemon/api/handlers/bash-jobs.js +2 -0
- package/dist/daemon/api/handlers/broker-ops.js +23 -2
- package/dist/daemon/api/handlers/human.js +1 -1
- package/dist/daemon/api/handlers/messages.js +11 -2
- package/dist/daemon/api/handlers/nodes.js +13 -1
- package/dist/daemon/api/handlers/reports.js +14 -6
- package/dist/daemon/api/map.js +1 -0
- package/dist/daemon/api/server.d.ts +9 -12
- package/dist/daemon/api/server.js +30 -19
- package/dist/daemon/companion-retire.js +3 -9
- package/dist/daemon/cron/sinks.js +7 -2
- package/dist/daemon/crtrd.js +4 -1
- package/dist/daemon/fleet.d.ts +2 -16
- package/dist/daemon/fleet.js +63 -150
- package/dist/daemon/human/finish.js +2 -0
- package/dist/daemon/manage.d.ts +11 -0
- package/dist/daemon/manage.js +32 -3
- package/dist/daemon/messaging/node-message.js +7 -1
- package/dist/daemon/reconcilers/bash-deadline.js +2 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +1 -0
- package/dist/daemon/reconcilers/broker-supervision.js +22 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +16 -6
- package/dist/daemon/reconcilers/live-obligation.js +18 -6
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +15 -4
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +34 -3
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -1
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +11 -2
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +14 -7
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +2 -2
- package/dist/daemon/reconcilers/storage-maintenance.js +3 -3
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -34
- package/dist/pi-extensions/canvas-context-intro.js +4 -76
- package/dist/pi-extensions/canvas-inbox-watcher.js +105 -4
- package/dist/pi-extensions/canvas-review-boundary.d.ts +3 -24
- package/dist/pi-extensions/canvas-review-boundary.js +4 -29
- package/dist/pi-extensions/canvas-stophook.js +1 -6
- package/dist/shared/birth-announcement.d.ts +12 -0
- package/dist/shared/birth-announcement.js +25 -0
- package/dist/shared/generated-context.d.ts +8 -4
- package/dist/shared/generated-context.js +15 -9
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +0 -254
- package/dist/pi-extensions/truncate.d.ts +0 -5
- package/dist/pi-extensions/truncate.js +0 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { envNodeId } from '../../shared/env.js';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { jobDir } from '../canvas/paths.js';
|
|
5
5
|
import { errorClassFromFault, } from '../fault-classifier.js';
|
|
@@ -10,6 +10,17 @@ const MESSAGE_CAP = 500;
|
|
|
10
10
|
function faultPath(nodeId) {
|
|
11
11
|
return join(jobDir(nodeId), 'fault');
|
|
12
12
|
}
|
|
13
|
+
function providerRetryPath(nodeId) {
|
|
14
|
+
return join(jobDir(nodeId), 'provider-retry');
|
|
15
|
+
}
|
|
16
|
+
function canonicalSessionFile(sessionFile) {
|
|
17
|
+
try {
|
|
18
|
+
return realpathSync(sessionFile);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return sessionFile;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
function clampMessage(message) {
|
|
14
25
|
return message.slice(0, MESSAGE_CAP);
|
|
15
26
|
}
|
|
@@ -78,30 +89,64 @@ function currentFault(nodeId) {
|
|
|
78
89
|
return null;
|
|
79
90
|
}
|
|
80
91
|
}
|
|
92
|
+
function currentProviderRetryEpisode(nodeId) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(readFileSync(providerRetryPath(nodeId), 'utf8'));
|
|
95
|
+
if (parsed.state !== 'pending' && parsed.state !== 'admitted' && parsed.state !== 'invalidated')
|
|
96
|
+
return null;
|
|
97
|
+
if (typeof parsed.sessionFile !== 'string' || parsed.sessionFile === '')
|
|
98
|
+
return null;
|
|
99
|
+
if (parsed.anchorEntryId !== null && typeof parsed.anchorEntryId !== 'string')
|
|
100
|
+
return null;
|
|
101
|
+
const fault = parseFault(JSON.stringify(parsed.fault));
|
|
102
|
+
if (fault === null ||
|
|
103
|
+
fault.link !== 'pi→provider' ||
|
|
104
|
+
fault.retry.disposition !== 'auto' ||
|
|
105
|
+
fault.retry.by !== 'daemon' ||
|
|
106
|
+
(fault.anchorEntryId ?? null) !== parsed.anchorEntryId)
|
|
107
|
+
return null;
|
|
108
|
+
return { state: parsed.state, sessionFile: canonicalSessionFile(parsed.sessionFile), anchorEntryId: parsed.anchorEntryId, fault };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
81
114
|
function writeFault(nodeId, fault) {
|
|
82
115
|
try {
|
|
83
116
|
mkdirSync(jobDir(nodeId), { recursive: true });
|
|
84
117
|
writeFileSync(faultPath(nodeId), JSON.stringify(fault));
|
|
118
|
+
return true;
|
|
85
119
|
}
|
|
86
120
|
catch {
|
|
87
|
-
|
|
121
|
+
return false;
|
|
88
122
|
}
|
|
89
123
|
}
|
|
90
|
-
function
|
|
124
|
+
function writeProviderRetryEpisode(nodeId, episode) {
|
|
125
|
+
try {
|
|
126
|
+
mkdirSync(jobDir(nodeId), { recursive: true });
|
|
127
|
+
writeFileSync(providerRetryPath(nodeId), JSON.stringify(episode));
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function resolvedMarker(nodeId, fault) {
|
|
91
135
|
const current = currentFault(nodeId);
|
|
92
|
-
if (current
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
fault = { ...fault, since: current.since, anchorEntryId: current.anchorEntryId ?? fault.anchorEntryId, retry };
|
|
99
|
-
}
|
|
100
|
-
else if (faultPriority(current) > faultPriority(fault)) {
|
|
101
|
-
return;
|
|
136
|
+
if (current === null)
|
|
137
|
+
return fault;
|
|
138
|
+
if (current.link === fault.link && current.kind === fault.kind) {
|
|
139
|
+
const retry = { ...current.retry, ...fault.retry };
|
|
140
|
+
if (fault.retry.disposition === 'fatal' && !('nextAt' in fault.retry)) {
|
|
141
|
+
delete retry.nextAt;
|
|
102
142
|
}
|
|
143
|
+
return { ...fault, since: current.since, anchorEntryId: current.anchorEntryId ?? fault.anchorEntryId, retry };
|
|
103
144
|
}
|
|
104
|
-
|
|
145
|
+
return faultPriority(current) > faultPriority(fault) ? null : fault;
|
|
146
|
+
}
|
|
147
|
+
function updateMarker(nodeId, fault) {
|
|
148
|
+
const resolved = resolvedMarker(nodeId, fault);
|
|
149
|
+
return resolved !== null && writeFault(nodeId, resolved) ? resolved : null;
|
|
105
150
|
}
|
|
106
151
|
function resolveOperationId(explicit) {
|
|
107
152
|
if (explicit !== undefined)
|
|
@@ -111,22 +156,23 @@ function resolveOperationId(explicit) {
|
|
|
111
156
|
return current;
|
|
112
157
|
return operationIdContext.fresh((operationId) => operationId);
|
|
113
158
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const fault = {
|
|
159
|
+
function storedFault(input) {
|
|
160
|
+
return {
|
|
117
161
|
link: input.link,
|
|
118
162
|
op: input.op,
|
|
119
163
|
kind: input.kind,
|
|
120
164
|
retry: input.retry,
|
|
121
165
|
message: clampMessage(input.message),
|
|
122
166
|
since: input.since ?? new Date().toISOString(),
|
|
123
|
-
operation_id:
|
|
167
|
+
operation_id: resolveOperationId(input.operation_id),
|
|
124
168
|
...(typeof input.anchorEntryId === 'string' && input.anchorEntryId !== '' ? { anchorEntryId: input.anchorEntryId } : {}),
|
|
125
169
|
};
|
|
170
|
+
}
|
|
171
|
+
function emitRecordedFault(nodeId, fault) {
|
|
126
172
|
emitEvent({
|
|
127
173
|
level: fault.retry.disposition === 'fatal' ? 'error' : 'warn',
|
|
128
174
|
event: 'fault.recorded',
|
|
129
|
-
operation_id:
|
|
175
|
+
operation_id: fault.operation_id,
|
|
130
176
|
node_id: nodeId,
|
|
131
177
|
error_class: errorClassFromFault(fault),
|
|
132
178
|
fields: {
|
|
@@ -138,8 +184,29 @@ export function recordFault(nodeId, input) {
|
|
|
138
184
|
...(fault.anchorEntryId === undefined ? {} : { anchor_entry_id: fault.anchorEntryId }),
|
|
139
185
|
},
|
|
140
186
|
});
|
|
187
|
+
}
|
|
188
|
+
export function recordFault(nodeId, input) {
|
|
189
|
+
const fault = storedFault(input);
|
|
190
|
+
emitRecordedFault(nodeId, fault);
|
|
141
191
|
updateMarker(nodeId, fault);
|
|
142
192
|
}
|
|
193
|
+
/** Atomically establishes the crash-recovery authority before publishing its
|
|
194
|
+
* ordinary marker, so a broker death can never leave only a retryable fault. */
|
|
195
|
+
export function recordPendingProviderRetryFault(nodeId, sessionFile, input) {
|
|
196
|
+
const fault = storedFault(input);
|
|
197
|
+
emitRecordedFault(nodeId, fault);
|
|
198
|
+
const marker = resolvedMarker(nodeId, fault);
|
|
199
|
+
if (marker === null)
|
|
200
|
+
return null;
|
|
201
|
+
if (!writeProviderRetryEpisode(nodeId, {
|
|
202
|
+
state: 'pending',
|
|
203
|
+
sessionFile: canonicalSessionFile(sessionFile),
|
|
204
|
+
anchorEntryId: marker.anchorEntryId ?? null,
|
|
205
|
+
fault: marker,
|
|
206
|
+
}))
|
|
207
|
+
return null;
|
|
208
|
+
return writeFault(nodeId, marker) ? marker : null;
|
|
209
|
+
}
|
|
143
210
|
export function recordFaultForCurrentNode(input) {
|
|
144
211
|
const nodeId = envNodeId();
|
|
145
212
|
if (nodeId === undefined || nodeId.trim() === '')
|
|
@@ -162,6 +229,11 @@ export function clearFault(nodeId, opts) {
|
|
|
162
229
|
if (opts?.link !== undefined && current.link !== opts.link)
|
|
163
230
|
return false;
|
|
164
231
|
rmSync(faultPath(nodeId));
|
|
232
|
+
if (opts?.preserveProviderRetryEpisode !== true) {
|
|
233
|
+
const episode = currentProviderRetryEpisode(nodeId);
|
|
234
|
+
if (episode?.fault.since === current.since)
|
|
235
|
+
rmSync(providerRetryPath(nodeId), { force: true });
|
|
236
|
+
}
|
|
165
237
|
const operationId = resolveOperationId();
|
|
166
238
|
emitEvent({
|
|
167
239
|
level: 'info',
|
|
@@ -197,6 +269,44 @@ export function clearFaultForCurrentNode(opts) {
|
|
|
197
269
|
export function readFault(nodeId) {
|
|
198
270
|
return currentFault(nodeId);
|
|
199
271
|
}
|
|
272
|
+
export function readProviderRetryEpisode(nodeId) {
|
|
273
|
+
return currentProviderRetryEpisode(nodeId);
|
|
274
|
+
}
|
|
275
|
+
/** Persist a newly scheduled provider retry before its broker-local timer is armed. */
|
|
276
|
+
export function recordPendingProviderRetryEpisode(nodeId, episode) {
|
|
277
|
+
return writeProviderRetryEpisode(nodeId, { ...episode, sessionFile: canonicalSessionFile(episode.sessionFile), state: 'pending' });
|
|
278
|
+
}
|
|
279
|
+
/** Admission is the irreversible boundary: an admitted retry is never replayed. */
|
|
280
|
+
export function admitProviderRetryEpisode(nodeId, sessionFile, fault) {
|
|
281
|
+
const current = currentProviderRetryEpisode(nodeId);
|
|
282
|
+
if (current === null ||
|
|
283
|
+
current.state !== 'pending' ||
|
|
284
|
+
current.sessionFile !== canonicalSessionFile(sessionFile) ||
|
|
285
|
+
current.fault.since !== fault.since ||
|
|
286
|
+
current.anchorEntryId !== (fault.anchorEntryId ?? null))
|
|
287
|
+
return false;
|
|
288
|
+
return writeProviderRetryEpisode(nodeId, {
|
|
289
|
+
state: 'admitted',
|
|
290
|
+
sessionFile: current.sessionFile,
|
|
291
|
+
anchorEntryId: current.anchorEntryId,
|
|
292
|
+
fault,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/** A replacement explicitly invalidates its old transcript's retry episode. */
|
|
296
|
+
export function invalidateProviderRetryEpisode(nodeId, sessionFile) {
|
|
297
|
+
const current = currentProviderRetryEpisode(nodeId);
|
|
298
|
+
if (current === null || current.sessionFile !== canonicalSessionFile(sessionFile) || current.state === 'invalidated')
|
|
299
|
+
return false;
|
|
300
|
+
return writeProviderRetryEpisode(nodeId, { ...current, state: 'invalidated' });
|
|
301
|
+
}
|
|
302
|
+
export function clearProviderRetryEpisode(nodeId) {
|
|
303
|
+
try {
|
|
304
|
+
rmSync(providerRetryPath(nodeId), { force: true });
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
/* best-effort */
|
|
308
|
+
}
|
|
309
|
+
}
|
|
200
310
|
export async function readFaultAsync(nodeId) {
|
|
201
311
|
return readFault(nodeId);
|
|
202
312
|
}
|
|
@@ -22,15 +22,9 @@ export interface FleetEntry {
|
|
|
22
22
|
/** The broker's supervised pid (never null — a launch with no pid is not
|
|
23
23
|
* registered). */
|
|
24
24
|
pid: number;
|
|
25
|
-
/** Date.now() at spawn — the respawn throttle's uptime baseline. */
|
|
26
|
-
launchedAt: number;
|
|
27
25
|
/** The ChildProcess-backed handle (host.ts) whose `exited` promise is the
|
|
28
26
|
* liveness authority. */
|
|
29
27
|
handle: HostHandle;
|
|
30
|
-
/** Respawn-throttle state at registration (consecutive short-lived exits
|
|
31
|
-
* observed before this launch). The authoritative counter lives in the
|
|
32
|
-
* daemon implementation and survives the entry's removal at exit. */
|
|
33
|
-
consecutiveFailures: number;
|
|
34
28
|
}
|
|
35
29
|
export interface FleetRegistry {
|
|
36
30
|
/** This daemon's epoch id — minted at boot, stamped into every broker's argv
|
|
@@ -22,6 +22,9 @@ export interface HostHandle {
|
|
|
22
22
|
signal: NodeJS.Signals | null;
|
|
23
23
|
}>;
|
|
24
24
|
}
|
|
25
|
+
export interface TeardownOptions {
|
|
26
|
+
excludePid?: number;
|
|
27
|
+
}
|
|
25
28
|
export interface Host {
|
|
26
29
|
/** Bring a node's ENGINE into existence from its launch recipe; return a
|
|
27
30
|
* supervisable handle. */
|
|
@@ -31,7 +34,7 @@ export interface Host {
|
|
|
31
34
|
* row) — share one selector. For the broker this IS isPidAlive(pi_pid). */
|
|
32
35
|
isAlive(node: string | NodeRow): boolean;
|
|
33
36
|
/** Tear the engine down (close/cancel teardown). */
|
|
34
|
-
teardown(nodeId: string): void;
|
|
37
|
+
teardown(nodeId: string, opts?: TeardownOptions): void;
|
|
35
38
|
/** Deliver an OS signal to the engine container — present so the daemon never
|
|
36
39
|
* reaches around the abstraction. */
|
|
37
40
|
signal(nodeId: string, sig: NodeJS.Signals): void;
|
|
@@ -40,4 +43,7 @@ export interface Host {
|
|
|
40
43
|
* the fast-fail seam for known module/path errors: a bad engine resolve or a
|
|
41
44
|
* missing plain session path throws here, before spawn(). */
|
|
42
45
|
export declare function preflightBrokerLaunch(nodeId: string, inv: PiInvocation, cwd: string): void;
|
|
46
|
+
/** Queue broker teardown onto the daemon event loop after the close response can
|
|
47
|
+
* settle. Repeated terminal reconciliation coalesces on the same node. */
|
|
48
|
+
export declare function requestBrokerTeardown(nodeId: string, opts?: TeardownOptions): void;
|
|
43
49
|
export declare const headlessBrokerHost: Host;
|
|
@@ -189,6 +189,24 @@ function escalateBrokerTeardown(tree, identities) {
|
|
|
189
189
|
killProcessTreePids(tree, 'SIGTERM', identities);
|
|
190
190
|
pollUntil(() => isAnyPidAlive(tree), BROKER_SIGKILL_GRACE_MS, () => killProcessTreePids(tree, 'SIGKILL', identities));
|
|
191
191
|
}
|
|
192
|
+
const requestedTeardowns = new Map();
|
|
193
|
+
/** Queue broker teardown onto the daemon event loop after the close response can
|
|
194
|
+
* settle. Repeated terminal reconciliation coalesces on the same node. */
|
|
195
|
+
export function requestBrokerTeardown(nodeId, opts = {}) {
|
|
196
|
+
const existing = requestedTeardowns.get(nodeId);
|
|
197
|
+
if (existing !== undefined) {
|
|
198
|
+
if (existing.excludePid === undefined && opts.excludePid !== undefined)
|
|
199
|
+
existing.excludePid = opts.excludePid;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
requestedTeardowns.set(nodeId, { ...opts });
|
|
203
|
+
setImmediate(() => {
|
|
204
|
+
const request = requestedTeardowns.get(nodeId);
|
|
205
|
+
requestedTeardowns.delete(nodeId);
|
|
206
|
+
if (request !== undefined)
|
|
207
|
+
headlessBrokerHost.teardown(nodeId, request);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
192
210
|
export const headlessBrokerHost = {
|
|
193
211
|
launch(nodeId, inv, opts) {
|
|
194
212
|
// Fast-fail known launch errors before we detach a broker process.
|
|
@@ -336,7 +354,7 @@ export const headlessBrokerHost = {
|
|
|
336
354
|
isAlive(node) {
|
|
337
355
|
return isPidAlive((typeof node === 'string' ? getNode(node) : node)?.pi_pid);
|
|
338
356
|
},
|
|
339
|
-
teardown(nodeId) {
|
|
357
|
+
teardown(nodeId, opts = {}) {
|
|
340
358
|
// Graceful: connect to view.sock + send a `shutdown` frame → the broker
|
|
341
359
|
// dispose()s the engine, unlinks the socket, exits 0. Status is already
|
|
342
360
|
// flipped done/canceled by the caller (crash-safe ordering), so the daemon
|
|
@@ -376,7 +394,7 @@ export const headlessBrokerHost = {
|
|
|
376
394
|
// snapshot's `reused: true` comes back with an EMPTY tree, so nothing
|
|
377
395
|
// below ever signals the stranger (or walks its process tree looking for
|
|
378
396
|
// "descendants", which would be the stranger's children, not ours).
|
|
379
|
-
const snapshot = pid != null ? captureTeardownSnapshot(pid, node?.pi_pid_identity ?? null) : null;
|
|
397
|
+
const snapshot = pid != null ? captureTeardownSnapshot(pid, node?.pi_pid_identity ?? null, opts.excludePid) : null;
|
|
380
398
|
if (snapshot?.reused === true) {
|
|
381
399
|
emitEvent({
|
|
382
400
|
level: 'warn',
|
|
@@ -59,8 +59,12 @@ export interface SpawnNodeOpts {
|
|
|
59
59
|
name?: string;
|
|
60
60
|
/** Editor-label handle (2-4 word kebab-case) for the node's first prompt. */
|
|
61
61
|
description?: string;
|
|
62
|
-
/** Parent node id. Omit for a user-opened root. */
|
|
62
|
+
/** Parent node id: the spine manager. Omit for a user-opened root. */
|
|
63
63
|
parent?: string | null;
|
|
64
|
+
/** Actor that created this node. Unlike parent (manager) and spawnedBy
|
|
65
|
+
* (audit provenance), it decides whether the parent gets a birth notice.
|
|
66
|
+
* Null/omitted means an external process. */
|
|
67
|
+
creator?: string | null;
|
|
64
68
|
/** Who spawned me (the `spawned_by` provenance edge), when it differs from
|
|
65
69
|
* `parent` — e.g. an independent root (parent=null) still records its
|
|
66
70
|
* spawner. Defaults to `parent`. */
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
import { envHomeOverride } from '../../shared/env.js';
|
|
16
16
|
import { randomBytes } from 'node:crypto';
|
|
17
17
|
import { mkdirSync } from 'node:fs';
|
|
18
|
-
import { createNode, getNode, getRow, subscribe, recordSpawn, contextDir, } from '../canvas/index.js';
|
|
18
|
+
import { createNode, getNode, getRow, subscribe, recordSpawn, subscribersOf, contextDir, } from '../canvas/index.js';
|
|
19
19
|
import { memoryDir } from './memory.js';
|
|
20
20
|
import { resolveInstallId } from '../canvas/install-id.js';
|
|
21
21
|
import { canvasDbPath } from '../canvas/paths.js';
|
|
22
22
|
import { usage } from '../errors.js';
|
|
23
23
|
import { assertProfileAvailableForBirth } from '../profiles/deletion-reservation.js';
|
|
24
|
+
import { fanDoctrineWake } from './close.js';
|
|
25
|
+
import { BIRTH_ANNOUNCEMENT_MARKER, birthAnnouncementBody } from '../../shared/birth-announcement.js';
|
|
24
26
|
// crtrd sets this once at boot (setInstallId) and it wins unconditionally —
|
|
25
27
|
// a daemon process only ever serves one canvas home for its whole lifetime,
|
|
26
28
|
// so no keying is needed there. Every OTHER in-process caller (tests,
|
|
@@ -225,6 +227,7 @@ export function spawnNode(opts) {
|
|
|
225
227
|
// Provenance is independent of the spine: a root has no parent but still
|
|
226
228
|
// records who spawned it. A child's spawner is its parent unless overridden.
|
|
227
229
|
const spawnedBy = opts.spawnedBy ?? parent;
|
|
230
|
+
const creator = opts.creator ?? null;
|
|
228
231
|
const mode = opts.mode ?? 'base';
|
|
229
232
|
// A user-opened root is resident (a conversation you live in); a spawned node
|
|
230
233
|
// is terminal until it must persist (promotion handles that later).
|
|
@@ -248,6 +251,7 @@ export function spawnNode(opts) {
|
|
|
248
251
|
status: 'active',
|
|
249
252
|
parent,
|
|
250
253
|
spawned_by: spawnedBy,
|
|
254
|
+
creator,
|
|
251
255
|
fork_from: opts.forkFrom,
|
|
252
256
|
fork_source_file: opts.forkSourceFile,
|
|
253
257
|
review_binding: opts.reviewBinding,
|
|
@@ -265,6 +269,9 @@ export function spawnNode(opts) {
|
|
|
265
269
|
if (parent !== null && getNode(parent) === null) {
|
|
266
270
|
throw new Error(`cannot spawn under unknown parent node: ${parent}`);
|
|
267
271
|
}
|
|
272
|
+
if (creator !== null && getNode(creator) === null) {
|
|
273
|
+
throw new Error(`cannot spawn from unknown creator node: ${creator}`);
|
|
274
|
+
}
|
|
268
275
|
assertProfileAvailableForBirth(meta.profile_id ?? null);
|
|
269
276
|
createNode(meta);
|
|
270
277
|
// Create the node-local memory directory so substrate docs can be written
|
|
@@ -279,6 +286,22 @@ export function spawnNode(opts) {
|
|
|
279
286
|
// A root (parent=null) gets NO subscription — nobody is woken by it.
|
|
280
287
|
subscribe(parent, meta.node_id, true);
|
|
281
288
|
}
|
|
289
|
+
if (parent !== null && creator !== parent) {
|
|
290
|
+
try {
|
|
291
|
+
fanDoctrineWake(meta.node_id, subscribersOf(meta.node_id), `Child created — ${meta.name} (${meta.node_id}, ${meta.kind})`, {
|
|
292
|
+
birth_announcement: BIRTH_ANNOUNCEMENT_MARKER,
|
|
293
|
+
body: birthAnnouncementBody({
|
|
294
|
+
name: meta.name,
|
|
295
|
+
nodeId: meta.node_id,
|
|
296
|
+
kind: meta.kind,
|
|
297
|
+
creator: creator === null ? 'an external process' : getNode(creator).name,
|
|
298
|
+
}),
|
|
299
|
+
}, 'deferred');
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
/* a failed birth announcement never prevents the child from existing */
|
|
303
|
+
}
|
|
304
|
+
}
|
|
282
305
|
// Audit-only provenance edge — recorded for a root too (from its spawner).
|
|
283
306
|
if (spawnedBy !== null && spawnedBy !== undefined && getNode(spawnedBy) !== null) {
|
|
284
307
|
recordSpawn(meta.node_id, spawnedBy);
|
|
@@ -89,12 +89,12 @@ export declare function detachToBackground(nodeId: string, pane?: string): boole
|
|
|
89
89
|
export declare function reapIfEmpty(nodeId: string, keepPane?: string): boolean;
|
|
90
90
|
/** Sweep the whole canvas for empty shells and reap them — the bulk cleanup
|
|
91
91
|
* behind `crtr canvas prune --empty`, for the husks that accumulated before
|
|
92
|
-
* close/detach learned to reap. Skips: the caller ($CRTR_NODE_ID),
|
|
93
|
-
* mid-first-turn, and any node with a LIVE broker (pi_pid alive) — a
|
|
94
|
-
* engine is the daemon's domain and may have output not yet persisted,
|
|
95
|
-
* GC never kills one (the per-action {@link reapIfEmpty} does, since
|
|
96
|
-
* explicitly closed/detached that node). `dryRun` reports candidates
|
|
97
|
-
* deleting. Returns the reaped (or, under dryRun, reapable) node ids. */
|
|
92
|
+
* close/detach learned to reap. Skips: the caller ($CRTR_NODE_ID), dead rows,
|
|
93
|
+
* nodes mid-first-turn, and any node with a LIVE broker (pi_pid alive) — a
|
|
94
|
+
* running engine is the daemon's domain and may have output not yet persisted,
|
|
95
|
+
* so bulk GC never kills one (the per-action {@link reapIfEmpty} does, since
|
|
96
|
+
* the user explicitly closed/detached that node). `dryRun` reports candidates
|
|
97
|
+
* without deleting. Returns the reaped (or, under dryRun, reapable) node ids. */
|
|
98
98
|
export declare function reapEmptyNodes(opts?: {
|
|
99
99
|
dryRun?: boolean;
|
|
100
100
|
}): string[];
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
// model-over-driver. Raw driver access is confined to placement, its pure
|
|
24
24
|
// tmux half, the bindings owner, and tmux chrome.
|
|
25
25
|
import { envNodeId } from '../../shared/env.js';
|
|
26
|
-
import { getNode, fullName, deleteNode, listNodes, openFocusRow, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses as listFocusRows, } from '../canvas/index.js';
|
|
26
|
+
import { getNode, fullName, deleteNode, listNodes, subscribersOf, openFocusRow, closeFocusRow, getFocusByNode, getFocusByPane, getFocusById, listFocuses as listFocusRows, } from '../canvas/index.js';
|
|
27
27
|
import { nodeHasAssistantMessage, isStreaming } from '../canvas/render-source.js';
|
|
28
28
|
import { headlessBrokerHost } from './host.js';
|
|
29
|
+
import { fanDoctrineWake } from './close.js';
|
|
29
30
|
import { paneExists, paneLocation, closePane, currentTmux, switchClient, selectWindow, selectLayout, selectPane, getPaneOption, setPaneOption, respawnPaneSync, windowOfPane, renameWindow, } from './tmux-driver.js';
|
|
30
31
|
// The PURE tmux + broker-socket half now lives in placement-tmux.ts. placement
|
|
31
32
|
// composes the viewer OPENER over openViewerPane (tmux) + registerViewerFocus
|
|
@@ -351,12 +352,12 @@ export function reapIfEmpty(nodeId, keepPane) {
|
|
|
351
352
|
}
|
|
352
353
|
/** Sweep the whole canvas for empty shells and reap them — the bulk cleanup
|
|
353
354
|
* behind `crtr canvas prune --empty`, for the husks that accumulated before
|
|
354
|
-
* close/detach learned to reap. Skips: the caller ($CRTR_NODE_ID),
|
|
355
|
-
* mid-first-turn, and any node with a LIVE broker (pi_pid alive) — a
|
|
356
|
-
* engine is the daemon's domain and may have output not yet persisted,
|
|
357
|
-
* GC never kills one (the per-action {@link reapIfEmpty} does, since
|
|
358
|
-
* explicitly closed/detached that node). `dryRun` reports candidates
|
|
359
|
-
* deleting. Returns the reaped (or, under dryRun, reapable) node ids. */
|
|
355
|
+
* close/detach learned to reap. Skips: the caller ($CRTR_NODE_ID), dead rows,
|
|
356
|
+
* nodes mid-first-turn, and any node with a LIVE broker (pi_pid alive) — a
|
|
357
|
+
* running engine is the daemon's domain and may have output not yet persisted,
|
|
358
|
+
* so bulk GC never kills one (the per-action {@link reapIfEmpty} does, since
|
|
359
|
+
* the user explicitly closed/detached that node). `dryRun` reports candidates
|
|
360
|
+
* without deleting. Returns the reaped (or, under dryRun, reapable) node ids. */
|
|
360
361
|
export function reapEmptyNodes(opts = {}) {
|
|
361
362
|
const dryRun = opts.dryRun ?? false;
|
|
362
363
|
const selfId = envNodeId() ?? '';
|
|
@@ -364,9 +365,15 @@ export function reapEmptyNodes(opts = {}) {
|
|
|
364
365
|
for (const row of listNodes()) {
|
|
365
366
|
const id = row.node_id;
|
|
366
367
|
const meta = getNode(id);
|
|
367
|
-
if (id === selfId
|
|
368
|
+
if (id === selfId
|
|
369
|
+
|| row.status === 'dead'
|
|
370
|
+
|| isPidAlive(row.pi_pid)
|
|
371
|
+
|| meta?.managed_worktree?.state === 'open'
|
|
372
|
+
|| !isEmptyNode(id))
|
|
368
373
|
continue;
|
|
374
|
+
const subscribers = subscribersOf(id);
|
|
369
375
|
if (!dryRun) {
|
|
376
|
+
fanDoctrineWake(id, subscribers, `Child closed — ${meta?.name ?? id} (${id}) was closed from the canvas and is no longer running.`, { reason: 'child-closed', child: id });
|
|
370
377
|
headlessBrokerHost.teardown(id);
|
|
371
378
|
tearDownNode(id);
|
|
372
379
|
if (!deleteNode(id).deleted)
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// Best-effort throughout: a tmux/fs failure on one node never aborts the reap.
|
|
23
23
|
import { getNode, updateNode, fullName, closeFocusRow, view, } from '../canvas/index.js';
|
|
24
24
|
import { transition } from './lifecycle.js';
|
|
25
|
-
import { headlessBrokerHost } from './host.js';
|
|
25
|
+
import { headlessBrokerHost, requestBrokerTeardown } from './host.js';
|
|
26
26
|
import { tearDownNode, focusOf, registerViewerFocus, respawnPaneSync, windowOfPane, renameWindow, } from './placement.js';
|
|
27
27
|
import { viewerSplitEnv, waitForBrokerViewSocket } from './placement-tmux.js';
|
|
28
28
|
import { buildLaunchSpecAsync, buildPiArgv } from './launch.js';
|
|
@@ -51,7 +51,7 @@ export function reapDescendants(rootId) {
|
|
|
51
51
|
// viewer pane + registry row (attach auto-reconnects, so the viewer must be
|
|
52
52
|
// closed here or it lingers ~30s in a misleading "reconnecting…" state).
|
|
53
53
|
transition(id, 'cancel', { reason: 'closed' });
|
|
54
|
-
|
|
54
|
+
requestBrokerTeardown(id);
|
|
55
55
|
tearDownNode(id);
|
|
56
56
|
reaped.push(id);
|
|
57
57
|
}
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
// and there is goal/roadmap ground state to re-enter on.
|
|
23
23
|
//
|
|
24
24
|
// reviveNode remains the ONLY sanctioned launcher of the node engine.
|
|
25
|
-
import { existsSync } from 'node:fs';
|
|
25
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
26
26
|
import { isAbsolute } from 'node:path';
|
|
27
|
-
import { findNodeBySessionFile, getNode, updateNode, clearPid, recordPid, setFrozen, clearFrozen, fullName, subscribersOf, } from '../canvas/index.js';
|
|
27
|
+
import { findNodeBySessionFile, getNode, updateNode, clearPid, recordLaunch, recordPid, setFrozen, clearFrozen, fullName, subscribersOf, } from '../canvas/index.js';
|
|
28
28
|
import { emitEvent } from '../events/emit.js';
|
|
29
29
|
import { cancelCronsOnWake } from '../canvas/crons.js';
|
|
30
30
|
import { isParked, transition } from './lifecycle.js';
|
|
@@ -41,6 +41,7 @@ import { clearContextExposureState, initializeForkContextExposure, } from '../su
|
|
|
41
41
|
import { rootOfSpine } from './nodes.js';
|
|
42
42
|
import { isReviewCompanionBound } from '../review/companion.js';
|
|
43
43
|
import { assertProfileActive } from '../profiles/manifest.js';
|
|
44
|
+
import { isPidAlive } from '../canvas/pid.js';
|
|
44
45
|
// resumeArgs — which session source a revive resumes from
|
|
45
46
|
/** Pick the `--session` source for a revive. Both resume=true (a true resume)
|
|
46
47
|
* and resume=false (a refresh-yield) resume by the absolute session-file path
|
|
@@ -64,6 +65,40 @@ export function resumeArgs(meta, resume) {
|
|
|
64
65
|
return {};
|
|
65
66
|
return resume ? { resumeSessionPath: path } : { resumeSessionPath: path, newCycle: true };
|
|
66
67
|
}
|
|
68
|
+
/** Return false only when a valid Pi session header positively names a cwd that
|
|
69
|
+
* has gone away. Missing/corrupt headers remain the engine's concern: old
|
|
70
|
+
* fixtures and partial transcripts do not prove a cwd mismatch. */
|
|
71
|
+
function sessionCwdExists(sessionFile) {
|
|
72
|
+
try {
|
|
73
|
+
for (const line of readFileSync(sessionFile, 'utf8').split('\n')) {
|
|
74
|
+
if (line.trim() === '')
|
|
75
|
+
continue;
|
|
76
|
+
const entry = JSON.parse(line);
|
|
77
|
+
if (entry.type !== 'session')
|
|
78
|
+
continue;
|
|
79
|
+
return typeof entry.cwd !== 'string' || existsSync(entry.cwd);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// The broker/engine owns malformed-transcript diagnostics; it is not proof
|
|
84
|
+
// that the stored cwd is absent.
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
/** The session-file preflight has two ordered vetoes. A live owner wins before
|
|
89
|
+
* inspecting its transcript, so this node can never adopt or clear another
|
|
90
|
+
* broker's file. Only an unowned/dead-owner file then reaches the cwd check. */
|
|
91
|
+
function resumableSessionArgs(nodeId, meta, resume) {
|
|
92
|
+
const path = meta.pi_session_file;
|
|
93
|
+
if (path == null || path === '' || !existsSync(path))
|
|
94
|
+
return {};
|
|
95
|
+
const owner = findNodeBySessionFile(path);
|
|
96
|
+
if (owner !== null && owner.node_id !== nodeId && isPidAlive(owner.pi_pid))
|
|
97
|
+
return {};
|
|
98
|
+
if (!sessionCwdExists(path))
|
|
99
|
+
return {};
|
|
100
|
+
return resumeArgs(meta, resume);
|
|
101
|
+
}
|
|
67
102
|
// Return type
|
|
68
103
|
/** A relaunch that has NO transcript to resume or branch from, and no durable
|
|
69
104
|
* work state on disk (no roadmap, no yield note), is not a revive at all — the
|
|
@@ -262,7 +297,7 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
262
297
|
const refreshPending = meta.intent === 'refresh' || meta.cycle_pending === true;
|
|
263
298
|
const parkedReentry = isParked(meta) && hasFreshGroundState(nodeId) && hasUnseenWakeMail(nodeId);
|
|
264
299
|
const effectiveResume = refreshPending || parkedReentry ? false : opts.resume;
|
|
265
|
-
const resume = forkBirthPending ? {} :
|
|
300
|
+
const resume = forkBirthPending ? {} : resumableSessionArgs(nodeId, meta, effectiveResume);
|
|
266
301
|
// hasSessionPath: there is a real `.jsonl` to hand pi via `--session`, whether
|
|
267
302
|
// as a true resume OR a cycling refresh-yield (resumeArgs sets `newCycle` in
|
|
268
303
|
// the latter case). cycling and resuming are mutually exclusive; `resuming`
|
|
@@ -354,7 +389,7 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
354
389
|
// this marker advertised. Snapshot the boot-fault attempt in the SAME
|
|
355
390
|
// breath (see beginBootFaultAttempt): its own verification, not this call's
|
|
356
391
|
// unchecked result, is what scopes a later diagnostic read to this attempt.
|
|
357
|
-
clearFault(nodeId);
|
|
392
|
+
clearFault(nodeId, { preserveProviderRetryEpisode: true });
|
|
358
393
|
const bootFault = beginBootFaultAttempt(nodeId);
|
|
359
394
|
// A cron armed --cancel-on-wake belongs to the dormancy being left, so a WAKE
|
|
360
395
|
// of its anchor deletes it — an inbox delivery, an attach, an explicit
|
|
@@ -394,6 +429,7 @@ function launchRevive(nodeId, meta, opts) {
|
|
|
394
429
|
clearPid(nodeId);
|
|
395
430
|
throw new Error(`[broker] refusing to revive ${nodeId}: ${error.message}`);
|
|
396
431
|
}
|
|
432
|
+
recordLaunch(nodeId, new Date().toISOString());
|
|
397
433
|
// Record the real broker pid. The pid is METADATA now (design D-13) — the
|
|
398
434
|
// liveness authority is the fleet entry registered synchronously inside
|
|
399
435
|
// `launch` above — but session_start re-records it after extension bind,
|
|
@@ -11,6 +11,9 @@ export interface SpawnChildOpts {
|
|
|
11
11
|
prompt: string;
|
|
12
12
|
/** Override the parent (defaults to the calling node from env). */
|
|
13
13
|
parent?: string;
|
|
14
|
+
/** Actor that created this node. Unlike parent (manager) and spawned_by
|
|
15
|
+
* (provenance), it controls the parent's birth announcement. */
|
|
16
|
+
creator?: string | null;
|
|
14
17
|
/** Spawn an INDEPENDENT root instead of a managed child: parent=null, no
|
|
15
18
|
* subscription back to the spawner, spawned_by=spawner. Resident and brought
|
|
16
19
|
* forefront by default — see `rootLifecycle` for the unattended variant. */
|
|
@@ -275,6 +275,7 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
275
275
|
// records spawned_by=spawner when a node (not a human shell) spawned it.
|
|
276
276
|
// A child's parent IS its manager.
|
|
277
277
|
parent: root ? null : spawner,
|
|
278
|
+
creator: opts.creator,
|
|
278
279
|
spawnedBy: root ? (spawner ?? undefined) : undefined,
|
|
279
280
|
profile_id: profileId,
|
|
280
281
|
forkFrom: opts.forkFrom,
|
|
@@ -24,6 +24,10 @@ export interface WarmRequest {
|
|
|
24
24
|
model: string | null;
|
|
25
25
|
situationalContext: string | null;
|
|
26
26
|
name: string | null;
|
|
27
|
+
/** Node that asked for this create, or null for an external process. Claim-time
|
|
28
|
+
* identity like `name` — a spare is minted creator-less and adopts the
|
|
29
|
+
* claimant's, so provenance survives the warm path. */
|
|
30
|
+
creator: string | null;
|
|
27
31
|
}
|
|
28
32
|
/** Claim a spare for `request`, reshaped onto its persona — or null when the
|
|
29
33
|
* pool cannot serve it. The caller falls back to a cold spawn; a claim never
|
|
@@ -128,6 +128,7 @@ export async function claimWarmNode(request) {
|
|
|
128
128
|
kind: request.kind,
|
|
129
129
|
mode: request.mode,
|
|
130
130
|
name: request.name ?? request.kind,
|
|
131
|
+
creator: request.creator,
|
|
131
132
|
// The recipe a later revive replays — skip it and the node silently comes
|
|
132
133
|
// back as the spare's persona.
|
|
133
134
|
launch: recipe.launch,
|
|
@@ -354,6 +355,7 @@ export function prewarmRecentRecipes() {
|
|
|
354
355
|
model: null,
|
|
355
356
|
situationalContext: null,
|
|
356
357
|
name: null,
|
|
358
|
+
creator: null,
|
|
357
359
|
}).then(mint).catch((err) => console.error(`[warm-pool] prewarm failed for ${candidate.cwd}: ${err.message}`));
|
|
358
360
|
}
|
|
359
361
|
catch (err) {
|
package/dist/core/worktree.d.ts
CHANGED
|
@@ -57,9 +57,10 @@ interface CleanEmptyProof {
|
|
|
57
57
|
* be a git-ancestor of (or equal to) the CURRENT tip of its recorded base
|
|
58
58
|
* branch — i.e. landing it would be a pure no-op. Any ambiguity (missing
|
|
59
59
|
* path, mid-rebase, wrong branch, an unresolvable base, or a real ancestry
|
|
60
|
-
* divergence) returns null
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* divergence) returns null. A missing base is the one exception: equality
|
|
61
|
+
* between the live HEAD and recorded creation SHA proves no work was added.
|
|
62
|
+
* This function only ever proves a no-op land from live Git state, never on
|
|
63
|
+
* a heuristic guess, so real unlanded work always falls through to the explicit-close guard. The locked
|
|
63
64
|
* path re-checks this predicate before recording deferred cleanup; production
|
|
64
65
|
* code must always go through `autoDropCleanManagedWorktree`. */
|
|
65
66
|
export declare function proveCleanEmptyManagedWorktree(wt: ManagedWorktree): CleanEmptyProof | null;
|