@north-light/crouter 0.3.263 → 0.3.270
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/broker.d.ts +11 -2
- package/dist/api/dto/broker.js +2 -2
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/nodes.d.ts +10 -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-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 +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +7 -2
- 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 +607 -606
- 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/node/create.js +2 -2
- package/dist/commands/pkg/plugin-inspect.js +2 -2
- package/dist/commands/pkg/plugin-manage.js +3 -3
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/doctor.js +4 -3
- package/dist/commands/sys/panels/models-panel.js +3 -1
- package/dist/core/__tests__/human-deliver.test.js +1 -1
- package/dist/core/__tests__/integration/lifecycle-hooks.test.d.ts +1 -0
- package/dist/core/__tests__/integration/lifecycle-hooks.test.js +182 -0
- package/dist/core/__tests__/integration/revive.test.js +19 -17
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/push-final-guard.test.js +1 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/revive-capacity.test.js +20 -20
- package/dist/core/__tests__/revive-parked-fresh.test.js +14 -14
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +2 -2
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
- package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
- package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +1 -1
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +2 -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 +33 -1
- package/dist/core/canvas/render-source.js +80 -22
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/command-hooks/discovery.d.ts +27 -1
- package/dist/core/command-hooks/discovery.js +52 -0
- package/dist/core/command-hooks/index.d.ts +4 -3
- package/dist/core/command-hooks/index.js +2 -1
- package/dist/core/command-hooks/lifecycle-catalog.d.ts +3 -0
- package/dist/core/command-hooks/lifecycle-catalog.js +4 -0
- package/dist/core/command-hooks/report.d.ts +11 -2
- package/dist/core/command-hooks/report.js +10 -1
- package/dist/core/command-hooks/schema.d.ts +16 -1
- package/dist/core/command-hooks/schema.js +128 -40
- package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +12 -0
- package/dist/core/command-hooks/transport/exec-lifecycle.js +152 -0
- package/dist/core/config.js +13 -5
- package/dist/core/human/feedback-companion.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/model-routes.js +1 -1
- package/dist/core/review/realize.js +1 -1
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/engine-drive.d.ts +14 -3
- package/dist/core/runtime/broker/engine-drive.js +29 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +3 -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/rebind.js +7 -0
- package/dist/core/runtime/broker-protocol.d.ts +16 -1
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +27 -3
- 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/node-read.d.ts +2 -0
- package/dist/core/runtime/node-read.js +18 -9
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive-all.d.ts +1 -1
- package/dist/core/runtime/revive-all.js +2 -2
- package/dist/core/runtime/revive.d.ts +2 -2
- package/dist/core/runtime/revive.js +71 -13
- package/dist/core/runtime/session-visibility.d.ts +10 -14
- package/dist/core/runtime/session-visibility.js +43 -30
- 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 +22 -0
- package/dist/core/runtime/stamp/channel.js +83 -0
- package/dist/core/runtime/stamp/protocol.d.ts +90 -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/runtime/turn-visibility.d.ts +10 -0
- package/dist/core/runtime/turn-visibility.js +31 -0
- package/dist/core/self-update.js +2 -5
- 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__/profile-launch-gates.test.js +2 -2
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/attach.js +4 -4
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/canvas.js +62 -16
- package/dist/daemon/api/handlers/messages.js +14 -3
- package/dist/daemon/api/handlers/nodes.js +17 -7
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/cron/sinks.js +3 -3
- package/dist/daemon/fleet.js +1 -1
- package/dist/daemon/messaging/node-message.js +16 -2
- package/dist/daemon/profile-delete.js +1 -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/daemon/reconcilers/node-lifecycle/freeze-lane.js +6 -6
- package/dist/hook-authoring.d.ts +31 -1
- package/dist/hook-authoring.js +42 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- 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/shared/generated-context.d.ts +1 -2
- package/dist/shared/generated-context.js +3 -4
- package/dist/types.d.ts +10 -0
- 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})`,
|
|
@@ -288,39 +319,53 @@ async function handleSnapshot(ctx) {
|
|
|
288
319
|
const asks = ticketCountsForNodes(rows.map((r) => r.node_id));
|
|
289
320
|
await enrichRowsFromSource(localCanvasSource, rows, asks);
|
|
290
321
|
const nodes = rows.map((row) => {
|
|
291
|
-
|
|
292
|
-
|
|
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';
|
|
293
326
|
return {
|
|
294
327
|
node_id: row.node_id,
|
|
295
328
|
name: row.name,
|
|
296
|
-
title:
|
|
297
|
-
description:
|
|
329
|
+
title: row.handle ?? row.name,
|
|
330
|
+
description: row.description ?? null,
|
|
298
331
|
kind: row.kind,
|
|
299
332
|
mode: row.mode,
|
|
300
|
-
lifecycle: row.lifecycle ??
|
|
333
|
+
lifecycle: row.lifecycle ?? src?.lifecycle,
|
|
301
334
|
status: row.status,
|
|
302
335
|
cwd: row.cwd,
|
|
303
|
-
profile_id:
|
|
304
|
-
parent:
|
|
336
|
+
profile_id: row.profile_id ?? null,
|
|
337
|
+
parent: src?.parent ?? null,
|
|
305
338
|
created: row.created,
|
|
306
339
|
host_kind: hostKind,
|
|
307
340
|
enterable: hostKind === 'broker',
|
|
308
341
|
attention_count: row.asks,
|
|
309
|
-
...(
|
|
342
|
+
...(src?.cycles != null ? { cycles: src.cycles } : {}),
|
|
310
343
|
...(row.mtimeMs !== undefined ? { last_activity: new Date(row.mtimeMs).toISOString() } : {}),
|
|
311
344
|
ctx_tokens: row.ctx_tokens,
|
|
312
345
|
streaming: row.streaming ?? false,
|
|
313
|
-
jobs_active:
|
|
346
|
+
jobs_active: src !== undefined && isBrokerLive(src)
|
|
314
347
|
? activeBackgroundBashJobs(contextDir(row.node_id)).length
|
|
315
348
|
: 0,
|
|
316
|
-
waiting_for:
|
|
349
|
+
waiting_for: src?.waiting_for ?? null,
|
|
317
350
|
hanging: (row.hanging ?? null),
|
|
318
351
|
viewed: row.viewed ?? false,
|
|
319
|
-
final_report:
|
|
320
|
-
finalized_at:
|
|
352
|
+
final_report: src?.final_report ?? null,
|
|
353
|
+
finalized_at: src?.finalized_at ?? null,
|
|
321
354
|
};
|
|
322
355
|
});
|
|
323
|
-
|
|
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) ?? []]));
|
|
324
369
|
const body = { generated_at: nowIso(), nodes, subscriptions };
|
|
325
370
|
return { status: 200, body };
|
|
326
371
|
}
|
|
@@ -479,6 +524,7 @@ export const canvasRoutes = [
|
|
|
479
524
|
{ method: 'POST', pattern: '/v1/canvas/history/search', handler: handleHistorySearch },
|
|
480
525
|
{ method: 'POST', pattern: '/v1/canvas/history/grep', handler: handleHistoryGrep },
|
|
481
526
|
{ method: 'GET', pattern: '/v1/canvas/history/read', handler: handleHistoryRead },
|
|
527
|
+
{ method: 'POST', pattern: '/v1/canvas/history/stats', handler: handleHistoryStats },
|
|
482
528
|
{ method: 'GET', pattern: '/v1/canvas/snapshot', handler: handleSnapshot },
|
|
483
529
|
{ method: 'GET', pattern: '/v1/canvas/roster', handler: handleRoster },
|
|
484
530
|
{ method: 'POST', pattern: '/v1/canvas/prune', handler: handlePrune },
|
|
@@ -224,7 +224,7 @@ async function handleMessage(ctx) {
|
|
|
224
224
|
commitReopen(id, expectedFinalReport);
|
|
225
225
|
// A fresh revive appends no inbox entry, so nothing survives a freeze for
|
|
226
226
|
// the daemon to act on later: at the cap the caller is told, not ignored.
|
|
227
|
-
const result = reviveNode(id, { resume: false, capacity: 'refuse' });
|
|
227
|
+
const result = await reviveNode(id, { resume: false, capacity: 'refuse' });
|
|
228
228
|
const body = {
|
|
229
229
|
node_id: id,
|
|
230
230
|
delivered: false,
|
|
@@ -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,
|
|
@@ -340,7 +351,7 @@ async function handleMessage(ctx) {
|
|
|
340
351
|
// The entry is already durable, so a wake denied for capacity is only
|
|
341
352
|
// delayed: the row freezes and the tick relaunches it — at which point
|
|
342
353
|
// the inbox watcher delivers this entry as it always would.
|
|
343
|
-
const result = reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
354
|
+
const result = await reviveNode(id, { resume: true, capacity: 'freeze' });
|
|
344
355
|
revived = result.launch !== undefined;
|
|
345
356
|
frozen = result.outcome === 'frozen';
|
|
346
357
|
}
|
|
@@ -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;
|
|
@@ -367,6 +374,8 @@ async function handleSnapshot(ctx) {
|
|
|
367
374
|
snapshot: {
|
|
368
375
|
messages: read.snapshot.messages,
|
|
369
376
|
...(read.snapshot.messageIds === undefined ? {} : { messageIds: read.snapshot.messageIds }),
|
|
377
|
+
messageVisibility: read.snapshot.messageVisibility,
|
|
378
|
+
turnVisibility: read.snapshot.turnVisibility,
|
|
370
379
|
stats: read.snapshot.stats,
|
|
371
380
|
state: read.snapshot.state,
|
|
372
381
|
display: read.snapshot.display,
|
|
@@ -416,6 +425,7 @@ async function handleMessages(ctx) {
|
|
|
416
425
|
node_id: id,
|
|
417
426
|
messages: read.messages,
|
|
418
427
|
...(read.messageIds === undefined ? {} : { message_ids: read.messageIds }),
|
|
428
|
+
message_visibility: read.messageVisibility,
|
|
419
429
|
next_cursor: read.nextCursor,
|
|
420
430
|
captured_at: nowIso(),
|
|
421
431
|
};
|
|
@@ -484,7 +494,7 @@ async function handleFork(ctx) {
|
|
|
484
494
|
const result = await forkNode(id);
|
|
485
495
|
return { status: 201, body: detailOf(result.node.node_id) };
|
|
486
496
|
}
|
|
487
|
-
function handleRevive(ctx) {
|
|
497
|
+
async function handleRevive(ctx) {
|
|
488
498
|
const id = ctx.params['id'];
|
|
489
499
|
requireMeta(id);
|
|
490
500
|
const body = ctx.body;
|
|
@@ -500,7 +510,7 @@ function handleRevive(ctx) {
|
|
|
500
510
|
// rather than a message. A cron id that no longer resolves (canceled or
|
|
501
511
|
// consumed mid-run) simply yields no block — prose, never an error.
|
|
502
512
|
const cron = !resume && body?.cron_id !== undefined ? getCron(body.cron_id) : null;
|
|
503
|
-
const result = reviveNode(id, {
|
|
513
|
+
const result = await reviveNode(id, {
|
|
504
514
|
resume,
|
|
505
515
|
// An explicit revive is a caller asking for this node NOW; at the cap it
|
|
506
516
|
// gets the refusal naming it rather than a silent nothing-happened.
|
|
@@ -622,8 +632,8 @@ function handleWait(ctx) {
|
|
|
622
632
|
setMessageWait(id, req.controller);
|
|
623
633
|
return { status: 200, body: detailOf(id) };
|
|
624
634
|
}
|
|
625
|
-
function handleReviveAll() {
|
|
626
|
-
const result = reviveAll();
|
|
635
|
+
async function handleReviveAll() {
|
|
636
|
+
const result = await reviveAll();
|
|
627
637
|
const body = {
|
|
628
638
|
revived: [...result.revived],
|
|
629
639
|
failed: [...result.failed],
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -15,7 +15,7 @@ import { spawnChild } from '../../core/runtime/spawn.js';
|
|
|
15
15
|
import { cronWakeOrigin } from '../../core/runtime/bearings.js';
|
|
16
16
|
import { parseSink } from '../cron-sink.js';
|
|
17
17
|
import { resolveLiveProfile, resolveLiveProfileName } from './armed-context.js';
|
|
18
|
-
function deliverNodeSink(c, target, stdout) {
|
|
18
|
+
async function deliverNodeSink(c, target, stdout) {
|
|
19
19
|
const meta = getNode(target);
|
|
20
20
|
if (meta === null)
|
|
21
21
|
throw new Error(`node sink ${target} is gone`);
|
|
@@ -38,7 +38,7 @@ function deliverNodeSink(c, target, stdout) {
|
|
|
38
38
|
// The durable append succeeds even if its best-effort wake fails.
|
|
39
39
|
if (entry.tier !== 'deferred' && !isBrokerLive(meta)) {
|
|
40
40
|
try {
|
|
41
|
-
reviveNode(target, { resume: true, capacity: 'freeze' });
|
|
41
|
+
await reviveNode(target, { resume: true, capacity: 'freeze' });
|
|
42
42
|
}
|
|
43
43
|
catch {
|
|
44
44
|
/* best-effort wake — the entry is durably appended regardless */
|
|
@@ -93,7 +93,7 @@ export async function deliverToSink(c, stdout) {
|
|
|
93
93
|
if (sink === null)
|
|
94
94
|
return 'no sink armed — nothing delivered';
|
|
95
95
|
if (sink.kind === 'node') {
|
|
96
|
-
deliverNodeSink(c, sink.node, stdout);
|
|
96
|
+
await deliverNodeSink(c, sink.node, stdout);
|
|
97
97
|
return `delivered: node ${sink.node}`;
|
|
98
98
|
}
|
|
99
99
|
if (sink.kind === 'spawn') {
|
package/dist/daemon/fleet.js
CHANGED
|
@@ -343,7 +343,7 @@ export class DaemonFleet {
|
|
|
343
343
|
},
|
|
344
344
|
});
|
|
345
345
|
try {
|
|
346
|
-
reviveNode(nodeId, {
|
|
346
|
+
await reviveNode(nodeId, {
|
|
347
347
|
// A recovery that loses the race for the last slot freezes and waits
|
|
348
348
|
// for one — the tick thaws it — rather than erroring the node away.
|
|
349
349
|
capacity: 'freeze',
|
|
@@ -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.
|
|
@@ -73,7 +87,7 @@ export async function deliverNodeMessage(args) {
|
|
|
73
87
|
try {
|
|
74
88
|
// Mail is durable, so a wake denied for capacity waits for a slot
|
|
75
89
|
// rather than being lost: the row freezes and the tick relaunches it.
|
|
76
|
-
const result = reviveNode(args.node_id, { resume: true, capacity: 'freeze' });
|
|
90
|
+
const result = await reviveNode(args.node_id, { resume: true, capacity: 'freeze' });
|
|
77
91
|
revived = result.launch !== undefined;
|
|
78
92
|
if (result.outcome === 'frozen')
|
|
79
93
|
reason = 'capacity_frozen';
|
|
@@ -216,7 +216,7 @@ async function detachProfile(plan) {
|
|
|
216
216
|
const detached = new Set(detachedNodeIds);
|
|
217
217
|
for (const nodeId of restartNodeIds) {
|
|
218
218
|
if (detached.has(nodeId))
|
|
219
|
-
reviveNode(nodeId, { resume: true, recovery: true, capacity: 'freeze' });
|
|
219
|
+
await reviveNode(nodeId, { resume: true, recovery: true, capacity: 'freeze' });
|
|
220
220
|
}
|
|
221
221
|
return {
|
|
222
222
|
...resultForPlan(plan),
|
|
@@ -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')
|
|
@@ -83,9 +83,9 @@ export class FreezeLaneReconciler {
|
|
|
83
83
|
const id = entry.row.node_id;
|
|
84
84
|
try {
|
|
85
85
|
if (entry.enact === 'thaw')
|
|
86
|
-
return this.enactThaw(entry.row);
|
|
86
|
+
return await this.enactThaw(entry.row);
|
|
87
87
|
if (entry.enact === 'idle-release')
|
|
88
|
-
return this.enactIdleRelease(now, entry.row);
|
|
88
|
+
return await this.enactIdleRelease(now, entry.row);
|
|
89
89
|
// The SAME policy table an exit event runs, so tick recovery and exit
|
|
90
90
|
// recovery can never diverge. It relaunches through
|
|
91
91
|
// `reviveNode(capacity: 'freeze')`, which freezes the row when the ledger
|
|
@@ -106,17 +106,17 @@ export class FreezeLaneReconciler {
|
|
|
106
106
|
return 'skipped';
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
enactThaw(row) {
|
|
109
|
+
async enactThaw(row) {
|
|
110
110
|
if (row.lifecycle === 'resident' && getNode(row.node_id)?.pi_session_id != null && !hasPendingWake(row.node_id)) {
|
|
111
111
|
transition(row.node_id, 'release');
|
|
112
112
|
clearFrozen(row.node_id);
|
|
113
113
|
return 'skipped';
|
|
114
114
|
}
|
|
115
|
-
return reviveNode(row.node_id, { resume: true, capacity: 'freeze', recovery: true }).outcome === 'frozen'
|
|
115
|
+
return (await reviveNode(row.node_id, { resume: true, capacity: 'freeze', recovery: true })).outcome === 'frozen'
|
|
116
116
|
? 'frozen'
|
|
117
117
|
: 'launched';
|
|
118
118
|
}
|
|
119
|
-
enactIdleRelease(now, row) {
|
|
119
|
+
async enactIdleRelease(now, row) {
|
|
120
120
|
const id = row.node_id;
|
|
121
121
|
const cursorNow = readCursor(id);
|
|
122
122
|
const prior = this.relaunchState.get(id);
|
|
@@ -147,7 +147,7 @@ export class FreezeLaneReconciler {
|
|
|
147
147
|
// A relaunch the broker cap froze got no slot and therefore proved nothing
|
|
148
148
|
// about this delivery, so it must not spend an attempt: only a launch that
|
|
149
149
|
// actually happened advances the episode toward exhaustion.
|
|
150
|
-
if (reviveNode(id, { resume: true, capacity: 'freeze' }).outcome === 'frozen')
|
|
150
|
+
if ((await reviveNode(id, { resume: true, capacity: 'freeze' })).outcome === 'frozen')
|
|
151
151
|
return 'frozen';
|
|
152
152
|
this.relaunchState.set(id, { cursor: cursorNow, attempts: attempts + 1, lastAttempt: now });
|
|
153
153
|
emitEvent({
|
package/dist/hook-authoring.d.ts
CHANGED
|
@@ -6,6 +6,23 @@ export interface HookContext {
|
|
|
6
6
|
cwd: string;
|
|
7
7
|
providedParams: string[];
|
|
8
8
|
}
|
|
9
|
+
export interface LifecycleHookNode {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
mode: string;
|
|
14
|
+
lifecycle: string;
|
|
15
|
+
cwd: string;
|
|
16
|
+
nodeDir: string;
|
|
17
|
+
contextDir: string;
|
|
18
|
+
jobDir: string;
|
|
19
|
+
reportsDir: string;
|
|
20
|
+
}
|
|
21
|
+
export interface LifecycleHookRuntime {
|
|
22
|
+
isBirth: boolean;
|
|
23
|
+
canvasHome: string;
|
|
24
|
+
profile: string | null;
|
|
25
|
+
}
|
|
9
26
|
interface HookRequestBase {
|
|
10
27
|
protocolVersion: 1;
|
|
11
28
|
op: string;
|
|
@@ -26,7 +43,20 @@ export interface AfterHookRequest extends HookRequestBase {
|
|
|
26
43
|
export interface ReplaceHookRequest extends HookRequestBase {
|
|
27
44
|
phase: 'replace';
|
|
28
45
|
}
|
|
29
|
-
|
|
46
|
+
/** Lifecycle handlers run on every matching event. They must be idempotent because `node:start` fires for birth and every revive. */
|
|
47
|
+
export interface LifecycleHookRequest {
|
|
48
|
+
protocolVersion: 1;
|
|
49
|
+
op: string;
|
|
50
|
+
event: 'node:start';
|
|
51
|
+
phase: 'on';
|
|
52
|
+
operationId: string;
|
|
53
|
+
node: LifecycleHookNode;
|
|
54
|
+
runtime: LifecycleHookRuntime;
|
|
55
|
+
context: {
|
|
56
|
+
cwd: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export type HookRequest = BeforeHookRequest | AfterHookRequest | ReplaceHookRequest | LifecycleHookRequest;
|
|
30
60
|
export interface HookSuccess {
|
|
31
61
|
protocolVersion: 1;
|
|
32
62
|
ok: true;
|
package/dist/hook-authoring.js
CHANGED
|
@@ -239,18 +239,40 @@ function concatBytes(chunks, total) {
|
|
|
239
239
|
function validateRequest(value) {
|
|
240
240
|
if (!isRecord(value))
|
|
241
241
|
return invalid('request must be an object');
|
|
242
|
-
if (!hasOnlyKeys(value, ['protocolVersion', 'op', 'phase', 'command', 'operationId', 'input', 'context', 'result'])) {
|
|
243
|
-
return invalid('request contains unknown fields');
|
|
244
|
-
}
|
|
245
242
|
if (value['protocolVersion'] !== 1)
|
|
246
243
|
return invalid('unsupported hook protocol version');
|
|
247
244
|
if (typeof value['op'] !== 'string' || value['op'] === '')
|
|
248
245
|
return invalid('op must be a non-empty string');
|
|
249
|
-
if (!isStringArray(value['command']) || value['command'].length === 0)
|
|
250
|
-
return invalid('command must be a non-empty string array');
|
|
251
246
|
if (typeof value['operationId'] !== 'string' || !/^[0-9a-f]{32}$/.test(value['operationId'])) {
|
|
252
247
|
return invalid('operationId must be a 32-character lowercase hexadecimal string');
|
|
253
248
|
}
|
|
249
|
+
if (value['phase'] === 'on') {
|
|
250
|
+
if (!hasOnlyKeys(value, ['protocolVersion', 'op', 'event', 'phase', 'operationId', 'node', 'runtime', 'context']))
|
|
251
|
+
return invalid('lifecycle request contains unknown fields');
|
|
252
|
+
if (value['event'] !== 'node:start')
|
|
253
|
+
return invalid('unsupported lifecycle event');
|
|
254
|
+
if (!isLifecycleNode(value['node']))
|
|
255
|
+
return invalid('lifecycle request node contains invalid fields');
|
|
256
|
+
if (!isLifecycleRuntime(value['runtime']))
|
|
257
|
+
return invalid('lifecycle request runtime contains invalid fields');
|
|
258
|
+
if (!isLifecycleContext(value['context']))
|
|
259
|
+
return invalid('lifecycle request context must contain cwd');
|
|
260
|
+
return valid({
|
|
261
|
+
protocolVersion: 1,
|
|
262
|
+
op: value['op'],
|
|
263
|
+
event: 'node:start',
|
|
264
|
+
phase: 'on',
|
|
265
|
+
operationId: value['operationId'],
|
|
266
|
+
node: value['node'],
|
|
267
|
+
runtime: value['runtime'],
|
|
268
|
+
context: value['context'],
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (!hasOnlyKeys(value, ['protocolVersion', 'op', 'phase', 'command', 'operationId', 'input', 'context', 'result'])) {
|
|
272
|
+
return invalid('request contains unknown fields');
|
|
273
|
+
}
|
|
274
|
+
if (!isStringArray(value['command']) || value['command'].length === 0)
|
|
275
|
+
return invalid('command must be a non-empty string array');
|
|
254
276
|
if (!isRecord(value['input']) || !isJson(value['input']))
|
|
255
277
|
return invalid('input must be a JSON object');
|
|
256
278
|
if (!isContext(value['context']))
|
|
@@ -317,6 +339,21 @@ function isContext(value) {
|
|
|
317
339
|
&& typeof value['cwd'] === 'string'
|
|
318
340
|
&& isStringArray(value['providedParams']);
|
|
319
341
|
}
|
|
342
|
+
function isLifecycleContext(value) {
|
|
343
|
+
return isRecord(value) && hasOnlyKeys(value, ['cwd']) && typeof value['cwd'] === 'string';
|
|
344
|
+
}
|
|
345
|
+
function isLifecycleNode(value) {
|
|
346
|
+
return isRecord(value)
|
|
347
|
+
&& hasOnlyKeys(value, ['id', 'name', 'kind', 'mode', 'lifecycle', 'cwd', 'nodeDir', 'contextDir', 'jobDir', 'reportsDir'])
|
|
348
|
+
&& ['id', 'name', 'kind', 'mode', 'lifecycle', 'cwd', 'nodeDir', 'contextDir', 'jobDir', 'reportsDir'].every((field) => typeof value[field] === 'string');
|
|
349
|
+
}
|
|
350
|
+
function isLifecycleRuntime(value) {
|
|
351
|
+
return isRecord(value)
|
|
352
|
+
&& hasOnlyKeys(value, ['isBirth', 'canvasHome', 'profile'])
|
|
353
|
+
&& typeof value['isBirth'] === 'boolean'
|
|
354
|
+
&& typeof value['canvasHome'] === 'string'
|
|
355
|
+
&& (typeof value['profile'] === 'string' || value['profile'] === null);
|
|
356
|
+
}
|
|
320
357
|
function isHookError(value) {
|
|
321
358
|
return isRecord(value)
|
|
322
359
|
&& hasOnlyKeys(value, ['code', 'message', 'field', 'next'])
|