@north-light/crouter 0.3.241 → 0.3.242
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/dto/broker-ops.d.ts +8 -3
- package/dist/api/dto/broker.d.ts +4 -2
- package/dist/api/dto/inbox.d.ts +1 -0
- package/dist/api/dto/nodes.d.ts +4 -0
- package/dist/api/dto/review-comments.d.ts +1 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/clients/attach/session/profile-files.js +4 -1
- package/dist/clients/attach/viewer.js +366 -366
- package/dist/commands/node/create.js +8 -3
- package/dist/commands/sys/daemon.js +1 -1
- package/dist/core/__tests__/child-death-wake.test.js +0 -48
- package/dist/core/__tests__/daemon-boot.test.js +0 -8
- package/dist/core/__tests__/dead-node-policy-table.test.js +1 -2
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +14 -2
- package/dist/core/__tests__/lifecycle.test.js +5 -5
- package/dist/core/__tests__/relaunch-root.test.js +91 -1
- package/dist/core/__tests__/revive-capacity.test.js +36 -1
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +73 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +1 -2
- package/dist/core/canvas/canvas.d.ts +6 -5
- package/dist/core/canvas/canvas.js +8 -7
- package/dist/core/canvas/types.d.ts +6 -0
- package/dist/core/review/realize.js +1 -1
- package/dist/core/runtime/broker/node-named.d.ts +1 -0
- package/dist/core/runtime/host.js +114 -103
- package/dist/core/runtime/lifecycle.js +3 -2
- package/dist/core/runtime/naming.d.ts +26 -8
- package/dist/core/runtime/naming.js +75 -43
- package/dist/core/runtime/reset.js +12 -5
- package/dist/core/runtime/revive-all.d.ts +4 -11
- package/dist/core/runtime/revive-all.js +6 -13
- package/dist/core/runtime/warm-pool.js +4 -4
- package/dist/daemon/api/__tests__/node-create-description.test.js +1 -0
- package/dist/daemon/api/handlers/broker-ops.js +14 -7
- package/dist/daemon/api/handlers/feedback-comments.js +1 -1
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/crtrd.js +17 -20
- package/dist/daemon/messaging/node-message.d.ts +1 -0
- package/dist/daemon/messaging/node-message.js +6 -2
- package/dist/daemon/reconcilers/live-obligation.d.ts +1 -2
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +0 -5
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +9 -13
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +0 -6
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +0 -2
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +0 -10
- package/dist/daemon/reconcilers/node-lifecycle/tick.d.ts +2 -2
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +4 -2
- package/dist/daemon/reconcilers/storage-maintenance.js +2 -2
- package/dist/daemon/review/comment-notify.d.ts +1 -0
- package/dist/daemon/review/comment-notify.js +1 -0
- package/dist/pi-extensions/broker-local.d.ts +1 -0
- package/dist/pi-extensions/broker-local.js +12 -7
- package/dist/pi-extensions/canvas-recap.js +1 -0
- package/dist/pi-extensions/canvas-stophook.js +2 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
// different directory than the last one simply misses and cold-spawns — the
|
|
51
51
|
// pool serves the common case of returning to the same working directory.
|
|
52
52
|
//
|
|
53
|
-
// Nothing ever
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
53
|
+
// Nothing ever resumes a spare: the daemon's row query hides the pool, so a
|
|
54
|
+
// spare whose broker dies (every daemon handover tears down every broker of the
|
|
55
|
+
// epoch) is garbage, not a parked node. It is also invisible to the count-based
|
|
56
|
+
// prune for the same reason. So the pool owns
|
|
57
57
|
// its own garbage collection — `reapStaleSpares`, below, run at daemon start
|
|
58
58
|
// and on every supervision tick.
|
|
59
59
|
import { createHash } from 'node:crypto';
|
|
@@ -78,6 +78,7 @@ test('node create stores caller description and automatic naming leaves it and n
|
|
|
78
78
|
const generated = await generatedNameRoute.handler(context('POST', `/v1/nodes/${nodeId}/broker/generated-name`, nodeId, {
|
|
79
79
|
kind: 'initial',
|
|
80
80
|
description: 'Generated replacement',
|
|
81
|
+
title: 'Generated replacement',
|
|
81
82
|
icon: '',
|
|
82
83
|
}));
|
|
83
84
|
assert.deepEqual(generated.body, { applied: false });
|
|
@@ -88,6 +88,7 @@ function extensionNode(node) {
|
|
|
88
88
|
node_id: node.node_id,
|
|
89
89
|
name: node.name,
|
|
90
90
|
...(node.description === undefined ? {} : { description: node.description }),
|
|
91
|
+
...(node.title === undefined ? {} : { title: node.title }),
|
|
91
92
|
...(node.icon === undefined ? {} : { icon: node.icon }),
|
|
92
93
|
kind: node.kind,
|
|
93
94
|
mode: node.mode,
|
|
@@ -153,17 +154,18 @@ function parseGeneratedNameBody(body) {
|
|
|
153
154
|
const value = objectBody(body, 'broker generated-name');
|
|
154
155
|
if (value['kind'] !== 'initial' && value['kind'] !== 'recap')
|
|
155
156
|
throw usage('kind must be initial or recap');
|
|
156
|
-
if (typeof value['description'] !== 'string' || typeof value['icon'] !== 'string') {
|
|
157
|
-
throw usage('description and icon must be strings');
|
|
157
|
+
if (typeof value['description'] !== 'string' || typeof value['title'] !== 'string' || typeof value['icon'] !== 'string') {
|
|
158
|
+
throw usage('description, title, and icon must be strings');
|
|
159
|
+
}
|
|
160
|
+
if (value['kind'] === 'initial') {
|
|
161
|
+
return { kind: 'initial', description: value['description'], title: value['title'], icon: value['icon'] };
|
|
158
162
|
}
|
|
159
|
-
if (value['kind'] === 'initial')
|
|
160
|
-
return { kind: 'initial', description: value['description'], icon: value['icon'] };
|
|
161
163
|
const expected = value['expected'];
|
|
162
164
|
if (typeof expected !== 'object' || expected === null || Array.isArray(expected)
|
|
163
|
-
|| ['name', 'description', 'icon', 'kind'].some((key) => typeof expected[key] !== 'string')) {
|
|
164
|
-
throw usage('recap expected must contain name, description, icon, and kind strings');
|
|
165
|
+
|| ['name', 'description', 'title', 'icon', 'kind'].some((key) => typeof expected[key] !== 'string')) {
|
|
166
|
+
throw usage('recap expected must contain name, description, title, icon, and kind strings');
|
|
165
167
|
}
|
|
166
|
-
return { kind: 'recap', description: value['description'], icon: value['icon'], expected: expected };
|
|
168
|
+
return { kind: 'recap', description: value['description'], title: value['title'], icon: value['icon'], expected: expected };
|
|
167
169
|
}
|
|
168
170
|
function parsePersonaAckBody(body) {
|
|
169
171
|
const value = objectBody(body, 'broker persona-ack');
|
|
@@ -371,27 +373,32 @@ function handleGeneratedName(ctx) {
|
|
|
371
373
|
return { applied: false };
|
|
372
374
|
const updated = updateNode(nodeId, {
|
|
373
375
|
description: request.description,
|
|
376
|
+
...(request.title.trim() === '' ? {} : { title: request.title }),
|
|
374
377
|
...(request.icon.trim() === '' ? {} : { icon: request.icon }),
|
|
375
378
|
});
|
|
376
379
|
return {
|
|
377
380
|
applied: true,
|
|
378
381
|
editorLabel: editorLabel(updated),
|
|
379
382
|
description: updated.description ?? '',
|
|
383
|
+
title: updated.title ?? '',
|
|
380
384
|
icon: updated.icon ?? '',
|
|
381
385
|
};
|
|
382
386
|
}
|
|
383
387
|
const expected = request.expected;
|
|
384
388
|
if (node.name !== expected.name || (node.description ?? '') !== expected.description
|
|
389
|
+
|| (node.title ?? '') !== expected.title
|
|
385
390
|
|| (node.icon ?? '') !== expected.icon || node.kind !== expected.kind)
|
|
386
391
|
return { applied: false };
|
|
387
392
|
const updated = updateNode(nodeId, {
|
|
388
393
|
description: request.description,
|
|
394
|
+
title: request.title.trim() === '' ? undefined : request.title,
|
|
389
395
|
icon: request.icon.trim() === '' ? undefined : request.icon,
|
|
390
396
|
});
|
|
391
397
|
return {
|
|
392
398
|
applied: true,
|
|
393
399
|
editorLabel: editorLabel(updated),
|
|
394
400
|
description: updated.description ?? '',
|
|
401
|
+
title: updated.title ?? '',
|
|
395
402
|
icon: updated.icon ?? '',
|
|
396
403
|
};
|
|
397
404
|
});
|
|
@@ -67,7 +67,7 @@ async function deliverCommentTurn(args) {
|
|
|
67
67
|
mode: 'interactive',
|
|
68
68
|
data: { page_id: basename(args.dir), comment_id: args.mutation.comment.id, verb: 'create' },
|
|
69
69
|
});
|
|
70
|
-
return { target_node_id: target, status: 'delivered', via: delivery.via, woke: delivery.revived };
|
|
70
|
+
return { target_node_id: target, status: 'delivered', via: delivery.via, woke: delivery.revived, ...(delivery.reason === undefined ? {} : { reason: delivery.reason }) };
|
|
71
71
|
}
|
|
72
72
|
catch (error) {
|
|
73
73
|
// State was committed before delivery; a delivery failure remains a
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -124,6 +124,8 @@ export function toNodeDetailDTO(meta, edges) {
|
|
|
124
124
|
};
|
|
125
125
|
if (meta.description !== undefined)
|
|
126
126
|
dto.description = meta.description;
|
|
127
|
+
if (meta.title !== undefined)
|
|
128
|
+
dto.title = meta.title;
|
|
127
129
|
if (meta.icon !== undefined)
|
|
128
130
|
dto.icon = meta.icon;
|
|
129
131
|
if (meta.cycles !== undefined)
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
// clock. A broker's real `exit` event enqueues an exit-policy job on the
|
|
10
10
|
// daemon's serial work lane (never inline on the Node event callback), where
|
|
11
11
|
// classifyDeadNode — one pure 8-row policy table over durable row state —
|
|
12
|
-
// decides forget / dormant / respawn / boot-failure. The
|
|
13
|
-
//
|
|
14
|
-
// recovery and startup recovery can never diverge. Every broker's argv is
|
|
12
|
+
// decides forget / dormant / respawn / boot-failure. The row-driven tick
|
|
13
|
+
// classifies disconnected rows through the same table. Every broker's argv is
|
|
15
14
|
// stamped `--epoch <id>`: at boot the census reaps every prior-epoch broker
|
|
16
15
|
// (no adoption), and at shutdown the daemon tears its whole fleet down, so no
|
|
17
16
|
// broker ever outlives the daemon epoch that launched it. A tmux pane, when
|
|
@@ -94,8 +93,8 @@ export function brokerCommandDeclaresCanvasHome(command, home) {
|
|
|
94
93
|
// readiness mirror, reap every broker of a PRIOR epoch on its own canvas.
|
|
95
94
|
// Ownership is never adopted across epochs — a prior daemon's ChildProcess
|
|
96
95
|
// handles died with it, so its brokers are unsupervisable; tear them down and
|
|
97
|
-
// let the
|
|
98
|
-
//
|
|
96
|
+
// let the next supervision tick classify their rows through the policy table.
|
|
97
|
+
// A broker whose argv carries NO `--epoch` tag is a legacy-generation
|
|
99
98
|
// broker and gets the same verdict (reap): it predates epoch ownership.
|
|
100
99
|
/** The `--epoch <id>` tag host.ts stamps into every broker's argv, or null
|
|
101
100
|
* for a legacy/untagged broker (which the census treats as prior-epoch). */
|
|
@@ -110,9 +109,9 @@ const CENSUS_POLL_MS = 150;
|
|
|
110
109
|
async function reapPriorEpochBrokers(epoch) {
|
|
111
110
|
const snapshot = captureLivenessSnapshot();
|
|
112
111
|
if (snapshot === null) {
|
|
113
|
-
// No process table — the census cannot run. The
|
|
114
|
-
//
|
|
115
|
-
//
|
|
112
|
+
// No process table — the census cannot run. The supervision tick still
|
|
113
|
+
// classifies rows; a surviving prior-epoch broker is at worst a duplicate
|
|
114
|
+
// engine risk the next daemon boot's census retries.
|
|
116
115
|
emitEvent({
|
|
117
116
|
level: 'error',
|
|
118
117
|
event: 'daemon.census.snapshot_failed',
|
|
@@ -184,10 +183,9 @@ async function reapPriorEpochBrokers(epoch) {
|
|
|
184
183
|
// Daemon-shutdown fleet teardown (invariant 2: no broker outlives the daemon
|
|
185
184
|
// epoch that launched it). SIGTERM every live entry, race each handle's real
|
|
186
185
|
// exit against ONE shared bounded grace, then SIGKILL each survivor's captured
|
|
187
|
-
// process tree. Rows are deliberately NOT rewritten here — the
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
// recovery path.
|
|
186
|
+
// process tree. Rows are deliberately NOT rewritten here — the next daemon's
|
|
187
|
+
// supervision tick classifies them through the policy table, so clean shutdown
|
|
188
|
+
// and crash converge on one recovery path.
|
|
191
189
|
const FLEET_TEARDOWN_GRACE_MS = 5_000;
|
|
192
190
|
async function teardownFleet(fleet) {
|
|
193
191
|
const entries = Array.from(fleet.entries());
|
|
@@ -292,7 +290,7 @@ export async function superviseTick(now = Date.now(), lifecycle = directTickLife
|
|
|
292
290
|
// Human bridge rows have no broker engine and participate in none of the
|
|
293
291
|
// recurring row-driven reconciler passes.
|
|
294
292
|
rows = listNodes({ status: ['active', 'idle'] }).filter((row) => row.kind !== 'human');
|
|
295
|
-
terminating = listNodes({ status: ['done', 'canceled', 'dead'],
|
|
293
|
+
terminating = listNodes({ status: ['done', 'canceled', 'dead'], withRecordedPidOrFrozen: true })
|
|
296
294
|
.filter((row) => row.kind !== 'human');
|
|
297
295
|
}
|
|
298
296
|
catch (err) {
|
|
@@ -427,7 +425,7 @@ export async function runDaemon(opts = {}) {
|
|
|
427
425
|
// The grace covers: response flush → the CLI process printing and exiting →
|
|
428
426
|
// pi persisting the tool result. It is NOT a correctness fallback; a broker
|
|
429
427
|
// SIGTERMed mid-turn still gets `abortAndExit`'s own bounded persistence
|
|
430
|
-
// window, and the successor's
|
|
428
|
+
// window, and the successor's supervision tick resolves it either way.
|
|
431
429
|
const HANDOVER_ACK_GRACE_MS = 1_000;
|
|
432
430
|
let handoverArmed = false;
|
|
433
431
|
/** Arm the handover. Idempotent: a retry while one is already scheduled
|
|
@@ -484,7 +482,7 @@ export async function runDaemon(opts = {}) {
|
|
|
484
482
|
catch (error) {
|
|
485
483
|
// The canvas is now daemonless. Loud, and recoverable: the next front-door
|
|
486
484
|
// command's ensureDaemon() starts one, and every torn-down node is
|
|
487
|
-
// reclassified by that daemon's
|
|
485
|
+
// reclassified by that daemon's supervision tick.
|
|
488
486
|
operationIdContext.fresh(() => {
|
|
489
487
|
emitEvent({ level: 'error', event: 'daemon.handover.successor_failed', error });
|
|
490
488
|
});
|
|
@@ -530,9 +528,8 @@ export async function runDaemon(opts = {}) {
|
|
|
530
528
|
void requestCleanup().catch(() => { });
|
|
531
529
|
}
|
|
532
530
|
/** The serial work lane (design D-3): supervision ticks, exit-policy jobs,
|
|
533
|
-
* matured backoff respawns
|
|
534
|
-
*
|
|
535
|
-
* row reads. `laneTail` never rejects — every append attaches the rejection
|
|
531
|
+
* and matured backoff respawns run strictly one at a time, so policy never
|
|
532
|
+
* interleaves with a tick's row reads. `laneTail` never rejects — every append attaches the rejection
|
|
536
533
|
* handler. Once admission closes, new jobs are DROPPED (not queued): a
|
|
537
534
|
* shutdown SIGTERM fans exit events across the whole fleet, and running
|
|
538
535
|
* their policy jobs would respawn brokers mid-teardown. */
|
|
@@ -574,8 +571,8 @@ export async function runDaemon(opts = {}) {
|
|
|
574
571
|
bindDaemonEventSource();
|
|
575
572
|
interval = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
576
573
|
// Mint this daemon's epoch and bind the ONE fleet registry for the process
|
|
577
|
-
// BEFORE anything can launch a broker (the API server
|
|
578
|
-
//
|
|
574
|
+
// BEFORE anything can launch a broker (the API server or any tick): every
|
|
575
|
+
// launch funnels through headlessBrokerHost.launch, which
|
|
579
576
|
// registers with the bound fleet — an unbound launch throws (design D-2).
|
|
580
577
|
const epoch = randomBytes(8).toString('hex');
|
|
581
578
|
fleet = new DaemonFleet({ epoch, enqueue });
|
|
@@ -58,6 +58,7 @@ export async function deliverNodeMessage(args) {
|
|
|
58
58
|
data: { ...args.data, body: args.body },
|
|
59
59
|
}));
|
|
60
60
|
let revived = false;
|
|
61
|
+
let reason;
|
|
61
62
|
if (args.mode !== 'quiet') {
|
|
62
63
|
target = getNode(args.node_id);
|
|
63
64
|
if (target === null)
|
|
@@ -66,12 +67,15 @@ export async function deliverNodeMessage(args) {
|
|
|
66
67
|
try {
|
|
67
68
|
// Mail is durable, so a wake denied for capacity waits for a slot
|
|
68
69
|
// rather than being lost: the row freezes and the tick relaunches it.
|
|
69
|
-
|
|
70
|
+
const result = reviveNode(args.node_id, { resume: true, capacity: 'freeze' });
|
|
71
|
+
revived = result.launch !== undefined;
|
|
72
|
+
if (result.outcome === 'frozen')
|
|
73
|
+
reason = 'capacity_frozen';
|
|
70
74
|
}
|
|
71
75
|
catch {
|
|
72
76
|
// The durable entry remains authoritative; one wake attempt is enough.
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
|
-
return { entry_id: entry.entry_id, via: 'inbox', revived };
|
|
80
|
+
return { entry_id: entry.entry_id, via: 'inbox', revived, ...(reason === undefined ? {} : { reason }) };
|
|
77
81
|
}
|
|
@@ -12,8 +12,7 @@ export declare function hasLiveObligation(nodeId: string): boolean;
|
|
|
12
12
|
* cycle rather than causing one. */
|
|
13
13
|
export declare function hasPendingWake(nodeId: string): boolean;
|
|
14
14
|
/** What parking an unattended node actually amounts to — the one rule both the
|
|
15
|
-
* live-broker park clock
|
|
16
|
-
* reconciliation (dormant-inbox) apply.
|
|
15
|
+
* live-broker park clock and the lifecycle tick apply.
|
|
17
16
|
*
|
|
18
17
|
* `release` — something can still wake it, so parking is a pause.
|
|
19
18
|
* `park` — a resident with nothing left to wake it has completed its
|
|
@@ -43,11 +43,6 @@ export declare class FreezeLaneReconciler {
|
|
|
43
43
|
endEpisode(nodeId: string): void;
|
|
44
44
|
run(now: number, ctx: FreezeLaneContext): Promise<FrozenCounts>;
|
|
45
45
|
private enact;
|
|
46
|
-
/** A frozen row is owed exactly one thing: the launch the cap refused. Strict
|
|
47
|
-
* resume is the request, and `reviveNode` itself vetoes it for a pending
|
|
48
|
-
* refresh or an unconfirmed cycle — the same modes the dead-row table would
|
|
49
|
-
* have chosen. It is recovery, not a wake: nothing arrived for this node
|
|
50
|
-
* while it waited, so an armed deadline survives the thaw. */
|
|
51
46
|
private enactThaw;
|
|
52
47
|
private enactIdleRelease;
|
|
53
48
|
}
|
|
@@ -8,11 +8,7 @@
|
|
|
8
8
|
// needs to pre-reserve anything. A candidate that finds the ledger full freezes
|
|
9
9
|
// (`reviveNode` with `capacity: 'freeze'` is the single freeze writer) and waits
|
|
10
10
|
// for the next tick.
|
|
11
|
-
|
|
12
|
-
// The idle-release episode rules below are the former DormantInboxReconciler's,
|
|
13
|
-
// moved here whole: a relaunch that only ever tried to deliver mail is a launch
|
|
14
|
-
// like any other, and it now competes for slots through the same lane.
|
|
15
|
-
import { getRow, subscribersOf, } from '../../../core/canvas/index.js';
|
|
11
|
+
import { clearFrozen, getNode, getRow, subscribersOf, } from '../../../core/canvas/index.js';
|
|
16
12
|
import { fullName } from '../../../core/canvas/labels.js';
|
|
17
13
|
import { isSafeNodeId } from '../../../core/canvas/paths.js';
|
|
18
14
|
import { emitEvent } from '../../../core/events/emit.js';
|
|
@@ -87,7 +83,7 @@ export class FreezeLaneReconciler {
|
|
|
87
83
|
const id = entry.row.node_id;
|
|
88
84
|
try {
|
|
89
85
|
if (entry.enact === 'thaw')
|
|
90
|
-
return this.enactThaw(
|
|
86
|
+
return this.enactThaw(entry.row);
|
|
91
87
|
if (entry.enact === 'idle-release')
|
|
92
88
|
return this.enactIdleRelease(now, entry.row);
|
|
93
89
|
// The SAME policy table an exit event runs, so tick recovery and exit
|
|
@@ -110,13 +106,13 @@ export class FreezeLaneReconciler {
|
|
|
110
106
|
return 'skipped';
|
|
111
107
|
}
|
|
112
108
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return reviveNode(
|
|
109
|
+
enactThaw(row) {
|
|
110
|
+
if (row.lifecycle === 'resident' && getNode(row.node_id)?.pi_session_id != null && !hasPendingWake(row.node_id)) {
|
|
111
|
+
transition(row.node_id, 'release');
|
|
112
|
+
clearFrozen(row.node_id);
|
|
113
|
+
return 'skipped';
|
|
114
|
+
}
|
|
115
|
+
return reviveNode(row.node_id, { resume: true, capacity: 'freeze', recovery: true }).outcome === 'frozen'
|
|
120
116
|
? 'frozen'
|
|
121
117
|
: 'launched';
|
|
122
118
|
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// respawn-policy.ts — the tick's Backoff gate, and P5's file to own outright.
|
|
2
|
-
//
|
|
3
|
-
// P3 only READS `respawn_not_before`: nothing writes it yet, so the gate is
|
|
4
|
-
// inert in effect. P5 replaces the in-memory throttle in `daemon/fleet.ts` with
|
|
5
|
-
// the durable counter writes (`respawn_failures` + `respawn_not_before`) that
|
|
6
|
-
// make it bite, and adds them here rather than in `tick.ts`.
|
|
7
1
|
/** Whether a durable backoff still holds this row back from a relaunch. A row
|
|
8
2
|
* in backoff holds no slot and is excluded from the saturation figure — it is
|
|
9
3
|
* waiting on a clock, not on capacity. */
|
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
import type { NodeRow } from '../../../core/canvas/index.js';
|
|
2
|
-
/** Handle a terminal row that still holds a process. Deliberately inert until
|
|
3
|
-
* P4: a row reaching here is already excluded from every other tick duty. */
|
|
4
2
|
export declare function handleTerminatingRow(row: NodeRow): void;
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
// terminating.ts — the Terminating state's owner, and P4's file to own outright.
|
|
2
|
-
//
|
|
3
|
-
// A terminal row that still holds a process is a node whose teardown has been
|
|
4
|
-
// requested but not finished. P4 gives it one bounded shutdown request and an
|
|
5
|
-
// escalation after its grace, moving the inline `headlessBrokerHost.teardown`
|
|
6
|
-
// calls at the close/reset sites into this daemon-side lane. P3 changes no
|
|
7
|
-
// teardown behavior: it only OBSERVES such rows (the tick queries terminal rows
|
|
8
|
-
// carrying a recorded pid) and hands them here, so P4 edits this file alone.
|
|
9
|
-
/** Handle a terminal row that still holds a process. Deliberately inert until
|
|
10
|
-
* P4: a row reaching here is already excluded from every other tick duty. */
|
|
11
1
|
export function handleTerminatingRow(row) {
|
|
12
2
|
void row;
|
|
13
3
|
}
|
|
@@ -2,8 +2,8 @@ import { type NodeRow } from '../../../core/canvas/index.js';
|
|
|
2
2
|
import type { FleetRegistry } from '../../../core/runtime/fleet.js';
|
|
3
3
|
import { type FrozenCounts } from './freeze-lane.js';
|
|
4
4
|
export interface NodeLifecycleContext {
|
|
5
|
-
/** Every active and idle row (human rows excluded), plus terminal rows
|
|
6
|
-
*
|
|
5
|
+
/** Every active and idle row (human rows excluded), plus terminal rows with
|
|
6
|
+
* a recorded pid or frozen mark. */
|
|
7
7
|
rows: readonly NodeRow[];
|
|
8
8
|
fleet: FleetRegistry;
|
|
9
9
|
}
|
|
@@ -64,9 +64,11 @@ export class NodeLifecycleReconciler {
|
|
|
64
64
|
this.thawMark(row);
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
// 2. A terminal row still holding a process is Terminating, and P4's.
|
|
68
67
|
if (isTerminal(row)) {
|
|
69
|
-
|
|
68
|
+
if (row.pi_pid != null)
|
|
69
|
+
handleTerminatingRow(row);
|
|
70
|
+
else if (row.frozen_at != null)
|
|
71
|
+
candidates.push({ row, enact: 'thaw' });
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
// A paused profile launches nothing, and its stuck-delivery episode is over
|
|
@@ -19,8 +19,8 @@ export class StorageMaintenanceReconciler {
|
|
|
19
19
|
run(now, ctx) {
|
|
20
20
|
void ctx; // no consumer today — kept only for six-way call-site uniformity
|
|
21
21
|
// Placement reap MUST precede focus GC, so GC catches a focus row the reap
|
|
22
|
-
// strands. Both run after
|
|
23
|
-
//
|
|
22
|
+
// strands. Both run after the lifecycle tick, so a same-tick revive has
|
|
23
|
+
// already had the chance to re-anchor its focus.
|
|
24
24
|
this.reapDeadResidue(now);
|
|
25
25
|
this.gcStaleFocuses();
|
|
26
26
|
this.reapGhostRows(now);
|
|
@@ -7,6 +7,7 @@ export interface CommentDelivery {
|
|
|
7
7
|
status: 'delivered' | 'skipped' | 'failed';
|
|
8
8
|
via?: 'engine' | 'inbox';
|
|
9
9
|
woke: boolean;
|
|
10
|
+
reason?: 'capacity_frozen';
|
|
10
11
|
error?: string;
|
|
11
12
|
}
|
|
12
13
|
/** Render the one bounded, durable comment notice delivered to a review companion. */
|
|
@@ -96,6 +96,7 @@ export async function notifyComment(args) {
|
|
|
96
96
|
status: 'delivered',
|
|
97
97
|
via: delivery.via,
|
|
98
98
|
woke: delivery.revived,
|
|
99
|
+
...(delivery.reason === undefined ? {} : { reason: delivery.reason }),
|
|
99
100
|
};
|
|
100
101
|
appendDeliveryEvent({
|
|
101
102
|
review_id: args.review.review_id,
|
|
@@ -13,6 +13,7 @@ export declare function generateAndCommitInitialName(nodeId: string, prompt: str
|
|
|
13
13
|
export declare function generateAndCommitRecapName(nodeId: string, conversation: string, expected: {
|
|
14
14
|
name: string;
|
|
15
15
|
description: string;
|
|
16
|
+
title: string;
|
|
16
17
|
icon: string;
|
|
17
18
|
kind: string;
|
|
18
19
|
}, isStillAutomatic: () => boolean, onNamed?: (editorLabel: string) => void): void;
|
|
@@ -10,7 +10,7 @@ import { join } from 'node:path';
|
|
|
10
10
|
import { CRTR_DIR_NAME } from '../types.js';
|
|
11
11
|
import { brokerExtensionState, commitBrokerGeneratedName, } from '../core/runtime/broker/daemon-ops.js';
|
|
12
12
|
import { publishNodeNamed } from '../core/runtime/broker/node-named.js';
|
|
13
|
-
import { headlessName, headlessNameFromConversation, sanitizeSessionName, slugFromPrompt, } from '../core/runtime/naming.js';
|
|
13
|
+
import { headlessName, headlessNameFromConversation, sanitizeSessionName, sanitizeSessionTitle, slugFromPrompt, titleFromPrompt, } from '../core/runtime/naming.js';
|
|
14
14
|
function contextDir(nodeId) {
|
|
15
15
|
if (nodeId === '' || nodeId === '.' || nodeId === '..' || /[\\/\0]/u.test(nodeId)) {
|
|
16
16
|
throw new TypeError('node_id must be a non-empty path segment');
|
|
@@ -37,8 +37,14 @@ export function readRoadmap(nodeId) {
|
|
|
37
37
|
const path = join(contextDir(nodeId), 'roadmap.md');
|
|
38
38
|
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
/** The label fields to commit: what the namer produced, or what the source text
|
|
41
|
+
* itself yields when it produced nothing usable. */
|
|
42
|
+
function generatedLabel(source, generated) {
|
|
43
|
+
return {
|
|
44
|
+
description: sanitizeSessionName(generated.name) || slugFromPrompt(source),
|
|
45
|
+
title: sanitizeSessionTitle(generated.title) || titleFromPrompt(source),
|
|
46
|
+
icon: generated.icon,
|
|
47
|
+
};
|
|
42
48
|
}
|
|
43
49
|
/** Generate and commit the first name for a node. The initial operation is
|
|
44
50
|
* guarded by crtrd, so a concurrent rename or another broker cannot be
|
|
@@ -53,8 +59,7 @@ export function generateAndCommitInitialName(nodeId, prompt, onNamed) {
|
|
|
53
59
|
return undefined;
|
|
54
60
|
return headlessName(body).then((generated) => commitBrokerGeneratedName(nodeId, {
|
|
55
61
|
kind: 'initial',
|
|
56
|
-
|
|
57
|
-
icon: generated.icon,
|
|
62
|
+
...generatedLabel(body, generated),
|
|
58
63
|
}));
|
|
59
64
|
})
|
|
60
65
|
.then((result) => {
|
|
@@ -68,6 +73,7 @@ export function generateAndCommitInitialName(nodeId, prompt, onNamed) {
|
|
|
68
73
|
// has nothing left to re-read on.
|
|
69
74
|
publishNodeNamed({
|
|
70
75
|
description: result.description ?? '',
|
|
76
|
+
title: result.title ?? '',
|
|
71
77
|
icon: result.icon ?? '',
|
|
72
78
|
editorLabel: result.editorLabel,
|
|
73
79
|
});
|
|
@@ -88,8 +94,7 @@ export function generateAndCommitRecapName(nodeId, conversation, expected, isSti
|
|
|
88
94
|
return undefined;
|
|
89
95
|
return commitBrokerGeneratedName(nodeId, {
|
|
90
96
|
kind: 'recap',
|
|
91
|
-
|
|
92
|
-
icon: generated.icon,
|
|
97
|
+
...generatedLabel(body, generated),
|
|
93
98
|
expected,
|
|
94
99
|
});
|
|
95
100
|
})
|
|
@@ -267,6 +267,7 @@ export function registerCanvasRecap(pi, recapGenerator = generateRecap, nameRege
|
|
|
267
267
|
generateAndCommitRecapName(nodeId, conversation, {
|
|
268
268
|
name: node.name,
|
|
269
269
|
description: node.description ?? '',
|
|
270
|
+
title: node.title ?? '',
|
|
270
271
|
icon: node.icon ?? '',
|
|
271
272
|
kind: node.kind,
|
|
272
273
|
}, () => pi.getSessionName?.() === original, (label) => {
|
|
@@ -360,6 +360,8 @@ export function registerCanvasStophook(pi) {
|
|
|
360
360
|
// back into its calling broker. Do not await the operation that tears
|
|
361
361
|
// this broker down from Pi's session-start dispatch.
|
|
362
362
|
void relaunchBrokerRoot(nodeId).catch((error) => {
|
|
363
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
364
|
+
ctx?.ui?.notify?.(`/new refused: ${message}`, 'error');
|
|
363
365
|
emitEvent({
|
|
364
366
|
event: 'broker.daemon.operation.failed',
|
|
365
367
|
level: 'error',
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.242",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.242",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|