@north-light/crouter 0.3.230 → 0.3.232
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 +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/dto/canvas.d.ts +10 -0
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/health.d.ts +2 -1
- package/dist/api/dto/lifecycle.d.ts +3 -4
- package/dist/api/dto/messages.d.ts +5 -4
- package/dist/api/dto/nodes.d.ts +2 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +8 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/01-spine/00-has-manager.md +1 -1
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +5 -0
- package/dist/builtin-memory/02-turn-lifecycle/02-resident.md +5 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +2 -2
- package/dist/builtin-memory/insights/capture.md +3 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +6 -1
- package/dist/clients/attach/render/diagram.js +13 -5
- package/dist/clients/attach/render/page-block.d.ts +0 -1
- package/dist/clients/attach/render/page-block.js +4 -57
- package/dist/clients/attach/viewer.js +570 -563
- package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +9 -0
- package/dist/clients/inbox/__tests__/integration/mount-panel.test.js +62 -1
- package/dist/clients/inbox/controller.d.ts +10 -0
- package/dist/clients/inbox/controller.js +56 -12
- package/dist/clients/inbox/tui/input.js +38 -10
- package/dist/clients/inbox/tui/page-body.d.ts +10 -0
- package/dist/clients/inbox/tui/page-body.js +66 -0
- package/dist/clients/inbox/tui/panel.js +6 -4
- package/dist/clients/inbox/tui/render.js +89 -17
- package/dist/clients/inbox/tui/types.d.ts +4 -4
- package/dist/commands/__tests__/human.test.js +18 -3
- package/dist/commands/__tests__/node-message.test.js +3 -3
- package/dist/commands/api-client.js +1 -7
- package/dist/commands/canvas-config.js +6 -14
- package/dist/commands/canvas-use.js +4 -6
- package/dist/commands/cron.js +16 -22
- package/dist/commands/human/prompts.d.ts +1 -1
- package/dist/commands/human/prompts.js +118 -112
- package/dist/commands/human/request.js +13 -14
- package/dist/commands/human/review.js +3 -4
- package/dist/commands/human/shared.d.ts +6 -0
- package/dist/commands/human/shared.js +43 -5
- package/dist/commands/human.js +1 -1
- package/dist/commands/memory/delete.js +4 -6
- package/dist/commands/memory/edit.js +0 -4
- package/dist/commands/memory/move.js +3 -5
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +9 -5
- package/dist/commands/memory/write.js +60 -29
- package/dist/commands/memory.js +1 -1
- package/dist/commands/node/bash.js +6 -9
- package/dist/commands/node/create.js +89 -22
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +30 -27
- package/dist/commands/node/message.js +24 -45
- package/dist/commands/node/subscription.js +6 -15
- package/dist/commands/node/wait.js +2 -3
- package/dist/commands/node-lifecycle-revive.js +1 -12
- package/dist/commands/pkg/browse/actions.js +2 -3
- package/dist/commands/pkg/market-manage.js +2 -5
- package/dist/commands/pkg/plugin-manage.js +7 -8
- package/dist/commands/profile/default.js +5 -5
- package/dist/commands/profile/delete.js +1 -1
- package/dist/commands/profile/env.js +9 -15
- package/dist/commands/profile/kind.js +3 -7
- package/dist/commands/profile/meta.js +3 -5
- package/dist/commands/profile/new.js +0 -6
- package/dist/commands/profile/pause.js +4 -8
- package/dist/commands/profile/project.js +5 -9
- package/dist/commands/profile/rename.js +3 -7
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/profile.js +4 -3
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/config.js +3 -4
- package/dist/commands/sys/support/prepare.js +8 -4
- package/dist/commands/sys/support/submit.js +2 -3
- package/dist/commands/sys/sync-deps.js +1 -9
- package/dist/commands/sys/sync-project-guidance.js +1 -7
- package/dist/commands/sys/sync-skills.js +1 -11
- package/dist/core/__tests__/cron-node-sink-parked-root.test.d.ts +1 -0
- package/dist/core/__tests__/cron-node-sink-parked-root.test.js +147 -0
- package/dist/core/__tests__/history-inbox.test.js +11 -1
- package/dist/core/__tests__/human-deliver.test.js +2 -1
- package/dist/core/__tests__/integration/command-plugins.test.js +0 -1
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -1
- package/dist/core/__tests__/lifecycle.test.js +30 -2
- package/dist/core/__tests__/revive-parked-fresh.test.d.ts +1 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +109 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +32 -5
- package/dist/core/canvas/attention.d.ts +2 -0
- package/dist/core/canvas/attention.js +25 -18
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.js +20 -2
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command.js +33 -8
- package/dist/core/help.d.ts +28 -2
- package/dist/core/help.js +46 -10
- package/dist/core/human/__tests__/page-html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-html-markdown.test.js +48 -0
- package/dist/core/human/component-docs.js +4 -4
- package/dist/core/human/page-html-markdown.d.ts +8 -0
- package/dist/core/human/page-html-markdown.js +260 -0
- package/dist/core/memory/lint.d.ts +15 -0
- package/dist/core/memory/lint.js +150 -90
- package/dist/core/profiles/__tests__/fuzzy-match.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/fuzzy-match.test.js +51 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +14 -7
- package/dist/core/profiles/manifest.js +62 -12
- package/dist/core/profiles/select.d.ts +3 -1
- package/dist/core/profiles/select.js +5 -3
- package/dist/core/profiles/state-block.js +4 -3
- package/dist/core/runtime/boot-root.d.ts +3 -2
- package/dist/core/runtime/canvas-extensions.d.ts +7 -1
- package/dist/core/runtime/canvas-extensions.js +8 -1
- package/dist/core/runtime/lifecycle.d.ts +11 -2
- package/dist/core/runtime/lifecycle.js +15 -2
- package/dist/core/runtime/model-selection.d.ts +4 -0
- package/dist/core/runtime/model-selection.js +5 -0
- package/dist/core/runtime/nodes.js +5 -0
- package/dist/core/runtime/reopen.d.ts +6 -0
- package/dist/core/runtime/reopen.js +12 -1
- package/dist/core/runtime/revive.d.ts +6 -0
- package/dist/core/runtime/revive.js +22 -2
- package/dist/core/runtime/spawn.d.ts +5 -2
- package/dist/core/runtime/spawn.js +18 -32
- package/dist/core/runtime/structured-output.d.ts +6 -0
- package/dist/core/runtime/structured-output.js +6 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.js +89 -0
- package/dist/core/substrate/on-read.js +2 -1
- package/dist/core/substrate/surface-match.js +164 -12
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/api/__tests__/broker-settle-park.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +102 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +188 -0
- package/dist/daemon/api/__tests__/node-create-description.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +83 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.js +92 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +173 -0
- package/dist/daemon/api/handlers/broker-ops.js +21 -0
- package/dist/daemon/api/handlers/canvas.js +10 -0
- package/dist/daemon/api/handlers/messages.js +25 -16
- package/dist/daemon/api/handlers/nodes.js +5 -0
- package/dist/daemon/api/handlers/profiles.js +22 -1
- package/dist/daemon/cron-run.js +19 -1
- package/dist/daemon/manage.d.ts +16 -1
- package/dist/daemon/manage.js +20 -1
- package/dist/daemon/park-pending.d.ts +13 -0
- package/dist/daemon/park-pending.js +42 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +13 -0
- package/dist/daemon/reconcilers/broker-supervision.js +139 -21
- package/dist/daemon/reconcilers/live-obligation.d.ts +10 -4
- package/dist/daemon/reconcilers/live-obligation.js +7 -3
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +0 -4
- package/dist/daemon/reconcilers/storage-maintenance.js +1 -33
- package/dist/pi-extensions/canvas-prompt-scrub.d.ts +13 -0
- package/dist/pi-extensions/canvas-prompt-scrub.js +53 -0
- package/dist/shared/generated-context.d.ts +7 -0
- package/dist/shared/generated-context.js +11 -0
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +0 -47
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { after, before, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { closeDb } from '../../../core/canvas/db.js';
|
|
7
|
+
import { createNode, getNode } from '../../../core/canvas/canvas.js';
|
|
8
|
+
import { contextDir, jobDir, viewSocketPath } from '../../../core/canvas/paths.js';
|
|
9
|
+
import { recordFault } from '../../../core/runtime/fault.js';
|
|
10
|
+
import { headlessBrokerHost } from '../../../core/runtime/host.js';
|
|
11
|
+
import { handleCreate } from '../handlers/nodes.js';
|
|
12
|
+
import { canvasRoutes } from '../handlers/canvas.js';
|
|
13
|
+
let home = '';
|
|
14
|
+
const previousHome = process.env['CRTR_HOME'];
|
|
15
|
+
const brokers = [];
|
|
16
|
+
const launchedBrokers = new Map();
|
|
17
|
+
let originalLaunch;
|
|
18
|
+
before(() => {
|
|
19
|
+
home = mkdtempSync('/tmp/crtr-snapshot-fields-');
|
|
20
|
+
process.env['CRTR_HOME'] = home;
|
|
21
|
+
originalLaunch = headlessBrokerHost.launch;
|
|
22
|
+
headlessBrokerHost.launch = (nodeId) => {
|
|
23
|
+
const broker = createServer();
|
|
24
|
+
brokers.push(broker);
|
|
25
|
+
launchedBrokers.set(nodeId, broker);
|
|
26
|
+
broker.listen(viewSocketPath(nodeId));
|
|
27
|
+
return { pid: process.pid, exited: new Promise(() => { }) };
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
after(async () => {
|
|
31
|
+
headlessBrokerHost.launch = originalLaunch;
|
|
32
|
+
await Promise.all(brokers.map((broker) => new Promise((resolve) => {
|
|
33
|
+
if (broker.listening)
|
|
34
|
+
broker.close(() => resolve());
|
|
35
|
+
else
|
|
36
|
+
resolve();
|
|
37
|
+
})));
|
|
38
|
+
closeDb();
|
|
39
|
+
rmSync(home, { recursive: true, force: true });
|
|
40
|
+
if (previousHome === undefined)
|
|
41
|
+
delete process.env['CRTR_HOME'];
|
|
42
|
+
else
|
|
43
|
+
process.env['CRTR_HOME'] = previousHome;
|
|
44
|
+
});
|
|
45
|
+
function meta(nodeId, overrides = {}) {
|
|
46
|
+
return {
|
|
47
|
+
node_id: nodeId,
|
|
48
|
+
name: nodeId,
|
|
49
|
+
created: new Date().toISOString(),
|
|
50
|
+
cwd: home,
|
|
51
|
+
kind: 'general',
|
|
52
|
+
mode: 'base',
|
|
53
|
+
lifecycle: 'resident',
|
|
54
|
+
status: 'idle',
|
|
55
|
+
host_kind: null,
|
|
56
|
+
profile_id: null,
|
|
57
|
+
parent: null,
|
|
58
|
+
...overrides,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function request(method, path, body) {
|
|
62
|
+
return {
|
|
63
|
+
method,
|
|
64
|
+
path,
|
|
65
|
+
params: {},
|
|
66
|
+
query: new URLSearchParams(),
|
|
67
|
+
body,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function snapshotRoute() {
|
|
71
|
+
const route = canvasRoutes.find((entry) => entry.method === 'GET' && entry.pattern === '/v1/canvas/snapshot');
|
|
72
|
+
assert.ok(route, 'snapshot route exists');
|
|
73
|
+
return route;
|
|
74
|
+
}
|
|
75
|
+
/** A live broker is the pid + view socket pair used by isBrokerLive(). */
|
|
76
|
+
async function listenFor(nodeId) {
|
|
77
|
+
const broker = createServer();
|
|
78
|
+
brokers.push(broker);
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
broker.once('error', reject);
|
|
81
|
+
broker.listen(viewSocketPath(nodeId), resolve);
|
|
82
|
+
});
|
|
83
|
+
return broker;
|
|
84
|
+
}
|
|
85
|
+
function activeJob(nodeId) {
|
|
86
|
+
const id = `${Date.now()}-snapshot-test`;
|
|
87
|
+
const dir = join(contextDir(nodeId), 'jobs', id);
|
|
88
|
+
mkdirSync(dir, { recursive: true });
|
|
89
|
+
writeFileSync(join(dir, 'cmd.sh'), 'printf live');
|
|
90
|
+
writeFileSync(join(dir, 'job.bg'), '');
|
|
91
|
+
}
|
|
92
|
+
test('one canvas snapshot carries title, brief, jobs, busy, latch, fault, and controller facts', async () => {
|
|
93
|
+
// Use the public create request for the brief: A2's birth contract is the
|
|
94
|
+
// source of the description that A3 must expose without a follow-up read.
|
|
95
|
+
const created = await handleCreate(request('POST', '/v1/nodes', {
|
|
96
|
+
kind: 'general',
|
|
97
|
+
root: true,
|
|
98
|
+
profile: null,
|
|
99
|
+
cwd: home,
|
|
100
|
+
prompt: '',
|
|
101
|
+
name: 'Live outcome',
|
|
102
|
+
description: 'Review the live outcome',
|
|
103
|
+
}));
|
|
104
|
+
const liveJobId = created.body.node_id;
|
|
105
|
+
activeJob(liveJobId);
|
|
106
|
+
const busyId = 'snapshot-busy';
|
|
107
|
+
createNode(meta(busyId, {
|
|
108
|
+
name: 'Busy outcome',
|
|
109
|
+
parent: liveJobId,
|
|
110
|
+
host_kind: 'broker',
|
|
111
|
+
pi_pid: process.pid,
|
|
112
|
+
}));
|
|
113
|
+
writeFileSync(join(jobDir(busyId), 'busy'), '');
|
|
114
|
+
const finalId = 'snapshot-final';
|
|
115
|
+
const finalizedAt = new Date().toISOString();
|
|
116
|
+
createNode(meta(finalId, {
|
|
117
|
+
name: 'Final outcome',
|
|
118
|
+
parent: liveJobId,
|
|
119
|
+
lifecycle: 'terminal',
|
|
120
|
+
status: 'done',
|
|
121
|
+
final_report: 'final.md',
|
|
122
|
+
finalized_at: finalizedAt,
|
|
123
|
+
}));
|
|
124
|
+
const faultId = 'snapshot-fault';
|
|
125
|
+
createNode(meta(faultId, {
|
|
126
|
+
name: 'Fault outcome',
|
|
127
|
+
parent: liveJobId,
|
|
128
|
+
status: 'dead',
|
|
129
|
+
}));
|
|
130
|
+
recordFault(faultId, {
|
|
131
|
+
link: 'viewer↔broker',
|
|
132
|
+
op: 'snapshot-test',
|
|
133
|
+
kind: 'connection',
|
|
134
|
+
retry: { disposition: 'manual', by: 'client' },
|
|
135
|
+
message: 'forced snapshot fault',
|
|
136
|
+
});
|
|
137
|
+
const controllerId = 'snapshot-controller';
|
|
138
|
+
createNode(meta(controllerId, { name: 'Controller', parent: liveJobId }));
|
|
139
|
+
const waitingId = 'snapshot-waiting';
|
|
140
|
+
createNode(meta(waitingId, {
|
|
141
|
+
name: 'Waiting outcome',
|
|
142
|
+
parent: liveJobId,
|
|
143
|
+
waiting_for: controllerId,
|
|
144
|
+
}));
|
|
145
|
+
const liveBroker = launchedBrokers.get(liveJobId);
|
|
146
|
+
assert.ok(liveBroker, 'created node has a live broker');
|
|
147
|
+
const busyBroker = await listenFor(busyId);
|
|
148
|
+
try {
|
|
149
|
+
const result = await snapshotRoute().handler(request('GET', '/v1/canvas/snapshot'));
|
|
150
|
+
const body = result.body;
|
|
151
|
+
const byId = new Map(body.nodes.map((node) => [node['node_id'], node]));
|
|
152
|
+
const live = byId.get(liveJobId);
|
|
153
|
+
assert.ok(live);
|
|
154
|
+
assert.equal(live['title'], 'Live outcome');
|
|
155
|
+
assert.equal(live['description'], 'Review the live outcome');
|
|
156
|
+
assert.equal(live['name'], 'Live outcome Review the live outcome');
|
|
157
|
+
assert.equal(live['jobs_active'], 1);
|
|
158
|
+
assert.equal(live['streaming'], false);
|
|
159
|
+
assert.equal(live['waiting_for'], null);
|
|
160
|
+
const busy = byId.get(busyId);
|
|
161
|
+
assert.ok(busy);
|
|
162
|
+
assert.equal(busy['title'], 'Busy outcome');
|
|
163
|
+
assert.equal(busy['jobs_active'], 0);
|
|
164
|
+
assert.equal(busy['streaming'], true);
|
|
165
|
+
const final = byId.get(finalId);
|
|
166
|
+
assert.ok(final);
|
|
167
|
+
assert.equal(final['title'], 'Final outcome');
|
|
168
|
+
assert.equal(final['final_report'], 'final.md');
|
|
169
|
+
assert.equal(final['finalized_at'], finalizedAt);
|
|
170
|
+
const fault = byId.get(faultId);
|
|
171
|
+
assert.ok(fault);
|
|
172
|
+
const hanging = fault['hanging'];
|
|
173
|
+
assert.equal(hanging.link, 'viewer↔broker');
|
|
174
|
+
assert.equal(hanging.op, 'snapshot-test');
|
|
175
|
+
assert.equal(hanging.kind, 'connection');
|
|
176
|
+
assert.deepEqual(hanging.retry, { disposition: 'manual', by: 'client' });
|
|
177
|
+
assert.equal(hanging.message, 'forced snapshot fault');
|
|
178
|
+
const waiting = byId.get(waitingId);
|
|
179
|
+
assert.ok(waiting);
|
|
180
|
+
assert.equal(waiting['title'], 'Waiting outcome');
|
|
181
|
+
assert.equal(waiting['waiting_for'], controllerId);
|
|
182
|
+
assert.equal(waiting['jobs_active'], 0);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await Promise.all([liveBroker, busyBroker].map((broker) => new Promise((resolve) => broker.close(() => resolve()))));
|
|
186
|
+
}
|
|
187
|
+
assert.ok(getNode(liveJobId));
|
|
188
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { after, before, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { closeDb } from '../../../core/canvas/db.js';
|
|
7
|
+
import { getNode } from '../../../core/canvas/canvas.js';
|
|
8
|
+
import { nodeDir } from '../../../core/canvas/paths.js';
|
|
9
|
+
import { headlessBrokerHost } from '../../../core/runtime/host.js';
|
|
10
|
+
import { handleCreate, nodeRoutes } from '../handlers/nodes.js';
|
|
11
|
+
import { brokerOperationRoutes } from '../handlers/broker-ops.js';
|
|
12
|
+
let home = '';
|
|
13
|
+
let originalHome;
|
|
14
|
+
let originalLaunch;
|
|
15
|
+
const brokers = [];
|
|
16
|
+
before(() => {
|
|
17
|
+
originalHome = process.env['CRTR_HOME'];
|
|
18
|
+
// macOS Unix-domain sockets have a short path limit; keep the canvas home short.
|
|
19
|
+
home = mkdtempSync('/tmp/crtr-node-description-');
|
|
20
|
+
process.env['CRTR_HOME'] = home;
|
|
21
|
+
originalLaunch = headlessBrokerHost.launch;
|
|
22
|
+
headlessBrokerHost.launch = (nodeId) => {
|
|
23
|
+
const broker = createServer();
|
|
24
|
+
brokers.push(broker);
|
|
25
|
+
broker.listen(join(nodeDir(nodeId), 'view.sock'));
|
|
26
|
+
return { pid: process.pid, exited: new Promise(() => { }) };
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
after(async () => {
|
|
30
|
+
headlessBrokerHost.launch = originalLaunch;
|
|
31
|
+
await Promise.all(brokers.map((broker) => new Promise((resolve) => {
|
|
32
|
+
if (broker.listening)
|
|
33
|
+
broker.close(() => resolve());
|
|
34
|
+
else
|
|
35
|
+
resolve();
|
|
36
|
+
})));
|
|
37
|
+
closeDb();
|
|
38
|
+
rmSync(home, { recursive: true, force: true });
|
|
39
|
+
if (originalHome === undefined)
|
|
40
|
+
delete process.env['CRTR_HOME'];
|
|
41
|
+
else
|
|
42
|
+
process.env['CRTR_HOME'] = originalHome;
|
|
43
|
+
});
|
|
44
|
+
function context(method, path, id, body) {
|
|
45
|
+
return {
|
|
46
|
+
method,
|
|
47
|
+
path,
|
|
48
|
+
params: id === undefined ? {} : { id },
|
|
49
|
+
query: new URLSearchParams(),
|
|
50
|
+
body,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
test('node create stores caller description and automatic naming leaves it and name unchanged', async () => {
|
|
54
|
+
const created = await handleCreate(context('POST', '/v1/nodes', undefined, {
|
|
55
|
+
kind: 'general',
|
|
56
|
+
root: true,
|
|
57
|
+
profile: null,
|
|
58
|
+
cwd: home,
|
|
59
|
+
prompt: '',
|
|
60
|
+
name: 'Review worker',
|
|
61
|
+
description: 'Review the worker conversation',
|
|
62
|
+
}));
|
|
63
|
+
assert.equal(created.status, 201);
|
|
64
|
+
const nodeId = created.body.node_id;
|
|
65
|
+
const detailRoute = nodeRoutes.find((route) => route.method === 'GET' && route.pattern === '/v1/nodes/:id');
|
|
66
|
+
assert.ok(detailRoute);
|
|
67
|
+
const detail = await detailRoute.handler(context('GET', `/v1/nodes/${nodeId}`, nodeId));
|
|
68
|
+
assert.equal(detail.body.name, 'Review worker');
|
|
69
|
+
assert.equal(detail.body.description, 'Review the worker conversation');
|
|
70
|
+
const generatedNameRoute = brokerOperationRoutes.find((route) => route.method === 'POST' && route.pattern === '/v1/nodes/:id/broker/generated-name');
|
|
71
|
+
assert.ok(generatedNameRoute);
|
|
72
|
+
const generated = await generatedNameRoute.handler(context('POST', `/v1/nodes/${nodeId}/broker/generated-name`, nodeId, {
|
|
73
|
+
kind: 'initial',
|
|
74
|
+
description: 'Generated replacement',
|
|
75
|
+
icon: '',
|
|
76
|
+
}));
|
|
77
|
+
assert.deepEqual(generated.body, { applied: false });
|
|
78
|
+
assert.equal(getNode(nodeId)?.name, 'Review worker');
|
|
79
|
+
assert.equal(getNode(nodeId)?.description, 'Review the worker conversation');
|
|
80
|
+
const fetched = await detailRoute.handler(context('GET', `/v1/nodes/${nodeId}`, nodeId));
|
|
81
|
+
assert.equal(fetched.body.name, 'Review worker');
|
|
82
|
+
assert.equal(fetched.body.description, 'Review the worker conversation');
|
|
83
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { after, afterEach, beforeEach, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { routes } from '../../../api/index.js';
|
|
7
|
+
import { CrtrError } from '../../../core/errors.js';
|
|
8
|
+
import { closeDb } from '../../../core/canvas/db.js';
|
|
9
|
+
import { createProfile, loadProfileManifest } from '../../../core/profiles/manifest.js';
|
|
10
|
+
import { resetScopeCache } from '../../../core/scope.js';
|
|
11
|
+
import { profileRoutes } from '../handlers/profiles.js';
|
|
12
|
+
const previousCrtrHome = process.env['CRTR_HOME'];
|
|
13
|
+
const previousHome = process.env.HOME;
|
|
14
|
+
let home = '';
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
closeDb();
|
|
17
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-profile-metadata-home-'));
|
|
18
|
+
process.env.HOME = home;
|
|
19
|
+
process.env['CRTR_HOME'] = join(home, 'canvas');
|
|
20
|
+
resetScopeCache();
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
closeDb();
|
|
24
|
+
rmSync(home, { recursive: true, force: true });
|
|
25
|
+
resetScopeCache();
|
|
26
|
+
});
|
|
27
|
+
after(() => {
|
|
28
|
+
if (previousHome === undefined)
|
|
29
|
+
delete process.env.HOME;
|
|
30
|
+
else
|
|
31
|
+
process.env.HOME = previousHome;
|
|
32
|
+
if (previousCrtrHome === undefined)
|
|
33
|
+
delete process.env['CRTR_HOME'];
|
|
34
|
+
else
|
|
35
|
+
process.env['CRTR_HOME'] = previousCrtrHome;
|
|
36
|
+
resetScopeCache();
|
|
37
|
+
});
|
|
38
|
+
function route(method, pattern) {
|
|
39
|
+
const found = profileRoutes.find((entry) => entry.method === method && entry.pattern === pattern);
|
|
40
|
+
assert.ok(found, `missing ${method} ${pattern} route`);
|
|
41
|
+
return found;
|
|
42
|
+
}
|
|
43
|
+
function context(params, body) {
|
|
44
|
+
return {
|
|
45
|
+
method: 'PATCH',
|
|
46
|
+
path: routes.profileMetadata(params['name']),
|
|
47
|
+
params,
|
|
48
|
+
query: new URLSearchParams(),
|
|
49
|
+
body,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function invoke(entry, ctx) {
|
|
53
|
+
return entry.handler(ctx);
|
|
54
|
+
}
|
|
55
|
+
test('PATCH metadata updates an existing profile and GET returns the opaque value', async () => {
|
|
56
|
+
const profile = createProfile('worker profile');
|
|
57
|
+
assert.deepEqual(loadProfileManifest(profile.profileId).manifest.metadata, undefined);
|
|
58
|
+
const patch = route('PATCH', '/v1/profiles/:name/metadata');
|
|
59
|
+
const patched = await invoke(patch, context({ name: profile.manifest.name }, {
|
|
60
|
+
set: { northlight_default_node_id: 'nlw-applet-42' },
|
|
61
|
+
}));
|
|
62
|
+
assert.equal(patched.status, 200);
|
|
63
|
+
assert.deepEqual(patched.body.metadata, {
|
|
64
|
+
northlight_default_node_id: 'nlw-applet-42',
|
|
65
|
+
});
|
|
66
|
+
const get = route('GET', '/v1/profiles/:name');
|
|
67
|
+
const read = await invoke(get, context({ name: profile.manifest.name }, undefined));
|
|
68
|
+
assert.deepEqual(read.body.metadata, {
|
|
69
|
+
northlight_default_node_id: 'nlw-applet-42',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
test('PATCH metadata rejects an unknown profile and invalid metadata keys', async () => {
|
|
73
|
+
const patch = route('PATCH', '/v1/profiles/:name/metadata');
|
|
74
|
+
await assert.rejects(() => invoke(patch, context({ name: 'missing-profile' }, { set: { role: 'worker' } })), (error) => error instanceof CrtrError && error.code === 'not_found');
|
|
75
|
+
const profile = createProfile('metadata validation');
|
|
76
|
+
await assert.rejects(() => invoke(patch, context({ name: profile.manifest.name }, { set: { 'not a key': 'value' } })), (error) => error instanceof CrtrError && error.code === 'usage');
|
|
77
|
+
assert.deepEqual(loadProfileManifest(profile.profileId).manifest.metadata, undefined);
|
|
78
|
+
});
|
|
79
|
+
test('PUT remains create-only after a metadata PATCH', async () => {
|
|
80
|
+
const profile = createProfile('create-only profile');
|
|
81
|
+
const patch = route('PATCH', '/v1/profiles/:name/metadata');
|
|
82
|
+
await invoke(patch, context({ name: profile.manifest.name }, { set: { role: 'worker' } }));
|
|
83
|
+
const ensure = route('PUT', '/v1/profiles/:name');
|
|
84
|
+
const result = await invoke(ensure, {
|
|
85
|
+
...context({ name: profile.manifest.name }, { metadata: { role: 'changed' } }),
|
|
86
|
+
method: 'PUT',
|
|
87
|
+
path: routes.profile(profile.manifest.name),
|
|
88
|
+
});
|
|
89
|
+
assert.deepEqual(result.body.metadata, {
|
|
90
|
+
role: 'worker',
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { after, before, beforeEach, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { createServer } from 'node:net';
|
|
5
|
+
import { createNode, getNode, openDb } from '../../../core/canvas/index.js';
|
|
6
|
+
import { closeDb } from '../../../core/canvas/db.js';
|
|
7
|
+
import { viewSocketPath } from '../../../core/canvas/paths.js';
|
|
8
|
+
import { readInboxSince } from '../../../core/feed/inbox.js';
|
|
9
|
+
import { headlessBrokerHost } from '../../../core/runtime/host.js';
|
|
10
|
+
import { messageRoutes } from '../handlers/messages.js';
|
|
11
|
+
let home;
|
|
12
|
+
const priorHome = process.env['CRTR_HOME'];
|
|
13
|
+
before(() => {
|
|
14
|
+
// macOS limits Unix-socket paths to 104 bytes; the system temp directory's
|
|
15
|
+
// per-user prefix is already most of that budget.
|
|
16
|
+
home = mkdtempSync('/tmp/crtr-reopen-');
|
|
17
|
+
process.env['CRTR_HOME'] = home;
|
|
18
|
+
});
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
closeDb();
|
|
21
|
+
rmSync(home, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
after(() => {
|
|
24
|
+
closeDb();
|
|
25
|
+
rmSync(home, { recursive: true, force: true });
|
|
26
|
+
if (priorHome === undefined)
|
|
27
|
+
delete process.env['CRTR_HOME'];
|
|
28
|
+
else
|
|
29
|
+
process.env['CRTR_HOME'] = priorHome;
|
|
30
|
+
});
|
|
31
|
+
function node(id, lifecycle, status, piPid) {
|
|
32
|
+
return {
|
|
33
|
+
node_id: id,
|
|
34
|
+
name: id,
|
|
35
|
+
created: new Date().toISOString(),
|
|
36
|
+
cwd: '/tmp',
|
|
37
|
+
kind: 'general',
|
|
38
|
+
mode: 'base',
|
|
39
|
+
lifecycle,
|
|
40
|
+
status,
|
|
41
|
+
...(piPid !== undefined ? { pi_pid: piPid } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function finalize(id) {
|
|
45
|
+
openDb()
|
|
46
|
+
.prepare("UPDATE nodes SET final_report = ?, finalized_at = ? WHERE node_id = ?")
|
|
47
|
+
.run('final.md', new Date().toISOString(), id);
|
|
48
|
+
}
|
|
49
|
+
async function sendMessage(id, body) {
|
|
50
|
+
const ctx = {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
path: `/v1/nodes/${id}/messages`,
|
|
53
|
+
params: { id },
|
|
54
|
+
query: new URLSearchParams(),
|
|
55
|
+
body,
|
|
56
|
+
};
|
|
57
|
+
return messageRoutes[0].handler(ctx);
|
|
58
|
+
}
|
|
59
|
+
async function startLiveBroker(id, failDelivery = false) {
|
|
60
|
+
let observed = null;
|
|
61
|
+
const server = createServer((socket) => {
|
|
62
|
+
let pending = '';
|
|
63
|
+
socket.on('data', (chunk) => {
|
|
64
|
+
pending += chunk.toString('utf8');
|
|
65
|
+
let newline = pending.indexOf('\n');
|
|
66
|
+
while (newline !== -1) {
|
|
67
|
+
const line = pending.slice(0, newline);
|
|
68
|
+
pending = pending.slice(newline + 1);
|
|
69
|
+
const frame = JSON.parse(line);
|
|
70
|
+
if (frame.type === 'deliver') {
|
|
71
|
+
observed = getNode(id);
|
|
72
|
+
socket.write(`${JSON.stringify({
|
|
73
|
+
type: 'ack',
|
|
74
|
+
for: 'deliver',
|
|
75
|
+
id: frame.id,
|
|
76
|
+
ok: !failDelivery,
|
|
77
|
+
detail: failDelivery ? 'forced delivery failure' : 'prompt',
|
|
78
|
+
})}\n`);
|
|
79
|
+
}
|
|
80
|
+
newline = pending.indexOf('\n');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
await new Promise((resolve, reject) => {
|
|
85
|
+
server.once('error', reject);
|
|
86
|
+
server.listen(viewSocketPath(id), resolve);
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
close: () => closeServer(server),
|
|
90
|
+
deliveryState: () => observed,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function closeServer(server) {
|
|
94
|
+
return new Promise((resolve, reject) => server.close((err) => (err === undefined ? resolve() : reject(err))));
|
|
95
|
+
}
|
|
96
|
+
function assertReopened(id) {
|
|
97
|
+
const meta = getNode(id);
|
|
98
|
+
assert.equal(meta?.lifecycle, 'resident');
|
|
99
|
+
assert.equal(meta?.final_report, null);
|
|
100
|
+
}
|
|
101
|
+
test('reopen makes live terminal, finalized terminal, and parked targets resident and unlatched before delivery', async () => {
|
|
102
|
+
const liveId = 'live-terminal';
|
|
103
|
+
createNode(node(liveId, 'terminal', 'active', process.pid));
|
|
104
|
+
const liveBroker = await startLiveBroker(liveId);
|
|
105
|
+
try {
|
|
106
|
+
const result = await sendMessage(liveId, { body: 'continue live work', reopen: true, delivery: 'interactive' });
|
|
107
|
+
assert.equal(result.body.delivered_via, 'engine');
|
|
108
|
+
assertReopened(liveId);
|
|
109
|
+
assert.equal(liveBroker.deliveryState()?.lifecycle, 'resident', 'engine delivery sees resident lifecycle');
|
|
110
|
+
assert.equal(liveBroker.deliveryState()?.final_report, null, 'engine delivery sees no final latch');
|
|
111
|
+
assert.equal(readInboxSince(liveId).length, 0, 'live delivery does not append an inbox entry');
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await liveBroker.close();
|
|
115
|
+
}
|
|
116
|
+
const originalLaunch = headlessBrokerHost.launch;
|
|
117
|
+
headlessBrokerHost.launch = (() => ({ pid: 0x7ffffffe, exited: Promise.resolve({ code: 0, signal: null }) }));
|
|
118
|
+
try {
|
|
119
|
+
const finalizedId = 'finalized-terminal';
|
|
120
|
+
createNode(node(finalizedId, 'terminal', 'done'));
|
|
121
|
+
finalize(finalizedId);
|
|
122
|
+
await sendMessage(finalizedId, { body: 'new mandate', reopen: true });
|
|
123
|
+
assertReopened(finalizedId);
|
|
124
|
+
assert.equal(readInboxSince(finalizedId).length, 1, 'finalized target receives its durable message after reopening');
|
|
125
|
+
const parkedId = 'parked-resident';
|
|
126
|
+
createNode(node(parkedId, 'resident', 'done'));
|
|
127
|
+
await sendMessage(parkedId, { body: 'resume the parked session', reopen: true });
|
|
128
|
+
assertReopened(parkedId);
|
|
129
|
+
assert.equal(readInboxSince(parkedId).length, 1, 'parked target receives its durable message after reopening');
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
headlessBrokerHost.launch = originalLaunch;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
test('a reopen commit failure delivers nothing', async () => {
|
|
136
|
+
const id = 'commit-failure';
|
|
137
|
+
createNode(node(id, 'terminal', 'done'));
|
|
138
|
+
finalize(id);
|
|
139
|
+
const finalReport = getNode(id)?.final_report;
|
|
140
|
+
openDb().exec(`
|
|
141
|
+
CREATE TRIGGER fail_reopen_latch_clear
|
|
142
|
+
BEFORE UPDATE OF final_report ON nodes
|
|
143
|
+
WHEN NEW.final_report IS NULL
|
|
144
|
+
BEGIN
|
|
145
|
+
SELECT RAISE(FAIL, 'forced reopen failure');
|
|
146
|
+
END;
|
|
147
|
+
`);
|
|
148
|
+
try {
|
|
149
|
+
await assert.rejects(() => sendMessage(id, { body: 'must not deliver', reopen: true }), /forced reopen failure/);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
openDb().exec('DROP TRIGGER IF EXISTS fail_reopen_latch_clear');
|
|
153
|
+
}
|
|
154
|
+
assert.equal(getNode(id)?.lifecycle, 'resident', 'lifecycle commit precedes the failed latch CAS');
|
|
155
|
+
assert.equal(getNode(id)?.final_report, finalReport, 'a failed CAS preserves the canonical-final pointer');
|
|
156
|
+
assert.equal(readInboxSince(id).length, 0, 'no inbox entry precedes a failed reopen commit');
|
|
157
|
+
});
|
|
158
|
+
test('a delivery failure after reopen leaves the target resident, unlatched, and without a message', async () => {
|
|
159
|
+
const id = 'delivery-failure';
|
|
160
|
+
createNode(node(id, 'terminal', 'done', process.pid));
|
|
161
|
+
finalize(id);
|
|
162
|
+
const broker = await startLiveBroker(id, true);
|
|
163
|
+
try {
|
|
164
|
+
await assert.rejects(() => sendMessage(id, { body: 'broker rejects this', reopen: true, delivery: 'interactive' }), /forced delivery failure/);
|
|
165
|
+
assertReopened(id);
|
|
166
|
+
assert.equal(broker.deliveryState()?.lifecycle, 'resident', 'failed engine delivery happened after reopen committed');
|
|
167
|
+
assert.equal(broker.deliveryState()?.final_report, null, 'failed engine delivery saw the cleared latch');
|
|
168
|
+
assert.equal(readInboxSince(id).length, 0, 'failed engine delivery creates no durable fallback message');
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
await broker.close();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
@@ -16,6 +16,8 @@ import { evaluateStop } from '../../../core/runtime/stop-guard.js';
|
|
|
16
16
|
import { clearFault } from '../../../core/runtime/fault.js';
|
|
17
17
|
import { updateModelRecipe } from '../../../core/runtime/model-swap.js';
|
|
18
18
|
import { completeCompanionRequestedSubmit } from '../../review/finish.js';
|
|
19
|
+
import { consumePendingPark } from '../../park-pending.js';
|
|
20
|
+
import { parkEvent } from '../../reconcilers/live-obligation.js';
|
|
19
21
|
function objectBody(body, operation) {
|
|
20
22
|
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
21
23
|
throw usage(`${operation} requires a JSON body`);
|
|
@@ -229,6 +231,11 @@ function hasUnseenNonDeferredInbox(nodeId) {
|
|
|
229
231
|
* boundary. This handler intentionally never invokes the broker that called it:
|
|
230
232
|
* the returned directive is the only Pi-side consequence. */
|
|
231
233
|
function settleBroker(nodeId, request) {
|
|
234
|
+
// A parking turn in flight ends HERE, whatever it ended as. Consuming
|
|
235
|
+
// unconditionally is the abort rule expressed once: every early return below
|
|
236
|
+
// — clean quit, pushed final, human ask, interrupted turn, refresh — discards
|
|
237
|
+
// the park and leaves the node live under a restarted clock.
|
|
238
|
+
const parkPending = consumePendingPark(nodeId);
|
|
232
239
|
const node = getNode(nodeId);
|
|
233
240
|
if (node === null) {
|
|
234
241
|
// A command may have recorded its Pi-local completion marker just as an
|
|
@@ -274,6 +281,20 @@ function settleBroker(nodeId, request) {
|
|
|
274
281
|
clearFault(nodeId);
|
|
275
282
|
return { action: 'shutdown' };
|
|
276
283
|
}
|
|
284
|
+
// The parking turn just ended. Re-evaluate the verdict the clock reached
|
|
285
|
+
// before this turn ran: a `crtr human send`, new wake-worthy mail, a fresh
|
|
286
|
+
// subscription to a child it spawned, or a pending structured-output request
|
|
287
|
+
// all make the node live work again, and the ordinary stop policy applies
|
|
288
|
+
// instead. `pushedFinal` was checked above, so a node that finished during
|
|
289
|
+
// its parking turn ends latched rather than parked.
|
|
290
|
+
if (parkPending && parkEvent(nodeId, node.lifecycle) === 'park') {
|
|
291
|
+
clearFault(nodeId);
|
|
292
|
+
transition(nodeId, 'park');
|
|
293
|
+
if (focusOf(nodeId) !== null)
|
|
294
|
+
tearDownNode(nodeId);
|
|
295
|
+
setPresence(nodeId, { pane: null, tmux_session: null, window: null });
|
|
296
|
+
return { action: 'shutdown' };
|
|
297
|
+
}
|
|
277
298
|
const decision = evaluateStop(nodeId, signals, request.backgroundJobsRunning);
|
|
278
299
|
clearFault(nodeId);
|
|
279
300
|
if (decision.action === 'reprompt')
|
|
@@ -13,6 +13,9 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { listNodes, nowIso, ticketsAcrossCanvas } from '../../../index.js';
|
|
15
15
|
import { getNode, pruneNodes, pruneToLimit, rebuildIndex, rosterEdges, rosterNodes, subscriptionsOf } from '../../../core/canvas/canvas.js';
|
|
16
|
+
import { activeBackgroundBashJobs } from '../../../core/bash-jobs.js';
|
|
17
|
+
import { contextDir } from '../../../core/canvas/paths.js';
|
|
18
|
+
import { isBrokerLive } from '../../../core/runtime/model-swap.js';
|
|
16
19
|
import { dashboardRowsAll, enrichRows } from '../../../core/canvas/render.js';
|
|
17
20
|
import { ticketCountsForNodes } from '../../../core/canvas/attention.js';
|
|
18
21
|
import { buildCorpus, resolveRef, } from '../../../core/canvas/history.js';
|
|
@@ -26,6 +29,7 @@ function handleAttention() {
|
|
|
26
29
|
name: t.name,
|
|
27
30
|
cwd: t.cwd,
|
|
28
31
|
count: t.count,
|
|
32
|
+
subjects: t.subjects,
|
|
29
33
|
})),
|
|
30
34
|
};
|
|
31
35
|
return { status: 200, body };
|
|
@@ -273,6 +277,8 @@ function handleSnapshot() {
|
|
|
273
277
|
return {
|
|
274
278
|
node_id: row.node_id,
|
|
275
279
|
name: row.name,
|
|
280
|
+
title: meta?.name ?? row.name,
|
|
281
|
+
description: meta?.description ?? null,
|
|
276
282
|
kind: row.kind,
|
|
277
283
|
mode: row.mode,
|
|
278
284
|
lifecycle: row.lifecycle ?? meta?.lifecycle,
|
|
@@ -288,6 +294,10 @@ function handleSnapshot() {
|
|
|
288
294
|
...(row.mtimeMs !== undefined ? { last_activity: new Date(row.mtimeMs).toISOString() } : {}),
|
|
289
295
|
ctx_tokens: row.ctx_tokens,
|
|
290
296
|
streaming: row.streaming ?? false,
|
|
297
|
+
jobs_active: meta !== null && isBrokerLive(meta)
|
|
298
|
+
? activeBackgroundBashJobs(contextDir(row.node_id)).length
|
|
299
|
+
: 0,
|
|
300
|
+
waiting_for: meta?.waiting_for ?? null,
|
|
291
301
|
hanging: (row.hanging ?? null),
|
|
292
302
|
viewed: row.viewed ?? false,
|
|
293
303
|
final_report: meta?.final_report ?? null,
|