@north-light/crouter 0.3.235 → 0.3.237
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/dist/api/client.d.ts +2 -1
- package/dist/api/client.js +3 -0
- package/dist/api/dto/broker-ops.d.ts +1 -1
- package/dist/api/dto/common.d.ts +2 -0
- package/dist/api/dto/lifecycle.d.ts +9 -2
- package/dist/api/dto/messages.d.ts +5 -0
- package/dist/api/dto/nodes.d.ts +14 -5
- package/dist/api/dto/worktree.d.ts +10 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +1 -1
- package/dist/builtin-memory/internal/agent-shaping.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +1 -1
- package/dist/clients/attach/render/tool-calls.js +18 -3
- package/dist/clients/attach/viewer.js +399 -399
- package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +6 -0
- package/dist/clients/inbox/review/roundtrip.js +1 -1
- package/dist/commands/__tests__/node-new.test.js +58 -0
- package/dist/commands/memory/lint.js +22 -13
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/node/create.d.ts +1 -1
- package/dist/commands/node/create.js +4 -1
- package/dist/commands/node/lifecycle.js +2 -2
- package/dist/commands/node-worktree.js +39 -8
- package/dist/commands/revive.js +11 -2
- package/dist/commands/sys/context/admin/actions.d.ts +48 -0
- package/dist/commands/sys/context/admin/actions.js +246 -0
- package/dist/commands/sys/context/admin/axes-panel.d.ts +21 -0
- package/dist/commands/sys/context/admin/axes-panel.js +117 -0
- package/dist/commands/sys/context/admin/detail-panel.d.ts +53 -0
- package/dist/commands/sys/context/admin/detail-panel.js +178 -0
- package/dist/commands/sys/context/admin/docs-panel.d.ts +45 -0
- package/dist/commands/sys/context/admin/docs-panel.js +186 -0
- package/dist/commands/sys/context/admin/model.d.ts +75 -0
- package/dist/commands/sys/context/admin/model.js +209 -0
- package/dist/commands/sys/context/admin/shell.d.ts +82 -0
- package/dist/commands/sys/context/admin/shell.js +669 -0
- package/dist/commands/sys/context/admin.d.ts +1 -0
- package/dist/commands/sys/context/admin.js +124 -0
- package/dist/commands/sys/context/doc.d.ts +1 -0
- package/dist/commands/sys/context/doc.js +196 -0
- package/dist/commands/sys/context/prompt-review.d.ts +1 -0
- package/dist/commands/sys/{prompt-review.js → context/prompt-review.js} +10 -10
- package/dist/commands/sys/context/resolve.d.ts +55 -0
- package/dist/commands/sys/context/resolve.js +428 -0
- package/dist/commands/sys/context/sysprompt.d.ts +1 -0
- package/dist/commands/sys/{sysprompt.js → context/sysprompt.js} +11 -11
- package/dist/commands/sys/context.d.ts +2 -0
- package/dist/commands/sys/context.js +20 -0
- package/dist/commands/sys.js +3 -4
- package/dist/core/__tests__/canvas-inbox-watcher-naming.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-inbox-watcher-naming.test.js +258 -0
- package/dist/core/__tests__/child-death-wake.test.js +38 -22
- package/dist/core/__tests__/cron-self-cancel-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/cron-self-cancel-delivery.test.js +72 -0
- package/dist/core/__tests__/daemon-boot.test.js +40 -28
- package/dist/core/__tests__/helpers/harness.js +1 -0
- package/dist/core/__tests__/human-deliver.test.js +1 -1
- package/dist/core/__tests__/human-node-not-supervised.test.js +10 -15
- package/dist/core/__tests__/integration/revive.test.js +12 -12
- package/dist/core/__tests__/integration/spawn-root.test.js +6 -0
- package/dist/core/__tests__/integration/worktree-reap.test.js +32 -0
- package/dist/core/__tests__/lifecycle-terminal-reasons.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +61 -0
- package/dist/core/__tests__/migration.test.js +38 -1
- package/dist/core/__tests__/push-final-guard.test.js +1 -1
- package/dist/core/__tests__/revive-capacity.test.d.ts +1 -0
- package/dist/core/__tests__/revive-capacity.test.js +127 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +7 -7
- package/dist/core/__tests__/seam/broker-cap-freeze.test.d.ts +1 -0
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +81 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +76 -7
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +1 -1
- package/dist/core/canvas/browse/render.js +11 -3
- package/dist/core/canvas/canvas.d.ts +15 -3
- package/dist/core/canvas/canvas.js +52 -16
- package/dist/core/canvas/migrations.js +21 -0
- package/dist/core/canvas/nav-render.js +3 -1
- package/dist/core/canvas/remote-canvas-source.js +2 -0
- package/dist/core/canvas/render-source.d.ts +3 -1
- package/dist/core/canvas/render-source.js +7 -3
- package/dist/core/canvas/status-glyph.d.ts +10 -1
- package/dist/core/canvas/status-glyph.js +19 -3
- package/dist/core/canvas/types.d.ts +22 -3
- package/dist/core/feed/feed.js +1 -1
- package/dist/core/human/feedback-companion.js +5 -2
- package/dist/core/memory/lint.d.ts +19 -0
- package/dist/core/memory/lint.js +97 -0
- package/dist/core/memory-resolver.d.ts +4 -0
- package/dist/core/memory-resolver.js +7 -7
- package/dist/core/preview-registry.js +4 -4
- package/dist/core/review/realize.js +4 -2
- package/dist/core/runtime/close.js +1 -1
- package/dist/core/runtime/fleet.d.ts +29 -1
- package/dist/core/runtime/fleet.js +23 -0
- package/dist/core/runtime/host.js +32 -11
- package/dist/core/runtime/launch-prompt.d.ts +1 -1
- package/dist/core/runtime/launch-prompt.js +2 -2
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/lifecycle.d.ts +4 -2
- package/dist/core/runtime/lifecycle.js +12 -1
- package/dist/core/runtime/memory.js +1 -1
- package/dist/core/runtime/reopen.js +10 -1
- package/dist/core/runtime/reset.js +1 -1
- package/dist/core/runtime/revive-all.d.ts +3 -0
- package/dist/core/runtime/revive-all.js +10 -4
- package/dist/core/runtime/revive.d.ts +12 -0
- package/dist/core/runtime/revive.js +62 -3
- package/dist/core/runtime/spawn.d.ts +9 -13
- package/dist/core/runtime/spawn.js +40 -17
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/index.d.ts +6 -3
- package/dist/core/substrate/index.js +4 -3
- package/dist/core/substrate/on-read.d.ts +0 -8
- package/dist/core/substrate/on-read.js +45 -124
- package/dist/core/substrate/plan.d.ts +94 -0
- package/dist/core/substrate/plan.js +266 -0
- package/dist/core/substrate/render.d.ts +5 -6
- package/dist/core/substrate/render.js +76 -129
- package/dist/core/substrate/schema.d.ts +1 -6
- package/dist/core/substrate/schema.js +1 -15
- package/dist/core/substrate/session-cache.d.ts +2 -10
- package/dist/core/substrate/session-cache.js +9 -47
- package/dist/core/substrate/subject-fields.d.ts +1 -4
- package/dist/core/substrate/subject-fields.js +2 -5
- package/dist/core/substrate/surface-match.d.ts +37 -17
- package/dist/core/substrate/surface-match.js +44 -53
- package/dist/core/tui/page-host.d.ts +6 -0
- package/dist/core/tui/page-host.js +12 -3
- package/dist/core/worktree.d.ts +8 -0
- package/dist/core/worktree.js +65 -0
- package/dist/daemon/__tests__/reconciler-signature.test.js +3 -2
- package/dist/daemon/__tests__/thaw-order.test.d.ts +1 -0
- package/dist/daemon/__tests__/thaw-order.test.js +55 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +6 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +6 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +1 -1
- package/dist/daemon/api/bridge.js +5 -1
- package/dist/daemon/api/handlers/attach.js +5 -1
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/messages.js +10 -2
- package/dist/daemon/api/handlers/nodes.js +8 -4
- package/dist/daemon/api/handlers/worktree.js +10 -1
- package/dist/daemon/api/map.js +7 -0
- package/dist/daemon/companion-retire.js +1 -1
- package/dist/daemon/cron/passes.d.ts +0 -5
- package/dist/daemon/cron/passes.js +0 -20
- package/dist/daemon/cron/sinks.js +1 -1
- package/dist/daemon/cron-run.d.ts +5 -6
- package/dist/daemon/cron-run.js +7 -16
- package/dist/daemon/crtrd.d.ts +0 -10
- package/dist/daemon/crtrd.js +23 -61
- package/dist/daemon/fleet.d.ts +10 -7
- package/dist/daemon/fleet.js +71 -12
- package/dist/daemon/messaging/node-message.js +3 -1
- package/dist/daemon/profile-delete.js +1 -1
- package/dist/daemon/reconcilers/broker-supervision.d.ts +26 -9
- package/dist/daemon/reconcilers/broker-supervision.js +39 -52
- package/dist/daemon/reconcilers/controller-death.d.ts +7 -0
- package/dist/daemon/reconcilers/controller-death.js +26 -0
- package/dist/daemon/reconcilers/cron-lane.d.ts +0 -3
- package/dist/daemon/reconcilers/cron-lane.js +0 -3
- package/dist/daemon/reconcilers/live-obligation.js +3 -1
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +53 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +170 -0
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +5 -0
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +16 -0
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -0
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +13 -0
- package/dist/daemon/reconcilers/node-lifecycle/tick.d.ts +26 -0
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +140 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +12 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/commands/sys/prompt-review.d.ts +0 -1
- package/dist/commands/sys/sysprompt.d.ts +0 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +0 -119
- package/dist/daemon/reconcilers/dormant-inbox.d.ts +0 -18
- package/dist/daemon/reconcilers/dormant-inbox.js +0 -157
- /package/dist/{core/__tests__/cron-broker-capacity.test.d.ts → commands/__tests__/node-new.test.d.ts} +0 -0
|
@@ -22,7 +22,7 @@ import assert from 'node:assert/strict';
|
|
|
22
22
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { appendInbox, readCursor, readInboxSince } from '../../feed/inbox.js';
|
|
25
|
-
import { registerWarmSpare, subscribe, updateNode } from '../../canvas/index.js';
|
|
25
|
+
import { registerWarmSpare, subscribe, subscriptionsOf, updateNode } from '../../canvas/index.js';
|
|
26
26
|
import { isPidAlive } from '../../canvas/pid.js';
|
|
27
27
|
import { reviveNode } from '../../runtime/revive.js';
|
|
28
28
|
import { createHarness } from '../helpers/harness.js';
|
|
@@ -78,7 +78,7 @@ test('idle-release settles its real watcher before exit; daemon revives exactly
|
|
|
78
78
|
},
|
|
79
79
|
},
|
|
80
80
|
});
|
|
81
|
-
reviveNode(manager, { resume: true });
|
|
81
|
+
reviveNode(manager, { resume: true, capacity: 'freeze' });
|
|
82
82
|
await h.awaitBoot(manager);
|
|
83
83
|
// A live managed child makes this unfocused terminal manager legitimately
|
|
84
84
|
// awaiting, so its real stophook takes the idle-release branch.
|
|
@@ -171,7 +171,7 @@ test('an unattended resident with no live obligation completes on the daemon clo
|
|
|
171
171
|
},
|
|
172
172
|
});
|
|
173
173
|
subscribe(root, nodeId, true);
|
|
174
|
-
reviveNode(nodeId, { resume: true });
|
|
174
|
+
reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
175
175
|
const boot = await h.awaitBoot(nodeId);
|
|
176
176
|
assert.equal(h.fleet.has(nodeId), true, 'the revived broker is fleet-owned');
|
|
177
177
|
const seenAt = 10_000;
|
|
@@ -223,7 +223,7 @@ test('a non-attending observer does not prevent unattended resident parking', {
|
|
|
223
223
|
intent: 'idle-release',
|
|
224
224
|
});
|
|
225
225
|
updateNode(nodeId, { launch: { extensions: [], tools: [], systemPrompt: '', env: {} } });
|
|
226
|
-
reviveNode(nodeId, { resume: true });
|
|
226
|
+
reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
227
227
|
const boot = await h.awaitBoot(nodeId);
|
|
228
228
|
await attach.attach(nodeId, 'observer', 'system-observer-test', { attends: false });
|
|
229
229
|
assert.equal(JSON.parse(readFileSync(join(h.home, 'nodes', nodeId, 'job', 'attach.json'), 'utf8')).viewers, 0, 'a non-attending hello is not persisted as human attendance');
|
|
@@ -268,7 +268,7 @@ test('a connected broker viewer keeps an obligation-free resident live', { timeo
|
|
|
268
268
|
env: {},
|
|
269
269
|
},
|
|
270
270
|
});
|
|
271
|
-
reviveNode(nodeId, { resume: true });
|
|
271
|
+
reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
272
272
|
const boot = await h.awaitBoot(nodeId);
|
|
273
273
|
// awaitBoot returns on the boot proof, which is written BEFORE the broker's
|
|
274
274
|
// own `persistAttachState()` lays down the truthful zero state. Writing our
|
|
@@ -311,7 +311,7 @@ test('the unattended park clock cancels an obligation-free terminal node and spa
|
|
|
311
311
|
try {
|
|
312
312
|
const boot = async (id) => {
|
|
313
313
|
updateNode(id, { launch: { extensions: [], tools: [], systemPrompt: '', env: {} } });
|
|
314
|
-
reviveNode(id, { resume: true });
|
|
314
|
+
reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
315
315
|
return (await h.awaitBoot(id)).pid;
|
|
316
316
|
};
|
|
317
317
|
const settled = h.fabricateBrokerNode({ id: 'terminal-no-work', parent: root, status: 'idle', intent: 'idle-release' });
|
|
@@ -349,6 +349,75 @@ test('the unattended park clock cancels an obligation-free terminal node and spa
|
|
|
349
349
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
|
|
350
350
|
}
|
|
351
351
|
});
|
|
352
|
+
test('a resident managed child parks without finalizing while a terminal child is settled by the same grace sweep', { timeout: 40_000 }, async () => {
|
|
353
|
+
const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
354
|
+
const priorGraceMs = process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'];
|
|
355
|
+
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
|
|
356
|
+
process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'] = '1000';
|
|
357
|
+
try {
|
|
358
|
+
const resident = h.fabricateBrokerNode({
|
|
359
|
+
id: 'resident-managed-child',
|
|
360
|
+
parent: root,
|
|
361
|
+
lifecycle: 'resident',
|
|
362
|
+
status: 'idle',
|
|
363
|
+
intent: 'idle-release',
|
|
364
|
+
pi_session_id: 'resident-managed-session',
|
|
365
|
+
});
|
|
366
|
+
subscribe(root, resident, true);
|
|
367
|
+
// Keep a managed terminal child in the same sweep, but make its parent
|
|
368
|
+
// historical so no live subscriber remains to hold an unfinished report.
|
|
369
|
+
const terminalManager = h.fabricateBrokerNode({ id: 'terminal-manager', status: 'done' });
|
|
370
|
+
const terminal = h.fabricateBrokerNode({
|
|
371
|
+
id: 'terminal-managed-child',
|
|
372
|
+
parent: terminalManager,
|
|
373
|
+
lifecycle: 'terminal',
|
|
374
|
+
status: 'idle',
|
|
375
|
+
intent: 'idle-release',
|
|
376
|
+
pi_session_id: 'terminal-managed-session',
|
|
377
|
+
});
|
|
378
|
+
subscribe(terminalManager, terminal, true);
|
|
379
|
+
const boot = async (id) => {
|
|
380
|
+
updateNode(id, { launch: { extensions: [], tools: [], systemPrompt: '', env: {} } });
|
|
381
|
+
reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
382
|
+
return (await h.awaitBoot(id)).pid;
|
|
383
|
+
};
|
|
384
|
+
const residentPid = await boot(resident);
|
|
385
|
+
const terminalPid = await boot(terminal);
|
|
386
|
+
const seenAt = 50_000;
|
|
387
|
+
await h.tick(seenAt);
|
|
388
|
+
// Clock expiry arms the resident's pending park and requests its summary
|
|
389
|
+
// turn; the terminal child is cancelled outright by the same pass.
|
|
390
|
+
await h.tick(seenAt + 1500);
|
|
391
|
+
// Summary grace expiry: the degraded enactment parks without a report.
|
|
392
|
+
await h.tick(seenAt + 1500 + 1000);
|
|
393
|
+
await h.waitFor(() => h.node(resident)?.status === 'done' && h.node(resident)?.intent === 'parked' && !isPidAlive(residentPid), {
|
|
394
|
+
timeoutMs: 10_000,
|
|
395
|
+
intervalMs: 20,
|
|
396
|
+
label: 'resident managed child parks and releases its broker',
|
|
397
|
+
});
|
|
398
|
+
await h.waitFor(() => h.node(terminal)?.status === 'canceled' && !isPidAlive(terminalPid), {
|
|
399
|
+
timeoutMs: 10_000,
|
|
400
|
+
intervalMs: 20,
|
|
401
|
+
label: 'terminal managed child settles and releases its broker',
|
|
402
|
+
});
|
|
403
|
+
await h.awaitFleetExit(resident);
|
|
404
|
+
await h.awaitFleetExit(terminal);
|
|
405
|
+
assert.equal(h.node(resident)?.intent, 'parked', 'resident child carries the durable park marker, not a final');
|
|
406
|
+
assert.equal(h.node(resident)?.final_report ?? null, null, 'resident child has no forced final report');
|
|
407
|
+
assert.ok(subscriptionsOf(root).some((edge) => edge.node_id === resident && edge.active), 'resident child keeps its active parent subscription');
|
|
408
|
+
assert.equal(h.node(terminal)?.status, 'canceled', 'terminal child is settled by unattended grace without a final');
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
if (priorParkMs === undefined)
|
|
412
|
+
delete process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
413
|
+
else
|
|
414
|
+
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = priorParkMs;
|
|
415
|
+
if (priorGraceMs === undefined)
|
|
416
|
+
delete process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'];
|
|
417
|
+
else
|
|
418
|
+
process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS'] = priorGraceMs;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
352
421
|
test('an unattended resident awaiting a live child remains wakeable', { timeout: 30_000 }, async () => {
|
|
353
422
|
const priorParkMs = process.env['CRTR_TEST_UNATTENDED_PARK_MS'];
|
|
354
423
|
process.env['CRTR_TEST_UNATTENDED_PARK_MS'] = '1500';
|
|
@@ -370,7 +439,7 @@ test('an unattended resident awaiting a live child remains wakeable', { timeout:
|
|
|
370
439
|
env: {},
|
|
371
440
|
},
|
|
372
441
|
});
|
|
373
|
-
reviveNode(nodeId, { resume: true });
|
|
442
|
+
reviveNode(nodeId, { resume: true, capacity: 'freeze' });
|
|
374
443
|
const boot = await h.awaitBoot(nodeId);
|
|
375
444
|
const seenAt = 20_000;
|
|
376
445
|
await h.tick(seenAt);
|
|
@@ -65,7 +65,7 @@ test('yield-message survives one pre-session_start retry and clears on session_s
|
|
|
65
65
|
// the retry still roots a fresh cycle carrying the note, never reopening the
|
|
66
66
|
// old session. Driving reviveNode directly is exactly what focus's revive path
|
|
67
67
|
// calls, minus a tmux pane this contract does not involve.
|
|
68
|
-
const focusRevive = reviveNode(id, { resume: true });
|
|
68
|
+
const focusRevive = reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
69
69
|
const focusPid = focusRevive.launch?.pid;
|
|
70
70
|
assert.ok(focusPid != null, 'the competing focus invocation launches the replacement broker');
|
|
71
71
|
assert.equal(focusRevive.resumed, false, 'refresh intent overrides this invocation\'s strict-resume request');
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Every hue (fg / bg color) is gated on `caps.color`; structural SGR (bold/dim/
|
|
12
12
|
// reverse) is allowed always. See detectColorCaps() for the gate, and the
|
|
13
13
|
// canvas-browse color spec for the rationale + palette.
|
|
14
|
-
import { STATUS_GLYPH, STATUS_COLOR, resolveNodeVisual, faultSummary } from '../status-glyph.js';
|
|
14
|
+
import { STATUS_GLYPH, STATUS_COLOR, isFrozenRow, resolveNodeVisual, faultSummary } from '../status-glyph.js';
|
|
15
15
|
import { TABS, matchIndices, promptText, previewSnippet } from './model.js';
|
|
16
16
|
// Span/color primitives live in core/tui/draw.ts (one copy, shared with the
|
|
17
17
|
// `crtr view` host). Re-export the color caps so browse's importers + tests keep
|
|
@@ -185,7 +185,11 @@ function spansWidth(spans) {
|
|
|
185
185
|
* node is genuinely mid-turn; otherwise the lifecycle word. The word is what makes
|
|
186
186
|
* state legible at a glance and survives NO_COLOR (the glyph is the second cue). */
|
|
187
187
|
function statusRail(r) {
|
|
188
|
-
const v = resolveNodeVisual(r.status, {
|
|
188
|
+
const v = resolveNodeVisual(r.status, {
|
|
189
|
+
streaming: r.streaming === true,
|
|
190
|
+
frozen: isFrozenRow(r),
|
|
191
|
+
hanging: r.hanging ?? null,
|
|
192
|
+
});
|
|
189
193
|
// Plain status keeps browse's own status WORD; overlays (hanging/⟳)
|
|
190
194
|
// carry their own word from resolveNodeVisual. A hanging kind label can exceed
|
|
191
195
|
// STATUS_W — acceptable: the rare, attention-demanding row gets a slightly
|
|
@@ -366,7 +370,11 @@ function previewPanel(r, width, caps, now, query) {
|
|
|
366
370
|
out.push('');
|
|
367
371
|
return out;
|
|
368
372
|
}
|
|
369
|
-
const v = resolveNodeVisual(r.status, {
|
|
373
|
+
const v = resolveNodeVisual(r.status, {
|
|
374
|
+
streaming: r.streaming === true,
|
|
375
|
+
frozen: isFrozenRow(r),
|
|
376
|
+
hanging: r.hanging ?? null,
|
|
377
|
+
});
|
|
370
378
|
const glyph = caps.color
|
|
371
379
|
? `${ESC}${v.color}m${v.glyph}${RESET}`
|
|
372
380
|
: v.glyph;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NodeMeta, NodeIdentity, NodeRow, NodeStatus, ExitIntent, SubscriptionRef, Mode, Lifecycle } from './types.js';
|
|
1
|
+
import type { NodeMeta, NodeIdentity, NodeRow, NodeStatus, ExitIntent, SubscriptionRef, Mode, Lifecycle, TerminalReason } from './types.js';
|
|
2
2
|
/** Create a node: scaffold its dirs, persist identity to meta.json, and seed the
|
|
3
3
|
* row (identity + runtime from the incoming meta). Returns the hydrated view. */
|
|
4
4
|
export declare function createNode(meta: NodeMeta): NodeMeta;
|
|
@@ -35,6 +35,7 @@ export declare function setIntent(nodeId: string, intent: ExitIntent): void;
|
|
|
35
35
|
export declare function transitionStatusIntent(nodeId: string, fields: {
|
|
36
36
|
status?: NodeStatus;
|
|
37
37
|
intent?: ExitIntent;
|
|
38
|
+
terminal_reason?: TerminalReason | null;
|
|
38
39
|
}, opts?: {
|
|
39
40
|
fromStatuses?: readonly NodeStatus[];
|
|
40
41
|
requireNoFinalReport?: boolean;
|
|
@@ -74,7 +75,16 @@ export declare function recordPid(nodeId: string, pid: number): void;
|
|
|
74
75
|
* `recordPid` and must be cleared together, or a subsequent relaunch that
|
|
75
76
|
* reuses the pid namespace could compare its own fresh pid against a STALE
|
|
76
77
|
* identity left over from the pid this row held before. */
|
|
77
|
-
export declare function
|
|
78
|
+
export declare function clearRecordedPid(nodeId: string): void;
|
|
79
|
+
export declare const clearPid: typeof clearRecordedPid;
|
|
80
|
+
export declare function recordLaunch(nodeId: string, at: string): void;
|
|
81
|
+
export declare function recordSettledTurn(nodeId: string, at: string): void;
|
|
82
|
+
export declare function setFrozen(nodeId: string, at: string): void;
|
|
83
|
+
export declare function clearFrozen(nodeId: string): void;
|
|
84
|
+
export declare function recordRespawnAttempt(nodeId: string, attempt: {
|
|
85
|
+
failures: number;
|
|
86
|
+
notBefore: string | null;
|
|
87
|
+
}): void;
|
|
78
88
|
/** ONE-TIME completion of the boot-scoped-identity format migration, run once
|
|
79
89
|
* at daemon startup (the natural boot-reconciliation chokepoint, right after
|
|
80
90
|
* `reconcileBootLiveness`). It closes the gap the `identitiesMatch` legacy
|
|
@@ -119,9 +129,11 @@ export declare function migrateLegacyPidIdentities(captureIdentity?: (pid: numbe
|
|
|
119
129
|
* Unlike `listNodes`, this includes unclaimed warm-pool spares: profile
|
|
120
130
|
* deletion owns every matching row, whether or not it is user-visible. */
|
|
121
131
|
export declare function listNodesByProfile(profileId: string): NodeMeta[];
|
|
122
|
-
/** All rows, optionally filtered by status
|
|
132
|
+
/** All rows, optionally filtered by status and by carrying a recorded pid (the
|
|
133
|
+
* supervision tick's terminal-row query). Unclaimed warm spares are excluded. */
|
|
123
134
|
export declare function listNodes(filter?: {
|
|
124
135
|
status?: NodeStatus | NodeStatus[];
|
|
136
|
+
withRecordedPid?: boolean;
|
|
125
137
|
}): NodeRow[];
|
|
126
138
|
/** Mark an already-spawned node as an unclaimed spare for `recipeKey`. Hides it
|
|
127
139
|
* from every listing until it is claimed. */
|
|
@@ -13,10 +13,9 @@ import { isPidAlive, recordedPidLiveness, capturePidIdentities, identitiesMatch,
|
|
|
13
13
|
import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, jobDir, } from './paths.js';
|
|
14
14
|
// meta.json (durable identity — the source of truth for what PERSISTS)
|
|
15
15
|
//
|
|
16
|
-
// One authoritative store per fact: meta.json holds NodeIdentity only;
|
|
17
|
-
// runtime fields
|
|
18
|
-
//
|
|
19
|
-
// below. getNode() hydrates the two back into the historical NodeMeta view.
|
|
16
|
+
// One authoritative store per fact: meta.json holds NodeIdentity only; live
|
|
17
|
+
// runtime fields are authoritative in the WAL'd `nodes` row. getNode() hydrates
|
|
18
|
+
// the two back into the historical NodeMeta view.
|
|
20
19
|
/** The identity keys meta.json persists. Listed explicitly so no runtime field
|
|
21
20
|
* can ever leak onto disk even when a fully-hydrated NodeMeta is handed in. */
|
|
22
21
|
const IDENTITY_KEYS = [
|
|
@@ -129,6 +128,12 @@ function rowFrom(r) {
|
|
|
129
128
|
created: r['created'],
|
|
130
129
|
intent: r['intent'] ?? null,
|
|
131
130
|
waiting_for: r['waiting_for'] ?? null,
|
|
131
|
+
frozen_at: r['frozen_at'] ?? null,
|
|
132
|
+
respawn_failures: r['respawn_failures'] ?? 0,
|
|
133
|
+
respawn_not_before: r['respawn_not_before'] ?? null,
|
|
134
|
+
launched_at: r['launched_at'] ?? null,
|
|
135
|
+
last_settled_at: r['last_settled_at'] ?? null,
|
|
136
|
+
terminal_reason: r['terminal_reason'] ?? null,
|
|
132
137
|
final_report: r['final_report'] ?? null,
|
|
133
138
|
finalized_at: r['finalized_at'] ?? null,
|
|
134
139
|
pi_pid: r['pi_pid'] ?? null,
|
|
@@ -142,7 +147,7 @@ function rowFrom(r) {
|
|
|
142
147
|
* no row exists yet (the hydration then falls back to whatever the meta held). */
|
|
143
148
|
function runtimeFromRow(nodeId) {
|
|
144
149
|
const r = openDb()
|
|
145
|
-
.prepare('SELECT status, intent, waiting_for, final_report, finalized_at, pi_pid, pi_pid_identity, "window", tmux_session, pane FROM nodes WHERE node_id = ?')
|
|
150
|
+
.prepare('SELECT status, intent, waiting_for, frozen_at, respawn_failures, respawn_not_before, launched_at, last_settled_at, terminal_reason, final_report, finalized_at, pi_pid, pi_pid_identity, "window", tmux_session, pane FROM nodes WHERE node_id = ?')
|
|
146
151
|
.get(nodeId);
|
|
147
152
|
if (r === undefined)
|
|
148
153
|
return null;
|
|
@@ -150,6 +155,12 @@ function runtimeFromRow(nodeId) {
|
|
|
150
155
|
status: r['status'] ?? 'active',
|
|
151
156
|
intent: r['intent'] ?? null,
|
|
152
157
|
waiting_for: r['waiting_for'] ?? null,
|
|
158
|
+
frozen_at: r['frozen_at'] ?? null,
|
|
159
|
+
respawn_failures: r['respawn_failures'] ?? 0,
|
|
160
|
+
respawn_not_before: r['respawn_not_before'] ?? null,
|
|
161
|
+
launched_at: r['launched_at'] ?? null,
|
|
162
|
+
last_settled_at: r['last_settled_at'] ?? null,
|
|
163
|
+
terminal_reason: r['terminal_reason'] ?? null,
|
|
153
164
|
final_report: r['final_report'] ?? null,
|
|
154
165
|
finalized_at: r['finalized_at'] ?? null,
|
|
155
166
|
pi_pid: r['pi_pid'] ?? null,
|
|
@@ -273,6 +284,10 @@ export function transitionStatusIntent(nodeId, fields, opts = {}) {
|
|
|
273
284
|
assignments.push('intent = ?');
|
|
274
285
|
values.push(fields.intent ?? null);
|
|
275
286
|
}
|
|
287
|
+
if (Object.prototype.hasOwnProperty.call(fields, 'terminal_reason')) {
|
|
288
|
+
assignments.push('terminal_reason = ?');
|
|
289
|
+
values.push(fields.terminal_reason ?? null);
|
|
290
|
+
}
|
|
276
291
|
const predicates = ['node_id = ?'];
|
|
277
292
|
values.push(nodeId);
|
|
278
293
|
if (opts.fromStatuses !== undefined) {
|
|
@@ -334,9 +349,29 @@ export function recordPid(nodeId, pid) {
|
|
|
334
349
|
* `recordPid` and must be cleared together, or a subsequent relaunch that
|
|
335
350
|
* reuses the pid namespace could compare its own fresh pid against a STALE
|
|
336
351
|
* identity left over from the pid this row held before. */
|
|
337
|
-
export function
|
|
352
|
+
export function clearRecordedPid(nodeId) {
|
|
338
353
|
openDb().prepare('UPDATE nodes SET pi_pid = NULL, pi_pid_identity = NULL WHERE node_id = ?').run(nodeId);
|
|
339
354
|
}
|
|
355
|
+
export const clearPid = clearRecordedPid;
|
|
356
|
+
export function recordLaunch(nodeId, at) {
|
|
357
|
+
openDb()
|
|
358
|
+
.prepare('UPDATE nodes SET launched_at = ?, frozen_at = NULL, respawn_not_before = NULL WHERE node_id = ?')
|
|
359
|
+
.run(at, nodeId);
|
|
360
|
+
}
|
|
361
|
+
export function recordSettledTurn(nodeId, at) {
|
|
362
|
+
openDb().prepare('UPDATE nodes SET last_settled_at = ? WHERE node_id = ?').run(at, nodeId);
|
|
363
|
+
}
|
|
364
|
+
export function setFrozen(nodeId, at) {
|
|
365
|
+
openDb().prepare('UPDATE nodes SET frozen_at = ? WHERE node_id = ?').run(at, nodeId);
|
|
366
|
+
}
|
|
367
|
+
export function clearFrozen(nodeId) {
|
|
368
|
+
openDb().prepare('UPDATE nodes SET frozen_at = NULL WHERE node_id = ?').run(nodeId);
|
|
369
|
+
}
|
|
370
|
+
export function recordRespawnAttempt(nodeId, attempt) {
|
|
371
|
+
openDb()
|
|
372
|
+
.prepare('UPDATE nodes SET respawn_failures = ?, respawn_not_before = ? WHERE node_id = ?')
|
|
373
|
+
.run(attempt.failures, attempt.notBefore, nodeId);
|
|
374
|
+
}
|
|
340
375
|
/** ONE-TIME completion of the boot-scoped-identity format migration, run once
|
|
341
376
|
* at daemon startup (the natural boot-reconciliation chokepoint, right after
|
|
342
377
|
* `reconcileBootLiveness`). It closes the gap the `identitiesMatch` legacy
|
|
@@ -421,20 +456,21 @@ export function listNodesByProfile(profileId) {
|
|
|
421
456
|
return meta;
|
|
422
457
|
});
|
|
423
458
|
}
|
|
424
|
-
/** All rows, optionally filtered by status
|
|
459
|
+
/** All rows, optionally filtered by status and by carrying a recorded pid (the
|
|
460
|
+
* supervision tick's terminal-row query). Unclaimed warm spares are excluded. */
|
|
425
461
|
export function listNodes(filter) {
|
|
426
|
-
const
|
|
427
|
-
|
|
462
|
+
const clauses = [NOT_A_SPARE];
|
|
463
|
+
const params = [];
|
|
428
464
|
if (filter?.status !== undefined) {
|
|
429
465
|
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
.prepare(`SELECT * FROM nodes WHERE status IN (${placeholders}) AND ${NOT_A_SPARE} ORDER BY created`)
|
|
433
|
-
.all(...statuses);
|
|
434
|
-
}
|
|
435
|
-
else {
|
|
436
|
-
rows = db.prepare(`SELECT * FROM nodes WHERE ${NOT_A_SPARE} ORDER BY created`).all();
|
|
466
|
+
clauses.push(`status IN (${statuses.map(() => '?').join(',')})`);
|
|
467
|
+
params.push(...statuses);
|
|
437
468
|
}
|
|
469
|
+
if (filter?.withRecordedPid === true)
|
|
470
|
+
clauses.push('pi_pid IS NOT NULL');
|
|
471
|
+
const rows = openDb()
|
|
472
|
+
.prepare(`SELECT * FROM nodes WHERE ${clauses.join(' AND ')} ORDER BY created`)
|
|
473
|
+
.all(...params);
|
|
438
474
|
return rows.map(rowFrom);
|
|
439
475
|
}
|
|
440
476
|
// Warm pool — spare booted roots awaiting a `prefer_warm` claim (see
|
|
@@ -1215,6 +1215,26 @@ function addNodeSessionFileIndex(db) {
|
|
|
1215
1215
|
}
|
|
1216
1216
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_pi_session_file ON nodes(pi_session_file);');
|
|
1217
1217
|
}
|
|
1218
|
+
/** v35 — durable node lifecycle execution state and terminal cause. */
|
|
1219
|
+
function addNodeLifecycleColumns(db) {
|
|
1220
|
+
const columns = nodeColumns(db);
|
|
1221
|
+
addNodeColumnIfMissing(db, columns, 'frozen_at', 'ALTER TABLE nodes ADD COLUMN frozen_at TEXT;');
|
|
1222
|
+
addNodeColumnIfMissing(db, columns, 'respawn_failures', 'ALTER TABLE nodes ADD COLUMN respawn_failures INTEGER NOT NULL DEFAULT 0;');
|
|
1223
|
+
addNodeColumnIfMissing(db, columns, 'respawn_not_before', 'ALTER TABLE nodes ADD COLUMN respawn_not_before TEXT;');
|
|
1224
|
+
addNodeColumnIfMissing(db, columns, 'launched_at', 'ALTER TABLE nodes ADD COLUMN launched_at TEXT;');
|
|
1225
|
+
addNodeColumnIfMissing(db, columns, 'last_settled_at', 'ALTER TABLE nodes ADD COLUMN last_settled_at TEXT;');
|
|
1226
|
+
addNodeColumnIfMissing(db, columns, 'terminal_reason', 'ALTER TABLE nodes ADD COLUMN terminal_reason TEXT;');
|
|
1227
|
+
db.exec(`
|
|
1228
|
+
UPDATE nodes SET terminal_reason = CASE
|
|
1229
|
+
WHEN status = 'done' AND final_report IS NOT NULL THEN 'finalized'
|
|
1230
|
+
WHEN status = 'done' AND intent = 'parked' THEN 'parked'
|
|
1231
|
+
WHEN status = 'done' THEN 'finished'
|
|
1232
|
+
WHEN status = 'canceled' THEN 'stranded'
|
|
1233
|
+
WHEN status = 'dead' THEN 'crashed'
|
|
1234
|
+
END
|
|
1235
|
+
WHERE status IN ('done','canceled','dead') AND terminal_reason IS NULL;
|
|
1236
|
+
`);
|
|
1237
|
+
}
|
|
1218
1238
|
/** The journaled lane of the state-migration registry, adapted to this chain's
|
|
1219
1239
|
* step shape. Registry order is preserved, so these versions stay stable as
|
|
1220
1240
|
* long as the registry is append-only (same contract as the local list). */
|
|
@@ -1262,6 +1282,7 @@ export const MIGRATIONS = [
|
|
|
1262
1282
|
/* v32 */ addCronHeldColumns,
|
|
1263
1283
|
/* v33 */ addHumanActionDeliveries,
|
|
1264
1284
|
/* v34 */ addNodeSessionFileIndex,
|
|
1285
|
+
/* v35 */ addNodeLifecycleColumns,
|
|
1265
1286
|
...JOURNALED_STATE_MIGRATIONS,
|
|
1266
1287
|
];
|
|
1267
1288
|
/** Migration indexes that manage their OWN transaction and therefore must not
|
|
@@ -14,7 +14,7 @@ import { isPidAlive } from './pid.js';
|
|
|
14
14
|
import { isBusy } from '../runtime/busy.js';
|
|
15
15
|
import { readFault } from '../runtime/fault.js';
|
|
16
16
|
import { activeFaultForDisplay } from './render-source.js';
|
|
17
|
-
import { resolveNodeVisual } from './status-glyph.js';
|
|
17
|
+
import { isFrozenRow, resolveNodeVisual } from './status-glyph.js';
|
|
18
18
|
// Tuning constants
|
|
19
19
|
/** pi's InteractiveMode.MAX_WIDGET_LINES — the hard cap on lines in a string
|
|
20
20
|
* array widget; anything beyond it pi truncates with its own "... (widget
|
|
@@ -62,6 +62,8 @@ export function nodeGlyph(node, remote = false) {
|
|
|
62
62
|
return '?';
|
|
63
63
|
const v = resolveNodeVisual(node.status, {
|
|
64
64
|
streaming: remote ? false : streamingFor(node),
|
|
65
|
+
// Durable row state, so it holds for a remote node too.
|
|
66
|
+
frozen: isFrozenRow(node),
|
|
65
67
|
hanging: remote ? null : hangingFor(node),
|
|
66
68
|
});
|
|
67
69
|
return `${ESC}${v.color}m${v.glyph}${RESET}`;
|
|
@@ -161,6 +161,8 @@ export class RemoteCanvasSource {
|
|
|
161
161
|
created: n.created,
|
|
162
162
|
intent: n.intent ?? null,
|
|
163
163
|
waiting_for: n.waiting_for ?? null,
|
|
164
|
+
frozen_at: n.frozen_at ?? null,
|
|
165
|
+
terminal_reason: n.terminal_reason ?? null,
|
|
164
166
|
pi_pid: n.pi_pid ?? null,
|
|
165
167
|
pi_pid_identity: n.pi_pid_identity ?? null,
|
|
166
168
|
window: n.window ?? null,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CanvasSource } from './source.js';
|
|
2
2
|
import { type Fault } from '../runtime/fault.js';
|
|
3
|
-
import type { NodeStatus, Lifecycle, NodeMeta, NodeRow } from './types.js';
|
|
3
|
+
import type { NodeStatus, Lifecycle, NodeMeta, NodeRow, TerminalReason } from './types.js';
|
|
4
4
|
export declare function activeFaultForDisplay<T extends {
|
|
5
5
|
status: NodeStatus;
|
|
6
6
|
pi_pid?: number | null;
|
|
@@ -24,6 +24,8 @@ export interface DashboardRow {
|
|
|
24
24
|
/** Generated first-task description. Searchable below user-authored prompts. */
|
|
25
25
|
description?: string;
|
|
26
26
|
status: NodeStatus;
|
|
27
|
+
frozen_at?: string | null;
|
|
28
|
+
terminal_reason?: TerminalReason | null;
|
|
27
29
|
kind: string;
|
|
28
30
|
mode: string;
|
|
29
31
|
ctx_tokens: number;
|
|
@@ -18,7 +18,7 @@ import { RemoteCanvasSource } from './remote-canvas-source.js';
|
|
|
18
18
|
import { fullName } from './labels.js';
|
|
19
19
|
import { jobDir, contextDir } from './paths.js';
|
|
20
20
|
import { isPidAlive } from './pid.js';
|
|
21
|
-
import { resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
21
|
+
import { isFrozenRow, resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
22
22
|
import { readFault } from '../runtime/fault.js';
|
|
23
23
|
import { parseCard } from '../../shared/generated-context.js';
|
|
24
24
|
// Active-fault overlay: stale daemon-owned provider faults are hidden once the
|
|
@@ -63,7 +63,7 @@ async function nodeLineFromSource(source, nodeId, indent, connector, remote, tic
|
|
|
63
63
|
if (node === null)
|
|
64
64
|
return `${indent}${connector}? <missing meta: ${nodeId}>`;
|
|
65
65
|
const hanging = remote ? null : hangingFor(node);
|
|
66
|
-
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
66
|
+
const glyph = resolveNodeVisual(node.status, { hanging, frozen: isFrozenRow(node) }).glyph;
|
|
67
67
|
const ctx = remote ? '0k' : fmtCtx(readNodeTelemetry(nodeId).tokens_in);
|
|
68
68
|
const asks = tickets[nodeId] ?? 0;
|
|
69
69
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
@@ -114,7 +114,7 @@ export async function renderTreeFromSource(source, rootId) {
|
|
|
114
114
|
const asks = tickets[rootId] ?? 0;
|
|
115
115
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
116
116
|
const hanging = remote ? null : hangingFor(node);
|
|
117
|
-
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
117
|
+
const glyph = resolveNodeVisual(node.status, { hanging, frozen: isFrozenRow(node) }).glyph;
|
|
118
118
|
const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
|
|
119
119
|
const out = [];
|
|
120
120
|
out.push(`${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`);
|
|
@@ -388,6 +388,8 @@ export function dashboardRowFromNodeRow(row, focusedNodeIds, remote) {
|
|
|
388
388
|
name: row.name, // handle only; enrichRow upgrades to fullName (meta.description)
|
|
389
389
|
handle: row.name,
|
|
390
390
|
status: row.status,
|
|
391
|
+
frozen_at: row.frozen_at ?? null,
|
|
392
|
+
terminal_reason: row.terminal_reason ?? null,
|
|
391
393
|
kind: row.kind,
|
|
392
394
|
mode: row.mode,
|
|
393
395
|
ctx_tokens: 0, // lazy: enrichRow
|
|
@@ -423,6 +425,8 @@ export async function dashboardRowsFromSource(source, rootId) {
|
|
|
423
425
|
handle: node.name,
|
|
424
426
|
description: node.description,
|
|
425
427
|
status: node.status,
|
|
428
|
+
frozen_at: node.frozen_at ?? null,
|
|
429
|
+
terminal_reason: node.terminal_reason ?? null,
|
|
426
430
|
kind: node.kind,
|
|
427
431
|
mode: node.mode,
|
|
428
432
|
ctx_tokens: tel.tokens_in ?? 0,
|
|
@@ -15,10 +15,19 @@ export interface NodeVisual {
|
|
|
15
15
|
word?: string;
|
|
16
16
|
bold?: boolean;
|
|
17
17
|
}
|
|
18
|
+
/** Whether a row is frozen: the broker ledger marked it waiting for a slot and
|
|
19
|
+
* no process holds it. Both halves matter — a launch clears `frozen_at`, so a
|
|
20
|
+
* mark sitting beside a live pid is stale rather than frozen. A projection
|
|
21
|
+
* that carries no pid at all (the dashboard row) reads on the mark alone. */
|
|
22
|
+
export declare function isFrozenRow(row: {
|
|
23
|
+
frozen_at?: string | null;
|
|
24
|
+
pi_pid?: number | null;
|
|
25
|
+
}): boolean;
|
|
18
26
|
/** Resolve a node's glyph + hue + overlay word, applying the precedence
|
|
19
|
-
* hanging > streaming > status. */
|
|
27
|
+
* hanging > frozen > streaming > status. */
|
|
20
28
|
export declare function resolveNodeVisual(status: NodeStatus, opts: {
|
|
21
29
|
streaming?: boolean;
|
|
30
|
+
frozen?: boolean;
|
|
22
31
|
hanging?: Fault | {
|
|
23
32
|
kind: FaultKind;
|
|
24
33
|
} | null;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// status-glyph.ts — the canonical status glyph and color map used by the canvas
|
|
2
2
|
// ASCII forest and interactive browser. The overlay precedence is
|
|
3
|
-
// hanging > streaming > status. `hanging` and `streaming` are
|
|
3
|
+
// hanging > frozen > streaming > status. `hanging` and `streaming` are
|
|
4
4
|
// mutually exclusive in practice (hanging means the turn ended, so `busy` is
|
|
5
|
-
// gone) but the precedence is defined regardless.
|
|
5
|
+
// gone) but the precedence is defined regardless. `frozen` outranks `streaming`
|
|
6
|
+
// because a frozen row has no process at all: any streaming read against it is
|
|
7
|
+
// a stale marker, and showing it as active is the lie the freeze state exists
|
|
8
|
+
// to stop telling.
|
|
6
9
|
import { FAULT_QUIET_MS } from '../runtime/fault.js';
|
|
7
10
|
// ── Status glyphs (the mono, color-free primary encoding) ─────────────────────
|
|
8
11
|
export const STATUS_GLYPH = {
|
|
@@ -24,6 +27,9 @@ export const STATUS_COLOR = {
|
|
|
24
27
|
// ── Streaming overlay (genuinely mid-turn) — browse's existing statusRail visual.
|
|
25
28
|
const STREAMING_GLYPH = '⟳';
|
|
26
29
|
const STREAMING_COLOR = 92; // bright green (brighter than active-status green)
|
|
30
|
+
// ── Frozen overlay (waiting on a broker slot: marked, but no process). ───────
|
|
31
|
+
const FROZEN_GLYPH = '◌';
|
|
32
|
+
const FROZEN_COLOR = 94; // bright blue
|
|
27
33
|
// ── Active-fault overlay (one glyph, kind label carries the detail). ─────────
|
|
28
34
|
export const HANGING_GLYPH = '⚠';
|
|
29
35
|
export const HANGING_COLOR = 33; // yellow
|
|
@@ -42,12 +48,22 @@ export function hangingLabel(kind) {
|
|
|
42
48
|
default: return 'other';
|
|
43
49
|
}
|
|
44
50
|
}
|
|
51
|
+
/** Whether a row is frozen: the broker ledger marked it waiting for a slot and
|
|
52
|
+
* no process holds it. Both halves matter — a launch clears `frozen_at`, so a
|
|
53
|
+
* mark sitting beside a live pid is stale rather than frozen. A projection
|
|
54
|
+
* that carries no pid at all (the dashboard row) reads on the mark alone. */
|
|
55
|
+
export function isFrozenRow(row) {
|
|
56
|
+
return row.frozen_at != null && row.pi_pid == null;
|
|
57
|
+
}
|
|
45
58
|
/** Resolve a node's glyph + hue + overlay word, applying the precedence
|
|
46
|
-
* hanging > streaming > status. */
|
|
59
|
+
* hanging > frozen > streaming > status. */
|
|
47
60
|
export function resolveNodeVisual(status, opts) {
|
|
48
61
|
if (opts.hanging != null) {
|
|
49
62
|
return { glyph: HANGING_GLYPH, color: HANGING_COLOR, word: hangingLabel(opts.hanging.kind), bold: true };
|
|
50
63
|
}
|
|
64
|
+
if (opts.frozen === true) {
|
|
65
|
+
return { glyph: FROZEN_GLYPH, color: FROZEN_COLOR, word: 'frozen' };
|
|
66
|
+
}
|
|
51
67
|
if (opts.streaming === true) {
|
|
52
68
|
return { glyph: STREAMING_GLYPH, color: STREAMING_COLOR, word: 'live', bold: true };
|
|
53
69
|
}
|
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
* only `dead` is a fault. */
|
|
12
12
|
export type NodeStatus = 'active' | 'idle' | 'done' | 'dead' | 'canceled';
|
|
13
13
|
/** Does stopping finalize the node? terminal = worker (finalizes on push --final);
|
|
14
|
-
* resident = interactable
|
|
15
|
-
*
|
|
14
|
+
* resident = an interactable conversation that parks and remains wakeable
|
|
15
|
+
* without being forced to submit a final. */
|
|
16
16
|
export type Lifecycle = 'terminal' | 'resident';
|
|
17
17
|
/** base = hands-on worker; orchestrator = delegating manager. Bespoke per kind. */
|
|
18
18
|
export type Mode = 'base' | 'orchestrator';
|
|
19
19
|
/** Why a node last stopped — drives the daemon's reap-vs-revive decision. */
|
|
20
20
|
export type ExitIntent = 'done' | 'refresh' | 'idle-release' | 'parked' | null;
|
|
21
|
+
export type TerminalReason = 'finalized' | 'finished' | 'parked' | 'closed' | 'retired' | 'crashed' | 'stranded';
|
|
21
22
|
/** The two structural edges. `subscribes_to` is the load-bearing spine (flow,
|
|
22
23
|
* org chart, views, completion routing). `spawned_by` is audit only. */
|
|
23
24
|
export type EdgeType = 'subscribes_to' | 'spawned_by';
|
|
@@ -48,7 +49,7 @@ export interface LaunchSpec {
|
|
|
48
49
|
env: Record<string, string>;
|
|
49
50
|
}
|
|
50
51
|
export interface ManagedWorktree {
|
|
51
|
-
state: 'open' | 'closed';
|
|
52
|
+
state: 'open' | 'closed' | 'abandoned';
|
|
52
53
|
/** A closed worktree remains registered until its owner has exited, so Git
|
|
53
54
|
* never deletes the caller's live checkout beneath it. */
|
|
54
55
|
cleanup?: 'pending' | 'complete';
|
|
@@ -59,6 +60,12 @@ export interface ManagedWorktree {
|
|
|
59
60
|
base_sha: string;
|
|
60
61
|
created: string;
|
|
61
62
|
closed?: string;
|
|
63
|
+
abandoned?: {
|
|
64
|
+
branch: string;
|
|
65
|
+
tip_sha: string;
|
|
66
|
+
at: string;
|
|
67
|
+
by: string;
|
|
68
|
+
};
|
|
62
69
|
}
|
|
63
70
|
/** Immutable companion provenance. The origin session/leaf/model coordinates
|
|
64
71
|
* remain canonical in the review row rather than being duplicated into node
|
|
@@ -181,6 +188,12 @@ export interface NodeRuntime {
|
|
|
181
188
|
/** Controller whose next inbox delivery permits this terminal node to sleep.
|
|
182
189
|
* Mutable durable runtime state: ancestry never implies a wait. */
|
|
183
190
|
waiting_for?: string | null;
|
|
191
|
+
frozen_at?: string | null;
|
|
192
|
+
respawn_failures?: number;
|
|
193
|
+
respawn_not_before?: string | null;
|
|
194
|
+
launched_at?: string | null;
|
|
195
|
+
last_settled_at?: string | null;
|
|
196
|
+
terminal_reason?: TerminalReason | null;
|
|
184
197
|
/** Immutable basename of the committed canonical final report, or null. */
|
|
185
198
|
final_report?: string | null;
|
|
186
199
|
/** ISO timestamp carried by the committed canonical final report, or null. */
|
|
@@ -244,6 +257,12 @@ export interface NodeRow {
|
|
|
244
257
|
/** Authoritative runtime columns (see NodeRuntime). */
|
|
245
258
|
intent: ExitIntent;
|
|
246
259
|
waiting_for: string | null;
|
|
260
|
+
frozen_at?: string | null;
|
|
261
|
+
respawn_failures?: number;
|
|
262
|
+
respawn_not_before?: string | null;
|
|
263
|
+
launched_at?: string | null;
|
|
264
|
+
last_settled_at?: string | null;
|
|
265
|
+
terminal_reason?: TerminalReason | null;
|
|
247
266
|
final_report?: string | null;
|
|
248
267
|
finalized_at?: string | null;
|
|
249
268
|
pi_pid: number | null;
|
package/dist/core/feed/feed.js
CHANGED
|
@@ -91,7 +91,7 @@ function commitCanonicalFinal(nodeId, staged) {
|
|
|
91
91
|
const subscribers = db.prepare(`SELECT from_id AS node_id, active, created FROM edges WHERE type = 'subscribes_to' AND to_id = ? ORDER BY created`).all(nodeId).map((row) => ({ node_id: row['node_id'], active: row['active'] === 1, created: row['created'] }));
|
|
92
92
|
install(staged);
|
|
93
93
|
installed = true;
|
|
94
|
-
db.prepare("UPDATE nodes SET status = 'done', intent = 'done', final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL").run(staged.basename, staged.ts, nodeId);
|
|
94
|
+
db.prepare("UPDATE nodes SET status = 'done', intent = 'done', terminal_reason = 'finalized', final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL").run(staged.basename, staged.ts, nodeId);
|
|
95
95
|
db.exec('COMMIT');
|
|
96
96
|
return { subscribers };
|
|
97
97
|
}
|
|
@@ -40,7 +40,7 @@ function materializeFeedbackBranch(dir, capture) {
|
|
|
40
40
|
* the first-comment race to a concurrently recorded companion. */
|
|
41
41
|
export function discardFeedbackCompanion(nodeId) {
|
|
42
42
|
try {
|
|
43
|
-
transition(nodeId, 'cancel');
|
|
43
|
+
transition(nodeId, 'cancel', { reason: 'retired' });
|
|
44
44
|
headlessBrokerHost.teardown(nodeId);
|
|
45
45
|
}
|
|
46
46
|
catch { /* the refusal is authoritative; a straggler node is inert */ }
|
|
@@ -88,7 +88,10 @@ export async function realizeFeedbackCompanion(args) {
|
|
|
88
88
|
});
|
|
89
89
|
try {
|
|
90
90
|
initializeForkContextExposure(node.node_id, args.senderNodeId);
|
|
91
|
-
|
|
91
|
+
// The bind deadline below only a live broker can meet, so a capacity
|
|
92
|
+
// refusal must surface here and discard the companion — freezing would
|
|
93
|
+
// burn the whole deadline waiting for a launch that is not coming.
|
|
94
|
+
reviveNode(node.node_id, { resume: false, capacity: 'refuse' });
|
|
92
95
|
if (!await awaitCompanionBind(node.node_id, COMPANION_BIND_DEADLINE_MS)) {
|
|
93
96
|
throw new Error(`feedback companion did not bind within ${COMPANION_BIND_DEADLINE_MS}ms`);
|
|
94
97
|
}
|