@north-light/crouter 0.3.262 → 0.3.269
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 +5 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/health.d.ts +2 -0
- package/dist/api/dto/nodes.d.ts +6 -2
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
- package/dist/builtin-memory/internal/memory-loading.md +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +10 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +12 -5
- package/dist/clients/attach/input/controller.d.ts +4 -0
- package/dist/clients/attach/input/controller.js +8 -0
- package/dist/clients/attach/session/bindings.d.ts +3 -0
- package/dist/clients/attach/session/bindings.js +7 -0
- package/dist/clients/attach/session/keys.d.ts +2 -0
- package/dist/clients/attach/session/keys.js +3 -3
- package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
- package/dist/clients/attach/session/whip-achievements.js +145 -0
- package/dist/clients/attach/slash/dispatch.js +5 -3
- package/dist/clients/attach/viewer.js +668 -667
- package/dist/commands/canvas-history/grep.js +3 -22
- package/dist/commands/canvas-history/read.js +5 -3
- package/dist/commands/canvas-history/search.js +3 -22
- package/dist/commands/canvas-history/shared.d.ts +8 -0
- package/dist/commands/canvas-history/shared.js +64 -1
- package/dist/commands/canvas-history/stats.d.ts +1 -0
- package/dist/commands/canvas-history/stats.js +68 -0
- package/dist/commands/canvas-history.js +4 -3
- package/dist/commands/memory/read.js +15 -38
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/context/admin/docs-panel.d.ts +3 -3
- package/dist/commands/sys/context/admin/model.d.ts +7 -2
- package/dist/commands/sys/context/admin/model.js +7 -1
- package/dist/commands/sys/context/admin/read-view.d.ts +5 -5
- package/dist/commands/sys/context/admin/read-view.js +9 -4
- package/dist/commands/sys/context/resolve.d.ts +32 -3
- package/dist/commands/sys/context/resolve.js +74 -25
- package/dist/commands/sys/panels/models-panel.d.ts +28 -5
- package/dist/commands/sys/panels/models-panel.js +560 -75
- package/dist/core/__tests__/integration/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/integration/revive.test.js +4 -2
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/model-routes-config.test.d.ts +1 -0
- package/dist/core/__tests__/model-routes-config.test.js +142 -0
- package/dist/core/__tests__/on-read-nested-store.test.js +8 -0
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +264 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +21 -6
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
- package/dist/core/broker-client/transport-relay.js +5 -1
- package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +13 -10
- package/dist/core/canvas/browse/model.js +16 -1
- package/dist/core/canvas/browse/render.d.ts +23 -3
- package/dist/core/canvas/browse/render.js +429 -64
- package/dist/core/canvas/canvas.js +12 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.d.ts +36 -1
- package/dist/core/canvas/history.js +328 -7
- package/dist/core/canvas/labels.d.ts +2 -2
- package/dist/core/canvas/migrations.js +32 -0
- package/dist/core/canvas/node-recap.d.ts +23 -0
- package/dist/core/canvas/node-recap.js +67 -0
- package/dist/core/canvas/render-source.d.ts +34 -2
- package/dist/core/canvas/render-source.js +83 -24
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/config.d.ts +19 -2
- package/dist/core/config.js +174 -51
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +11 -8
- package/dist/core/model-routes.js +11 -4
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.js +7 -0
- package/dist/core/runtime/broker/inbox.d.ts +12 -2
- package/dist/core/runtime/broker/inbox.js +68 -38
- package/dist/core/runtime/broker-protocol.d.ts +6 -0
- package/dist/core/runtime/broker.js +14 -0
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/interactive-deliver.d.ts +8 -2
- package/dist/core/runtime/interactive-deliver.js +12 -3
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/launch.d.ts +12 -6
- package/dist/core/runtime/launch.js +19 -17
- package/dist/core/runtime/model-swap.d.ts +5 -4
- package/dist/core/runtime/model-swap.js +7 -5
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive.js +22 -0
- package/dist/core/runtime/spawn.d.ts +3 -3
- package/dist/core/runtime/spawn.js +20 -3
- package/dist/core/runtime/stamp/channel.d.ts +18 -0
- package/dist/core/runtime/stamp/channel.js +74 -0
- package/dist/core/runtime/stamp/protocol.d.ts +86 -0
- package/dist/core/runtime/stamp/protocol.js +113 -0
- package/dist/core/runtime/tmux-driver.d.ts +11 -1
- package/dist/core/runtime/tmux-driver.js +27 -11
- package/dist/core/self-update.js +2 -5
- package/dist/core/substrate/listings.d.ts +1 -1
- package/dist/core/substrate/listings.js +6 -2
- package/dist/core/substrate/on-read.d.ts +61 -5
- package/dist/core/substrate/on-read.js +129 -22
- package/dist/core/substrate/render.d.ts +3 -2
- package/dist/core/substrate/render.js +133 -38
- package/dist/core/tui/draw.d.ts +1 -0
- package/dist/core/tui/draw.js +5 -1
- package/dist/core/tui/markdown.d.ts +17 -0
- package/dist/core/tui/markdown.js +407 -0
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
- package/dist/daemon/api/__tests__/seam/api-server.test.js +34 -0
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/canvas.js +80 -18
- package/dist/daemon/api/handlers/health.js +17 -12
- package/dist/daemon/api/handlers/messages.js +12 -1
- package/dist/daemon/api/handlers/nodes.js +10 -3
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/messaging/node-message.js +15 -1
- package/dist/daemon/reconcilers/broker-supervision.js +26 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
- package/dist/daemon/reconcilers/live-obligation.js +17 -0
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +2 -2
- package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
- package/dist/pi-extensions/canvas-recap.js +4 -0
- package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
- package/dist/pi-extensions/canvas-stamp.js +77 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -0
- package/dist/shared/__tests__/env-boundary.test.js +4 -2
- package/dist/shared/crtr-version.d.ts +2 -0
- package/dist/shared/crtr-version.js +18 -0
- package/dist/shared/env.d.ts +10 -0
- package/dist/shared/env.js +19 -4
- package/dist/types.d.ts +14 -2
- package/dist/types.js +4 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// the sanctioned place for it.
|
|
12
12
|
import { existsSync, readdirSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
|
-
import {
|
|
14
|
+
import { listNodes, pruneNodes, pruneToLimit, rebuildIndex, rosterEdges, rosterNodes } from '../../../core/canvas/canvas.js';
|
|
15
15
|
import { activeBackgroundBashJobs } from '../../../core/bash-jobs.js';
|
|
16
16
|
import { contextDir } from '../../../core/canvas/paths.js';
|
|
17
17
|
import { nowIso } from '../../../core/fs-utils.js';
|
|
@@ -20,7 +20,7 @@ import { dashboardRowsAllFromSource, enrichRowsFromSource } from '../../../core/
|
|
|
20
20
|
import { localCanvasSource } from '../../../core/canvas/source.js';
|
|
21
21
|
import { listFocuses } from '../../../core/canvas/focuses.js';
|
|
22
22
|
import { ticketCountsForNodes, ticketsAcrossCanvas } from '../../../core/canvas/attention.js';
|
|
23
|
-
import { buildCorpus, resolveRef, } from '../../../core/canvas/history.js';
|
|
23
|
+
import { buildCorpus, groupCorpus, resolveRef, } from '../../../core/canvas/history.js';
|
|
24
24
|
import { nodeDir, nodesRoot } from '../../../core/canvas/paths.js';
|
|
25
25
|
import { reapEmptyNodes } from '../../../core/runtime/placement.js';
|
|
26
26
|
import { general, notFound, usage } from '../../../core/errors.js';
|
|
@@ -130,9 +130,36 @@ function buildCorpusFromQuery(q) {
|
|
|
130
130
|
statuses: q.statuses,
|
|
131
131
|
sinceMs: q.since_ms,
|
|
132
132
|
untilMs: q.until_ms,
|
|
133
|
+
origins: q.origins,
|
|
134
|
+
roles: q.roles,
|
|
135
|
+
from: q.from,
|
|
133
136
|
};
|
|
134
137
|
return buildCorpus(scope, corpusFilter);
|
|
135
138
|
}
|
|
139
|
+
function parseStatsBody(body) {
|
|
140
|
+
if (body === undefined || body === null)
|
|
141
|
+
return {};
|
|
142
|
+
if (typeof body !== 'object')
|
|
143
|
+
throw usage('history stats body must be a JSON object');
|
|
144
|
+
return body;
|
|
145
|
+
}
|
|
146
|
+
const GROUP_KEYS = ['origin', 'node', 'kind', 'day', 'type', 'role'];
|
|
147
|
+
function handleHistoryStats(ctx) {
|
|
148
|
+
const q = parseStatsBody(ctx.body);
|
|
149
|
+
const by = (q.group_by ?? 'type');
|
|
150
|
+
if (!GROUP_KEYS.includes(by)) {
|
|
151
|
+
throw usage(`--group-by must be one of: ${GROUP_KEYS.join(', ')}; received: ${String(q.group_by)}`);
|
|
152
|
+
}
|
|
153
|
+
const arts = buildCorpusFromQuery(q);
|
|
154
|
+
const groups = groupCorpus(arts, by);
|
|
155
|
+
const body = {
|
|
156
|
+
group_by: by,
|
|
157
|
+
groups,
|
|
158
|
+
total: groups.reduce((sum, g) => sum + g.count, 0),
|
|
159
|
+
scanned: arts.length,
|
|
160
|
+
};
|
|
161
|
+
return { status: 200, body };
|
|
162
|
+
}
|
|
136
163
|
function handleHistoryGrep(ctx) {
|
|
137
164
|
const q = parseGrepBody(ctx.body);
|
|
138
165
|
const limit = Math.min(100, Math.max(1, q.limit ?? 20));
|
|
@@ -254,7 +281,11 @@ function handleHistoryRead(ctx) {
|
|
|
254
281
|
});
|
|
255
282
|
}
|
|
256
283
|
const source = resolved.source === 'report' && resolved.reportKind ? `report:${resolved.reportKind}` : resolved.source;
|
|
257
|
-
|
|
284
|
+
// `raw` is the verbatim record; `--frontmatter` is the narrower legacy ask
|
|
285
|
+
// for the same bytes on a markdown artifact. Either one yields the raw form
|
|
286
|
+
// when the artifact has one.
|
|
287
|
+
const wantRaw = ctx.query.get('format') === 'raw' || includeFrontmatter;
|
|
288
|
+
const content = wantRaw && resolved.raw !== null ? resolved.raw : resolved.body;
|
|
258
289
|
const body = {
|
|
259
290
|
ref,
|
|
260
291
|
node: `${resolved.nodeName} (${resolved.nodeId})`,
|
|
@@ -266,45 +297,75 @@ function handleHistoryRead(ctx) {
|
|
|
266
297
|
}
|
|
267
298
|
// snapshot \u2014 the enriched browser canvas roster + subscribes_to adjacency.
|
|
268
299
|
// Lifted from `commands/canvas-snapshot.ts` run() (dashboardRowsAll + enrichRows).
|
|
269
|
-
|
|
300
|
+
// `?live=true` selects active/idle rows before the row builder's disk reads.
|
|
301
|
+
function snapshotLiveOnly(ctx) {
|
|
302
|
+
for (const [name] of ctx.query) {
|
|
303
|
+
if (name !== 'live')
|
|
304
|
+
throw usage(`unknown snapshot query parameter: ${name}`);
|
|
305
|
+
}
|
|
306
|
+
const values = ctx.query.getAll('live');
|
|
307
|
+
if (values.length > 1)
|
|
308
|
+
throw usage('live must appear at most once');
|
|
309
|
+
const value = values[0];
|
|
310
|
+
if (value === undefined || value === 'false' || value === '0')
|
|
311
|
+
return false;
|
|
312
|
+
if (value === 'true' || value === '1')
|
|
313
|
+
return true;
|
|
314
|
+
throw usage('live must be true or false');
|
|
315
|
+
}
|
|
316
|
+
async function handleSnapshot(ctx) {
|
|
270
317
|
const focusedNodeIds = new Set(listFocuses().map((f) => f.node_id));
|
|
271
|
-
const rows = await dashboardRowsAllFromSource(localCanvasSource, focusedNodeIds);
|
|
318
|
+
const rows = await dashboardRowsAllFromSource(localCanvasSource, focusedNodeIds, snapshotLiveOnly(ctx));
|
|
272
319
|
const asks = ticketCountsForNodes(rows.map((r) => r.node_id));
|
|
273
320
|
await enrichRowsFromSource(localCanvasSource, rows, asks);
|
|
274
321
|
const nodes = rows.map((row) => {
|
|
275
|
-
|
|
276
|
-
|
|
322
|
+
// Every field here comes from the NodeRow listNodes already fetched (stashed
|
|
323
|
+
// on the row as srcRow) — this handler opens no meta.json at all.
|
|
324
|
+
const src = row.srcRow;
|
|
325
|
+
const hostKind = src?.host_kind ?? 'tmux';
|
|
277
326
|
return {
|
|
278
327
|
node_id: row.node_id,
|
|
279
328
|
name: row.name,
|
|
280
|
-
title:
|
|
281
|
-
description:
|
|
329
|
+
title: row.handle ?? row.name,
|
|
330
|
+
description: row.description ?? null,
|
|
282
331
|
kind: row.kind,
|
|
283
332
|
mode: row.mode,
|
|
284
|
-
lifecycle: row.lifecycle ??
|
|
333
|
+
lifecycle: row.lifecycle ?? src?.lifecycle,
|
|
285
334
|
status: row.status,
|
|
286
335
|
cwd: row.cwd,
|
|
287
|
-
profile_id:
|
|
288
|
-
parent:
|
|
336
|
+
profile_id: row.profile_id ?? null,
|
|
337
|
+
parent: src?.parent ?? null,
|
|
289
338
|
created: row.created,
|
|
290
339
|
host_kind: hostKind,
|
|
291
340
|
enterable: hostKind === 'broker',
|
|
292
341
|
attention_count: row.asks,
|
|
293
|
-
...(
|
|
342
|
+
...(src?.cycles != null ? { cycles: src.cycles } : {}),
|
|
294
343
|
...(row.mtimeMs !== undefined ? { last_activity: new Date(row.mtimeMs).toISOString() } : {}),
|
|
295
344
|
ctx_tokens: row.ctx_tokens,
|
|
296
345
|
streaming: row.streaming ?? false,
|
|
297
|
-
jobs_active:
|
|
346
|
+
jobs_active: src !== undefined && isBrokerLive(src)
|
|
298
347
|
? activeBackgroundBashJobs(contextDir(row.node_id)).length
|
|
299
348
|
: 0,
|
|
300
|
-
waiting_for:
|
|
349
|
+
waiting_for: src?.waiting_for ?? null,
|
|
301
350
|
hanging: (row.hanging ?? null),
|
|
302
351
|
viewed: row.viewed ?? false,
|
|
303
|
-
final_report:
|
|
304
|
-
finalized_at:
|
|
352
|
+
final_report: src?.final_report ?? null,
|
|
353
|
+
finalized_at: src?.finalized_at ?? null,
|
|
305
354
|
};
|
|
306
355
|
});
|
|
307
|
-
|
|
356
|
+
// Opt 3: one rosterEdges() pass grouped by subscriber, replacing the per-node
|
|
357
|
+
// subscriptionsOf query. Both are `subscribes_to` edges (active + inactive)
|
|
358
|
+
// ORDER BY created, so grouping preserves the exact per-node ordering. Rows
|
|
359
|
+
// with no outgoing edge still get an empty array (iterate rows, not the map).
|
|
360
|
+
const edgesByFrom = new Map();
|
|
361
|
+
for (const e of rosterEdges()) {
|
|
362
|
+
const list = edgesByFrom.get(e.from_id);
|
|
363
|
+
if (list === undefined)
|
|
364
|
+
edgesByFrom.set(e.from_id, [e.to_id]);
|
|
365
|
+
else
|
|
366
|
+
list.push(e.to_id);
|
|
367
|
+
}
|
|
368
|
+
const subscriptions = Object.fromEntries(rows.map((row) => [row.node_id, edgesByFrom.get(row.node_id) ?? []]));
|
|
308
369
|
const body = { generated_at: nowIso(), nodes, subscriptions };
|
|
309
370
|
return { status: 200, body };
|
|
310
371
|
}
|
|
@@ -463,6 +524,7 @@ export const canvasRoutes = [
|
|
|
463
524
|
{ method: 'POST', pattern: '/v1/canvas/history/search', handler: handleHistorySearch },
|
|
464
525
|
{ method: 'POST', pattern: '/v1/canvas/history/grep', handler: handleHistoryGrep },
|
|
465
526
|
{ method: 'GET', pattern: '/v1/canvas/history/read', handler: handleHistoryRead },
|
|
527
|
+
{ method: 'POST', pattern: '/v1/canvas/history/stats', handler: handleHistoryStats },
|
|
466
528
|
{ method: 'GET', pattern: '/v1/canvas/snapshot', handler: handleSnapshot },
|
|
467
529
|
{ method: 'GET', pattern: '/v1/canvas/roster', handler: handleRoster },
|
|
468
530
|
{ method: 'POST', pattern: '/v1/canvas/prune', handler: handlePrune },
|
|
@@ -12,26 +12,29 @@ import { currentVersion } from '../../../core/self-update.js';
|
|
|
12
12
|
import { API_VERSION } from '../../../api/index.js';
|
|
13
13
|
import { DEFAULT_INTERVAL_MS } from '../../supervise-cadence.js';
|
|
14
14
|
import { envCrtrdTcp } from '../../../shared/env.js';
|
|
15
|
+
import { isPidAlive } from '../../../core/canvas/pid.js';
|
|
16
|
+
import { isBusy } from '../../../core/runtime/busy.js';
|
|
15
17
|
// The daemon's supervise-loop cadence, echoed as an informational field; the
|
|
16
18
|
// loop itself is unchanged (hard constraint). Derived from the single shared
|
|
17
19
|
// constant so it can never drift from crtrd's actual interval.
|
|
18
20
|
const TICK_INTERVAL_MS = DEFAULT_INTERVAL_MS;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
function nodeCounts() {
|
|
21
|
+
/** Read all canvas rows — a cheap store-access probe for health and status. */
|
|
22
|
+
function canvasRows() {
|
|
22
23
|
try {
|
|
23
|
-
|
|
24
|
-
for (const row of listNodes()) {
|
|
25
|
-
counts[row.status] = (counts[row.status] ?? 0) + 1;
|
|
26
|
-
}
|
|
27
|
-
return counts;
|
|
24
|
+
return listNodes();
|
|
28
25
|
}
|
|
29
26
|
catch {
|
|
30
27
|
return null;
|
|
31
28
|
}
|
|
32
29
|
}
|
|
30
|
+
function nodeCounts(rows) {
|
|
31
|
+
const counts = {};
|
|
32
|
+
for (const row of rows)
|
|
33
|
+
counts[row.status] = (counts[row.status] ?? 0) + 1;
|
|
34
|
+
return counts;
|
|
35
|
+
}
|
|
33
36
|
function handleHealthz() {
|
|
34
|
-
const storeAccessible =
|
|
37
|
+
const storeAccessible = canvasRows() !== null;
|
|
35
38
|
const body = {
|
|
36
39
|
ok: storeAccessible,
|
|
37
40
|
daemon_up: true,
|
|
@@ -41,16 +44,18 @@ function handleHealthz() {
|
|
|
41
44
|
return { status: storeAccessible ? 200 : 503, body };
|
|
42
45
|
}
|
|
43
46
|
function handleStatus() {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
47
|
+
const rows = canvasRows();
|
|
48
|
+
if (rows === null) {
|
|
46
49
|
// The store is unreachable — spec §8 store_unavailable.
|
|
47
50
|
throw { status: 503, code: 'store_unavailable', message: 'canvas store is inaccessible' };
|
|
48
51
|
}
|
|
52
|
+
const busyNodeCount = rows.filter((row) => row.kind !== 'human' && isBusy(row.node_id) && isPidAlive(row.pi_pid)).length;
|
|
49
53
|
const tcp = envCrtrdTcp();
|
|
50
54
|
const body = {
|
|
51
55
|
daemon_pid: process.pid,
|
|
52
56
|
tick_interval_ms: TICK_INTERVAL_MS,
|
|
53
|
-
node_counts:
|
|
57
|
+
node_counts: nodeCounts(rows),
|
|
58
|
+
busy_node_count: busyNodeCount,
|
|
54
59
|
listeners: {
|
|
55
60
|
socket_path: apiSocketPath(),
|
|
56
61
|
tcp: tcp !== undefined && tcp !== '' ? tcp : null,
|
|
@@ -249,7 +249,18 @@ async function handleMessage(ctx) {
|
|
|
249
249
|
try {
|
|
250
250
|
if (hasSidecar)
|
|
251
251
|
appendSituationalContext(id, sidecar);
|
|
252
|
-
await deliverLive(id, req.body,
|
|
252
|
+
await deliverLive(id, req.body, {
|
|
253
|
+
cards: [...(hasSidecar ? [sidecar] : []), ...oneShotCards],
|
|
254
|
+
// Same authorship the durable route would record for this send, so a
|
|
255
|
+
// reader cannot tell live from inbox delivery by the stamp.
|
|
256
|
+
origin: from === 'human'
|
|
257
|
+
? { type: 'human' }
|
|
258
|
+
: {
|
|
259
|
+
type: 'runtime',
|
|
260
|
+
kind: 'inbox',
|
|
261
|
+
entries: [{ ...(from === null ? {} : { from }), kind: 'message', tier }],
|
|
262
|
+
},
|
|
263
|
+
});
|
|
253
264
|
cancelCronsOnWake(id);
|
|
254
265
|
const body = {
|
|
255
266
|
node_id: id,
|
|
@@ -156,9 +156,10 @@ function buildRecipe(req, parent, cwd) {
|
|
|
156
156
|
/** The warm-pool request this create is asking for, or null when it carries
|
|
157
157
|
* something a pre-booted spare cannot honor. A spare is a bare root with no
|
|
158
158
|
* kickoff, so anything that shapes the node AT BIRTH — a prompt to run, a
|
|
159
|
-
* forked conversation, a managed worktree, a
|
|
160
|
-
* parent — disqualifies it. `kind`, `mode`, `
|
|
161
|
-
* and `output_schema` are all applied at claim time and
|
|
159
|
+
* forked conversation, a managed worktree, a model override, a caller-pinned
|
|
160
|
+
* node id, a spine parent — disqualifies it. `kind`, `mode`, `name`,
|
|
161
|
+
* `situational_context` and `output_schema` are all applied at claim time and
|
|
162
|
+
* so do not. A terminal
|
|
162
163
|
* root is not warm-compatible: every spare is resident. */
|
|
163
164
|
function warmRequestFor(req, cwd, profileId) {
|
|
164
165
|
if (!warmPoolEnabled())
|
|
@@ -171,6 +172,11 @@ function warmRequestFor(req, cwd, profileId) {
|
|
|
171
172
|
return null;
|
|
172
173
|
if ((req.prompt ?? '') !== '')
|
|
173
174
|
return null;
|
|
175
|
+
// An explicit override freezes the requested model before the first turn;
|
|
176
|
+
// applying it to a live spare would switch models after its broker already
|
|
177
|
+
// launched its default route.
|
|
178
|
+
if (req.model !== undefined)
|
|
179
|
+
return null;
|
|
174
180
|
if (req.fork_from !== undefined || req.worktree !== undefined || req.node_id !== undefined)
|
|
175
181
|
return null;
|
|
176
182
|
return {
|
|
@@ -196,6 +202,7 @@ async function assertNodeModelChangeRegistered(meta, target, registry) {
|
|
|
196
202
|
lifecycle: target.lifecycle,
|
|
197
203
|
hasManager: meta.parent !== null,
|
|
198
204
|
model: target.model,
|
|
205
|
+
modelOverride: true,
|
|
199
206
|
cwd: meta.cwd,
|
|
200
207
|
profileId: meta.profile_id ?? null,
|
|
201
208
|
})).launch;
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -39,7 +39,21 @@ export async function deliverNodeMessage(args) {
|
|
|
39
39
|
// card; only live delivery needs this message's own envelope.
|
|
40
40
|
await deliverLive(args.node_id, args.runtime === undefined
|
|
41
41
|
? args.body
|
|
42
|
-
: formatCard(args.runtime.kind, args.runtime.facts, args.body)
|
|
42
|
+
: formatCard(args.runtime.kind, args.runtime.facts, args.body), {
|
|
43
|
+
// The authorship the durable fallback below would record for this
|
|
44
|
+
// same send, so the two routes stamp identically.
|
|
45
|
+
origin: args.from === 'human'
|
|
46
|
+
? { type: 'human' }
|
|
47
|
+
: {
|
|
48
|
+
type: 'runtime',
|
|
49
|
+
kind: 'inbox',
|
|
50
|
+
entries: [{
|
|
51
|
+
...(args.from == null ? {} : { from: args.from }),
|
|
52
|
+
kind: 'message',
|
|
53
|
+
tier: 'normal',
|
|
54
|
+
}],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
43
57
|
// The broker acknowledgement is asynchronous to the SQLite guard. Recheck
|
|
44
58
|
// before reporting an engine receipt so a final that committed during the
|
|
45
59
|
// request is not represented as a routable delivery.
|
|
@@ -24,23 +24,30 @@ import { deliverLive } from '../../core/runtime/interactive-deliver.js';
|
|
|
24
24
|
import { BrokerUnreachableError } from '../../core/runtime/broker-request.js';
|
|
25
25
|
import { formatCard, PARK_SUMMARY_PROMPT } from '../../shared/generated-context.js';
|
|
26
26
|
import { brokerThresholdsForDaemon } from '../../core/runtime/fleet.js';
|
|
27
|
+
import { readConfig } from '../../core/config.js';
|
|
27
28
|
import { beginPendingPark, consumePendingPark, pendingParkIds, pendingParkStartedAt } from '../park-pending.js';
|
|
28
29
|
import { applyParkEvent, parkEvent } from './live-obligation.js';
|
|
29
|
-
import { envNoDaemonAutostart, envTestParkSummaryGraceMs, envTestUnattendedParkMs, } from '../../shared/env.js';
|
|
30
|
+
import { envNoDaemonAutostart, envTestParkSummaryGraceMs, envTestUnattendedParkMs, envUnattendedParkMs, } from '../../shared/env.js';
|
|
30
31
|
const execFileAsync = promisify(execFile);
|
|
31
32
|
export const YIELD_STALL_GRACE_MS = 3 * 60_000;
|
|
32
33
|
const KILL_ESCALATE_MS = 20_000;
|
|
33
34
|
export const WEDGE_QUIET_MS = 20 * 60_000;
|
|
34
35
|
export const WEDGE_CPU_MAX_PERCENT = 2;
|
|
35
36
|
const OVER_CAP_WARN_THROTTLE_MS = 5 * 60_000;
|
|
36
|
-
const DEFAULT_UNATTENDED_PARK_MS = 15 * 60_000;
|
|
37
37
|
// Longer than a summary turn and shorter than the unattended clock itself, so a
|
|
38
38
|
// stuck parking turn cannot hold a broker for a whole extra clock period.
|
|
39
39
|
const DEFAULT_PARK_SUMMARY_GRACE_MS = 5 * 60_000;
|
|
40
40
|
export function unattendedParkMsForDaemon() {
|
|
41
|
+
// The `CRTR_UNATTENDED_PARK_MS` override wins whenever set, autostart or not.
|
|
42
|
+
const envOverride = envUnattendedParkMs();
|
|
43
|
+
if (envOverride !== undefined)
|
|
44
|
+
return envOverride;
|
|
45
|
+
// Otherwise the user-scope config value is the base (default 15 min), with the
|
|
46
|
+
// test-only override winning only when autostart is disabled.
|
|
47
|
+
const configured = readConfig('user').lifecycle.unattendedParkMs;
|
|
41
48
|
if (!envNoDaemonAutostart())
|
|
42
|
-
return
|
|
43
|
-
return envTestUnattendedParkMs() ??
|
|
49
|
+
return configured;
|
|
50
|
+
return envTestUnattendedParkMs() ?? configured;
|
|
44
51
|
}
|
|
45
52
|
export function parkSummaryGraceMsForDaemon() {
|
|
46
53
|
return envTestParkSummaryGraceMs() ?? DEFAULT_PARK_SUMMARY_GRACE_MS;
|
|
@@ -149,6 +156,11 @@ export class BrokerSupervisionReconciler {
|
|
|
149
156
|
ctx.lifecycle.registerDetached(warning);
|
|
150
157
|
}
|
|
151
158
|
async run(now, ctx) {
|
|
159
|
+
// Resolve the unattended-park interval once per tick — it reads user
|
|
160
|
+
// config.json, so a per-broker read would add one file read per eligible
|
|
161
|
+
// broker and could apply different values within one tick after a config
|
|
162
|
+
// write (mirrors how `runFleetPreamble` snapshots the broker thresholds).
|
|
163
|
+
const unattendedParkMs = unattendedParkMsForDaemon();
|
|
152
164
|
for (const row of ctx.rows) {
|
|
153
165
|
const id = row.node_id;
|
|
154
166
|
const entry = ctx.fleet.get(id);
|
|
@@ -161,7 +173,7 @@ export class BrokerSupervisionReconciler {
|
|
|
161
173
|
this.handleYieldStall(row, entry.pid, now);
|
|
162
174
|
await this.handleWedgeDetection(id, entry.pid, row.pi_pid_identity, now);
|
|
163
175
|
this.handleFatalFault(id);
|
|
164
|
-
this.handleUnattendedParking(id, getNode(id), entry.pid, now, ctx);
|
|
176
|
+
this.handleUnattendedParking(id, getNode(id), entry.pid, now, ctx, unattendedParkMs);
|
|
165
177
|
}
|
|
166
178
|
catch (err) {
|
|
167
179
|
this.unattendedSince.delete(id);
|
|
@@ -211,7 +223,7 @@ export class BrokerSupervisionReconciler {
|
|
|
211
223
|
return true;
|
|
212
224
|
return hasUnseenWakeMail(id);
|
|
213
225
|
}
|
|
214
|
-
handleUnattendedParking(id, row, livePid, now, ctx) {
|
|
226
|
+
handleUnattendedParking(id, row, livePid, now, ctx, unattendedParkMs) {
|
|
215
227
|
// An unclaimed warm-pool spare is not a conversation: the booted broker it
|
|
216
228
|
// holds IS its whole value, and nobody is meant to be attending it. Its
|
|
217
229
|
// lifetime belongs to `reapStaleSpares` (warm-pool.ts) and its own TTL, so
|
|
@@ -269,7 +281,7 @@ export class BrokerSupervisionReconciler {
|
|
|
269
281
|
this.unattendedSince.set(id, now);
|
|
270
282
|
return;
|
|
271
283
|
}
|
|
272
|
-
if (now - since <
|
|
284
|
+
if (now - since < unattendedParkMs)
|
|
273
285
|
return;
|
|
274
286
|
this.unattendedSince.delete(id);
|
|
275
287
|
const event = parkEvent(id, row.lifecycle);
|
|
@@ -302,7 +314,9 @@ export class BrokerSupervisionReconciler {
|
|
|
302
314
|
* the node is working again and its clock simply restarts. */
|
|
303
315
|
async requestParkSummary(id) {
|
|
304
316
|
try {
|
|
305
|
-
const route = await deliverLive(id, formatCard('park', {}, PARK_SUMMARY_PROMPT)
|
|
317
|
+
const route = await deliverLive(id, formatCard('park', {}, PARK_SUMMARY_PROMPT), {
|
|
318
|
+
origin: { type: 'runtime', kind: 'park' },
|
|
319
|
+
});
|
|
306
320
|
if (route === 'steer' && consumePendingPark(id))
|
|
307
321
|
return;
|
|
308
322
|
emitEvent({
|
|
@@ -361,7 +375,10 @@ export class BrokerSupervisionReconciler {
|
|
|
361
375
|
if (client === '')
|
|
362
376
|
return;
|
|
363
377
|
try {
|
|
364
|
-
|
|
378
|
+
// `-C` keeps the client's panes painting while the warning sits in
|
|
379
|
+
// its status line; without it every attached viewer freezes for the
|
|
380
|
+
// full 10s. The plain retry below covers a tmux without the flag.
|
|
381
|
+
await execFileAsync('tmux', ['display-message', '-C', '-d', '10000', '-c', client, msg], {
|
|
365
382
|
encoding: 'utf8',
|
|
366
383
|
timeout: 2000,
|
|
367
384
|
});
|
|
@@ -11,10 +11,17 @@ export declare function hasLiveObligation(nodeId: string): boolean;
|
|
|
11
11
|
* Deferred mail deliberately does not count: it rides the node's next natural
|
|
12
12
|
* cycle rather than causing one. */
|
|
13
13
|
export declare function hasPendingWake(nodeId: string): boolean;
|
|
14
|
+
/** Whether a person is looking at this node right now: a focused viewer pane,
|
|
15
|
+
* or a broker client that declared itself attending. A client that hellos with
|
|
16
|
+
* `attends: false` is a system observer and deliberately does not count, which
|
|
17
|
+
* is how Northlight Core identifies itself — so an open product page is
|
|
18
|
+
* invisible here, while crouter's own local and remote viewers are not. */
|
|
19
|
+
export declare function hasHumanViewer(nodeId: string): boolean;
|
|
14
20
|
/** What parking an unattended node actually amounts to — the one rule both the
|
|
15
21
|
* live-broker park clock and the lifecycle tick apply.
|
|
16
22
|
*
|
|
17
|
-
* `release` — something can still wake it,
|
|
23
|
+
* `release` — something can still wake it, or a person is watching it, so
|
|
24
|
+
* parking is a pause.
|
|
18
25
|
* `park` — a resident with nothing left to wake it has completed its
|
|
19
26
|
* conversation, and the row becomes prunable history. The live
|
|
20
27
|
* clock asks it for a summary turn first; this verdict is what
|
|
@@ -28,9 +28,26 @@ export function hasPendingWake(nodeId) {
|
|
|
28
28
|
return true;
|
|
29
29
|
if (hasUnseenWakeMail(nodeId))
|
|
30
30
|
return true;
|
|
31
|
+
return hasHumanViewer(nodeId);
|
|
32
|
+
}
|
|
33
|
+
/** Whether a person is looking at this node right now: a focused viewer pane,
|
|
34
|
+
* or a broker client that declared itself attending. A client that hellos with
|
|
35
|
+
* `attends: false` is a system observer and deliberately does not count, which
|
|
36
|
+
* is how Northlight Core identifies itself — so an open product page is
|
|
37
|
+
* invisible here, while crouter's own local and remote viewers are not. */
|
|
38
|
+
export function hasHumanViewer(nodeId) {
|
|
31
39
|
return focusOf(nodeId) !== null || hasAttachedBrokerViewers(nodeId);
|
|
32
40
|
}
|
|
33
41
|
export function parkEvent(nodeId, lifecycle) {
|
|
42
|
+
// Attendance defeats unattended parking outright: the grace exists to settle
|
|
43
|
+
// a conversation nobody is in, and terminalizing a node somebody is watching
|
|
44
|
+
// is the one outcome the grace must never reach. The live clock resets its
|
|
45
|
+
// own timer on the same test, so this covers the paths that reach a verdict
|
|
46
|
+
// without it — the settlement re-check after a parking turn, and the tick's
|
|
47
|
+
// sweep over already-idle rows. The live clock discards its timer on that
|
|
48
|
+
// same reset, so parking resumes a full grace after the last viewer detaches.
|
|
49
|
+
if (hasHumanViewer(nodeId))
|
|
50
|
+
return 'release';
|
|
34
51
|
if (hasLiveObligation(nodeId))
|
|
35
52
|
return 'release';
|
|
36
53
|
if (lifecycle === 'resident')
|
|
@@ -175,7 +175,7 @@ test('a matching command is held with its doc, every sibling in the message with
|
|
|
175
175
|
// both — and that single fact is what releases the re-issue below.
|
|
176
176
|
const afterBlock = loadContextExposureState('gate-hold');
|
|
177
177
|
assert.deepEqual(afterBlock.exposures.get(docPath), { transcript: 3 }, 'the held doc is recorded as delivered');
|
|
178
|
-
assert.equal(afterBlock.exposures.size,
|
|
178
|
+
assert.equal(afterBlock.exposures.size, 4, 'the content alias and the read-equivalent memory-read companion are recorded alongside the held doc');
|
|
179
179
|
// Siblings of the same assistant message. The doc is already exposed, so
|
|
180
180
|
// without the per-turn flag the render would come back empty and let them
|
|
181
181
|
// through. The flag covers the whole message, matching or not: every command
|
|
@@ -203,7 +203,7 @@ test('compaction re-arms the hold and leaves an exposure ledger that reloads', a
|
|
|
203
203
|
// proof it did.
|
|
204
204
|
const afterCompact = loadContextExposureState('gate-compact');
|
|
205
205
|
assert.equal(afterCompact.exposures.has(docPath), false, 'compaction forgets the delivery that compaction dropped');
|
|
206
|
-
assert.equal(afterCompact.exposures.size,
|
|
206
|
+
assert.equal(afterCompact.exposures.size, 2, 'the held doc and its alias are forgotten while its read-equivalent companion remains, with no rankless entry');
|
|
207
207
|
const reheld = assertHeld(await pi.bash('deploy prod'), 'the same command after compaction');
|
|
208
208
|
assert.ok(reheld.reason.includes(DOCTRINE_BODY), 'guidance the transcript no longer carries is delivered again');
|
|
209
209
|
});
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { envNodeId, envProfileId, envWatcherDebounceMs, envWatcherTickMs } from '../shared/env.js';
|
|
18
18
|
import { coalesceBrokerInbox, entryCards, readBrokerCanceledEntryIds, readBrokerCursor, readBrokerInboxSince, reportRefNodeId, } from '../core/runtime/broker/inbox.js';
|
|
19
19
|
import { advanceBrokerInboxCursor, brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
|
|
20
|
+
import { announceMessageOrigin } from '../core/runtime/stamp/channel.js';
|
|
20
21
|
import { captureGoalIfAbsent, generateAndCommitInitialName } from './broker-local.js';
|
|
21
22
|
import { emitEvent } from '../core/events/emit.js';
|
|
22
23
|
import { errorClassFromError } from '../core/events/errors.js';
|
|
@@ -97,6 +98,26 @@ function emitStoredBestEffort(operationId, input) {
|
|
|
97
98
|
// Delivery state remains authoritative when its observational record cannot append.
|
|
98
99
|
}
|
|
99
100
|
}
|
|
101
|
+
/** The authorship stamped onto a delivered message. A person's words arrive
|
|
102
|
+
* verbatim and are stamped as theirs — no channel, because the durable inbox
|
|
103
|
+
* is not a transport a reader can attribute. Every other delivery is the
|
|
104
|
+
* runtime's inbox card, carrying each entry's attribution as the coalescer
|
|
105
|
+
* received it. */
|
|
106
|
+
function deliveryOrigin(delivery) {
|
|
107
|
+
if (delivery.kind === 'human')
|
|
108
|
+
return { type: 'human' };
|
|
109
|
+
return {
|
|
110
|
+
type: 'runtime',
|
|
111
|
+
kind: 'inbox',
|
|
112
|
+
entries: delivery.entries.map((entry) => ({
|
|
113
|
+
...(entry.from === null ? {} : { from: entry.from }),
|
|
114
|
+
...(entry.from_name === undefined ? {} : { fromName: entry.from_name }),
|
|
115
|
+
kind: entry.kind,
|
|
116
|
+
tier: entry.tier,
|
|
117
|
+
...(entry.ref === undefined ? {} : { ref: entry.ref }),
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
100
121
|
function deliveryFacts(entries, channel) {
|
|
101
122
|
if (entries.length === 0)
|
|
102
123
|
throw new Error(`cannot create an empty ${channel} delivery`);
|
|
@@ -729,7 +750,7 @@ class CanvasInboxWatcher {
|
|
|
729
750
|
* unit: a throw anywhere requeues the whole entry, so a retry may repeat a
|
|
730
751
|
* card already handed off (context is restatable; the human's body is not,
|
|
731
752
|
* and it is sent last). */
|
|
732
|
-
sendVisible(entries, cards, content, route, stage) {
|
|
753
|
+
sendVisible(entries, cards, content, origin, route, stage) {
|
|
733
754
|
return this.sendDelivery('visible', entries, () => {
|
|
734
755
|
if (route !== 'idle' && route !== 'steer' && route !== 'followUp') {
|
|
735
756
|
throw new Error(`visible send requested for non-send route ${route}`);
|
|
@@ -737,6 +758,10 @@ class CanvasInboxWatcher {
|
|
|
737
758
|
for (const card of cards) {
|
|
738
759
|
this.pi.sendMessage({ customType: SITUATIONAL_CONTEXT_CUSTOM_TYPE, content: card, display: true }, route === 'idle' ? {} : { deliverAs: route });
|
|
739
760
|
}
|
|
761
|
+
// Claimed by the stamp writer against the message pi persists, so a send
|
|
762
|
+
// that throws here leaves an announcement nobody claims rather than one
|
|
763
|
+
// that mislabels a later message.
|
|
764
|
+
announceMessageOrigin(content, origin);
|
|
740
765
|
if (route === 'idle') {
|
|
741
766
|
const start = brokerIdleTurnStart();
|
|
742
767
|
if (start === undefined)
|
|
@@ -803,7 +828,7 @@ class CanvasInboxWatcher {
|
|
|
803
828
|
if (!this.canMutate())
|
|
804
829
|
return { inactive: true, requeueEntryIds };
|
|
805
830
|
if (delivery !== undefined) {
|
|
806
|
-
const result = this.sendVisible(delivery.entries, delivery.cards, delivery.text, route, stage);
|
|
831
|
+
const result = this.sendVisible(delivery.entries, delivery.cards, delivery.text, deliveryOrigin(delivery), route, stage);
|
|
807
832
|
if (result === 'inactive')
|
|
808
833
|
return { inactive: true, requeueEntryIds };
|
|
809
834
|
if (result === 'failed') {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
import { envNodeId, envRecapIdleMs } from '../shared/env.js';
|
|
42
42
|
import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
|
|
43
43
|
import { generateRecap } from '../core/runtime/recap.js';
|
|
44
|
+
import { writeNodeRecap } from '../core/canvas/node-recap.js';
|
|
44
45
|
import { isReviewBoundary } from '../core/runtime/session-visibility.js';
|
|
45
46
|
import { editorLabelForBrokerNode } from '../core/runtime/broker-extension-render.js';
|
|
46
47
|
import { generateAndCommitRecapName, readRoadmap } from './broker-local.js';
|
|
@@ -315,6 +316,9 @@ export function registerCanvasRecap(pi, recapGenerator = generateRecap, nameRege
|
|
|
315
316
|
// Automatic work paints only while its idle snapshot remains current.
|
|
316
317
|
if ((!manual && activityVersion !== snapshotVersion) || !canPaint())
|
|
317
318
|
return;
|
|
319
|
+
// Persist what we are about to paint. The widget dies with the broker; the
|
|
320
|
+
// file is what `crtr canvas browse` shows for a dormant node.
|
|
321
|
+
writeNodeRecap(nodeId, briefing);
|
|
318
322
|
recapShown = true;
|
|
319
323
|
recapStale = activityVersion !== snapshotVersion;
|
|
320
324
|
showRecap(briefing);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface StampCtxLike {
|
|
2
|
+
sessionManager: {
|
|
3
|
+
/** Whole file, not the active branch: a session stamp on an abandoned
|
|
4
|
+
* branch still describes this file's owner. */
|
|
5
|
+
getEntries: () => unknown[];
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
interface PiLike {
|
|
9
|
+
on: (event: 'session_start' | 'message_end', handler: (event: unknown, ctx: StampCtxLike) => void | Promise<void>) => void;
|
|
10
|
+
appendEntry: (customType: string, data?: unknown) => void;
|
|
11
|
+
}
|
|
12
|
+
/** Register the stamp writer. */
|
|
13
|
+
export declare function registerCanvasStamp(pi: PiLike): void;
|
|
14
|
+
export default registerCanvasStamp;
|