@north-light/crouter 0.3.231 → 0.3.233
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-memory/internal/memory-loading.md +4 -3
- 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 +11 -7
- 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/__tests__/surface-match-pre-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-pre-command.test.js +92 -0
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/injected-store.d.ts +6 -0
- package/dist/core/substrate/injected-store.js +24 -0
- package/dist/core/substrate/on-read.d.ts +17 -1
- package/dist/core/substrate/on-read.js +38 -3
- package/dist/core/substrate/schema.d.ts +3 -3
- package/dist/core/substrate/schema.js +4 -4
- package/dist/core/substrate/surface-match.d.ts +19 -0
- package/dist/core/substrate/surface-match.js +224 -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/__tests__/pre-command-gate.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/pre-command-gate.test.js +220 -0
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +14 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +75 -2
- 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,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,
|
|
@@ -8,13 +8,12 @@ import { deliverLive, interruptLive } from '../../../core/runtime/interactive-de
|
|
|
8
8
|
import { cancelPendingHumanMessages } from '../../../core/feed/inbox.js';
|
|
9
9
|
import { isProfilePaused } from '../../../core/profiles/manifest.js';
|
|
10
10
|
import { withFreshTerminalGuard } from '../../../core/canvas/canvas.js';
|
|
11
|
-
import { assertNotFinalized, assertFinalizedForReopen, commitReopen } from '../../../core/runtime/reopen.js';
|
|
11
|
+
import { assertNotFinalized, assertFinalizedForReopen, commitReopen, commitReopenResident, } from '../../../core/runtime/reopen.js';
|
|
12
12
|
import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
|
|
13
13
|
import { appendSituationalContext, formatSituationalProse } from '../../../core/runtime/situational-context.js';
|
|
14
14
|
import { formatDataCard } from '../../../shared/generated-context.js';
|
|
15
15
|
import { hasNoNaturalCycle } from '../../../core/runtime/revive-all.js';
|
|
16
|
-
import {
|
|
17
|
-
import { readRoadmap } from '../../../core/runtime/roadmap.js';
|
|
16
|
+
import { hasFreshGroundState } from '../../../core/runtime/revive.js';
|
|
18
17
|
import { notFound, usage } from '../../../core/errors.js';
|
|
19
18
|
import { InputError } from '../../../core/io.js';
|
|
20
19
|
import { ApiError } from '../../../api/index.js';
|
|
@@ -137,8 +136,8 @@ function parseSendBody(body) {
|
|
|
137
136
|
// Runtime cards ARE accepted here: a card-bearing send is an ordinary human
|
|
138
137
|
// send that happens to carry context, and the live deliver frame places the
|
|
139
138
|
// cards ahead of the body in one turn (D11).
|
|
140
|
-
if (fresh || out.
|
|
141
|
-
throw usage('interactive delivery supports a body
|
|
139
|
+
if (fresh || out.situational_context !== undefined || out.output_schema !== undefined) {
|
|
140
|
+
throw usage('interactive delivery supports a body, runtime cards, and reopen only (no fresh, situational_context, or output_schema)');
|
|
142
141
|
}
|
|
143
142
|
if (!hasBody)
|
|
144
143
|
throw usage('interactive delivery requires a non-empty body');
|
|
@@ -179,11 +178,7 @@ function deferredNoNaturalCycle(id, status) {
|
|
|
179
178
|
* would resume amnesiac. Rejected fail-loud (422) BEFORE any revive/arm — the
|
|
180
179
|
* CLI's old pure pre-check, relocated behind the API so `--fresh` parity holds. */
|
|
181
180
|
function assertRecoverableForFresh(id) {
|
|
182
|
-
|
|
183
|
-
if (goal !== null && goal.trim() !== '')
|
|
184
|
-
return;
|
|
185
|
-
const roadmap = readRoadmap(id);
|
|
186
|
-
if (roadmap !== null && roadmap.trim() !== '')
|
|
181
|
+
if (hasFreshGroundState(id))
|
|
187
182
|
return;
|
|
188
183
|
// ApiError (not a plain literal) so its message survives `toErrorBody`.
|
|
189
184
|
throw new ApiError(422, 'unprocessable', `node ${id} has no goal/roadmap on disk — a fresh revive would resume amnesiac.`);
|
|
@@ -236,6 +231,13 @@ async function handleMessage(ctx) {
|
|
|
236
231
|
};
|
|
237
232
|
return { status: 201, body };
|
|
238
233
|
}
|
|
234
|
+
// An interactive reopen commits before even choosing its engine-or-inbox
|
|
235
|
+
// route. A live transport race then falls back with this same committed state.
|
|
236
|
+
let reopenCommitted = false;
|
|
237
|
+
if (req.delivery === 'interactive' && req.reopen) {
|
|
238
|
+
commitReopenResident(id);
|
|
239
|
+
reopenCommitted = true;
|
|
240
|
+
}
|
|
239
241
|
// --- Interactive delivery: route via the LIVE broker's serialized frame
|
|
240
242
|
// loop (broker routes idle→prompt / streaming→steer itself) instead of
|
|
241
243
|
// the durable inbox — no watcher latency, and send/stop share one
|
|
@@ -261,19 +263,19 @@ async function handleMessage(ctx) {
|
|
|
261
263
|
// fall through to the durable inbox path
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
|
-
// --- Immediate delivery (spec §7.7) —
|
|
265
|
-
// reopen
|
|
266
|
-
//
|
|
266
|
+
// --- Immediate delivery (spec §7.7) — pre-delivery side effects first,
|
|
267
|
+
// then the reopen commit, then exactly one inbox append. Once the commit
|
|
268
|
+
// succeeds, an append failure makes no further state change. ---
|
|
267
269
|
const deliverImmediate = () => {
|
|
268
|
-
const expectedFinalReport = req.reopen ? assertFinalizedForReopen(id) : null;
|
|
269
270
|
if (!req.reopen)
|
|
270
271
|
assertNotFinalized(id);
|
|
271
272
|
if (schema !== null)
|
|
272
273
|
writeOutputSchema(id, 'oneoff', schema);
|
|
273
274
|
if (hasSidecar)
|
|
274
275
|
appendSituationalContext(id, sidecar);
|
|
275
|
-
if (req.reopen)
|
|
276
|
-
|
|
276
|
+
if (req.reopen && !reopenCommitted) {
|
|
277
|
+
commitReopenResident(id);
|
|
278
|
+
}
|
|
277
279
|
const cards = oneShotCards.length > 0 ? { cards: oneShotCards } : {};
|
|
278
280
|
if (hasBody || schema !== null) {
|
|
279
281
|
const messageBody = hasBody
|
|
@@ -302,6 +304,13 @@ async function handleMessage(ctx) {
|
|
|
302
304
|
// cycle ahead of it, so a deferred entry would sit unseen forever — reject
|
|
303
305
|
// fail-loud BEFORE any side effect, atomically with the append (the read and
|
|
304
306
|
// the write share one canvas write-lock). Non-deferred tiers deliver directly.
|
|
307
|
+
//
|
|
308
|
+
// `--reopen` does NOT exempt a target from this guard. It commits the target
|
|
309
|
+
// resident and retracts the finalization latch, but it never moves `status`
|
|
310
|
+
// out of `done` — so a reopened target is still one `hasNoNaturalCycle` says
|
|
311
|
+
// nothing will ever bring back on its own, and a deferred entry appended to it
|
|
312
|
+
// would wait forever. Reopen widens the WAKE-CAPABLE tiers to a finalized
|
|
313
|
+
// target; deferred stays rejected, and the rejection names those tiers.
|
|
305
314
|
let entry;
|
|
306
315
|
if (tier === 'deferred') {
|
|
307
316
|
const outcome = withFreshTerminalGuard(id, hasNoNaturalCycle, deliverImmediate);
|
|
@@ -88,6 +88,9 @@ function parseCreateBody(body) {
|
|
|
88
88
|
if (b['kind'] !== undefined && (typeof b['kind'] !== 'string' || b['kind'].trim() === '')) {
|
|
89
89
|
throw usage('kind must be a non-empty string when provided');
|
|
90
90
|
}
|
|
91
|
+
if (b['description'] !== undefined && (typeof b['description'] !== 'string' || b['description'].trim() === '')) {
|
|
92
|
+
throw usage('description must be a non-empty string when provided');
|
|
93
|
+
}
|
|
91
94
|
const rootLifecycle = b['root_lifecycle'];
|
|
92
95
|
if (rootLifecycle !== undefined &&
|
|
93
96
|
rootLifecycle !== 'resident' &&
|
|
@@ -110,6 +113,8 @@ function buildRecipe(req, parent, cwd) {
|
|
|
110
113
|
recipe.mode = req.mode;
|
|
111
114
|
if (req.name !== undefined)
|
|
112
115
|
recipe.name = req.name;
|
|
116
|
+
if (req.description !== undefined)
|
|
117
|
+
recipe.description = req.description;
|
|
113
118
|
if (parent !== undefined)
|
|
114
119
|
recipe.parent = parent;
|
|
115
120
|
if (req.root === true)
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// GET /v1/profiles/:name resolve one profile by name or id
|
|
8
8
|
// DELETE /v1/profiles/:name required-force cascade or detach
|
|
9
9
|
import { assertInstalledKind } from '../../../core/config.js';
|
|
10
|
-
import {
|
|
10
|
+
import { usage } from '../../../core/errors.js';
|
|
11
|
+
import { assertProfileProjects, createProfile, ensureRootProfile, listProfiles, loadProfileManifest, updateProfileMetadata, } from '../../../core/profiles/manifest.js';
|
|
11
12
|
import { requestProfileDeletion } from '../../profile-delete.js';
|
|
12
13
|
import { toProfileDTO } from '../map.js';
|
|
13
14
|
const ROOT_PROFILE_NAME = 'root';
|
|
@@ -42,6 +43,25 @@ function handleEnsure(ctx) {
|
|
|
42
43
|
});
|
|
43
44
|
return { status: 200, body: toProfileDTO(created) };
|
|
44
45
|
}
|
|
46
|
+
/** `PATCH /v1/profiles/:name/metadata` — merge and remove metadata entries
|
|
47
|
+
* on an existing profile. Resolution accepts the profile id or unique name,
|
|
48
|
+
* matching the read and delete routes; this is deliberately separate from the
|
|
49
|
+
* create-only ensure route. */
|
|
50
|
+
function handleSetMetadata(ctx) {
|
|
51
|
+
const profile = loadProfileManifest(ctx.params['name']);
|
|
52
|
+
const raw = ctx.body ?? {};
|
|
53
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
54
|
+
throw usage('profile metadata body must be an object', { field: 'body' });
|
|
55
|
+
}
|
|
56
|
+
const req = raw;
|
|
57
|
+
const unset = req.unset === undefined ? [] : req.unset;
|
|
58
|
+
if (!Array.isArray(unset) || unset.some((key) => typeof key !== 'string')) {
|
|
59
|
+
throw usage('profile metadata `unset` must be an array of strings', { field: 'unset' });
|
|
60
|
+
}
|
|
61
|
+
const set = req.set === undefined ? {} : req.set;
|
|
62
|
+
const updated = updateProfileMetadata(profile.profileId, set, unset);
|
|
63
|
+
return { status: 200, body: toProfileDTO(updated) };
|
|
64
|
+
}
|
|
45
65
|
/** `GET /v1/profiles` — every profile with a readable manifest. */
|
|
46
66
|
function handleList() {
|
|
47
67
|
return { status: 200, body: listProfiles().map(toProfileDTO) };
|
|
@@ -61,6 +81,7 @@ async function handleDelete(ctx) {
|
|
|
61
81
|
}
|
|
62
82
|
export const profileRoutes = [
|
|
63
83
|
{ method: 'PUT', pattern: '/v1/profiles/:name', handler: handleEnsure },
|
|
84
|
+
{ method: 'PATCH', pattern: '/v1/profiles/:name/metadata', handler: handleSetMetadata },
|
|
64
85
|
{ method: 'DELETE', pattern: '/v1/profiles/:name', handler: handleDelete },
|
|
65
86
|
{ method: 'GET', pattern: '/v1/profiles', handler: handleList },
|
|
66
87
|
{ method: 'GET', pattern: '/v1/profiles/:name', handler: handleGet },
|
package/dist/daemon/cron-run.js
CHANGED
|
@@ -44,6 +44,9 @@ import { appendInbox } from '../core/feed/inbox.js';
|
|
|
44
44
|
import { emitEvent } from '../core/events/emit.js';
|
|
45
45
|
import { loadProfileManifest } from '../core/profiles/manifest.js';
|
|
46
46
|
import { hasNoNaturalCycle } from '../core/runtime/revive-all.js';
|
|
47
|
+
import { isParked } from '../core/runtime/lifecycle.js';
|
|
48
|
+
import { reviveNode } from '../core/runtime/revive.js';
|
|
49
|
+
import { isBrokerLive } from '../core/runtime/model-swap.js';
|
|
47
50
|
import { spawnChild } from '../core/runtime/spawn.js';
|
|
48
51
|
import { cronWakeOrigin } from '../core/runtime/bearings.js';
|
|
49
52
|
import { nextSlotAfter, cadenceDisplay } from '../core/wake.js';
|
|
@@ -181,7 +184,12 @@ function deliverNodeSink(c, target, stdout) {
|
|
|
181
184
|
const meta = getNode(target);
|
|
182
185
|
if (meta === null)
|
|
183
186
|
throw new Error(`node sink ${target} is gone`);
|
|
184
|
-
|
|
187
|
+
// A node parked by the unattended clock is written `done` with the park
|
|
188
|
+
// marker. Its cron is an explicit next cycle, unlike mass revival or deferred
|
|
189
|
+
// delivery, so accept precisely that parked shape. A final latch, a
|
|
190
|
+
// cancellation, or a node finished any other way remains undeliverable and
|
|
191
|
+
// takes the ordinary failure path.
|
|
192
|
+
if (hasNoNaturalCycle(meta) && !isParked(meta)) {
|
|
185
193
|
throw new Error(`node sink ${target} has no natural cycle ahead (done or finalized)`);
|
|
186
194
|
}
|
|
187
195
|
const from = c.created_by != null && getNode(c.created_by) !== null ? c.created_by : null;
|
|
@@ -192,6 +200,16 @@ function deliverNodeSink(c, target, stdout) {
|
|
|
192
200
|
label: `⏰ cron ${c.name}`,
|
|
193
201
|
data: { body: stdout },
|
|
194
202
|
});
|
|
203
|
+
// Mirror ordinary durable message delivery: the append is authoritative, and
|
|
204
|
+
// a wake failure must not turn that successful delivery into a cron failure.
|
|
205
|
+
if (c.tier !== 'deferred' && !isBrokerLive(meta)) {
|
|
206
|
+
try {
|
|
207
|
+
reviveNode(target, { resume: true });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
/* best-effort wake — the entry is durably appended regardless */
|
|
211
|
+
}
|
|
212
|
+
}
|
|
195
213
|
}
|
|
196
214
|
/** Birth a fresh node with stdout as its kickoff (the recommended sink for
|
|
197
215
|
* jobs meant to outlive their creator). Returns the newborn's node id. */
|
package/dist/daemon/manage.d.ts
CHANGED
|
@@ -99,7 +99,22 @@ export declare function stopDaemonProcess(pid: number, timeoutMs?: number, deps?
|
|
|
99
99
|
* If the daemon is already running, returns {started:false, existing_pid}.
|
|
100
100
|
* If spawning fails (e.g. missing dist — run `npm run build` first), throws. */
|
|
101
101
|
export declare function spawnDaemon(): Promise<SpawnDaemonResult>;
|
|
102
|
-
/**
|
|
102
|
+
/** Whether autostart is suppressed for this invocation. `--no-autostart` or
|
|
103
|
+
* `CRTR_NO_DAEMON_AUTOSTART=1` forces fail-loud on a cold socket (spec §7.1) —
|
|
104
|
+
* for environments that manage crtrd externally and want the diagnostic, and
|
|
105
|
+
* for test lanes, which point `CRTR_HOME` at a throwaway directory: an
|
|
106
|
+
* autostarted daemon there is detached and resident, so it outlives both the
|
|
107
|
+
* test process and the deleted temp home with nothing left to stop it.
|
|
108
|
+
*
|
|
109
|
+
* This is the ONE gate. `ensureDaemon` below is the only path that spawns a
|
|
110
|
+
* daemon implicitly, so the check belongs here rather than at each caller —
|
|
111
|
+
* `cliClient()` reads it too, but the in-process spawn/recycle/promote callers
|
|
112
|
+
* reach `ensureDaemon` without going through the CLI client at all. An
|
|
113
|
+
* explicit `crtr sys daemon start` calls `spawnDaemon` directly and is
|
|
114
|
+
* deliberately unaffected. */
|
|
115
|
+
export declare function autostartDisabled(): boolean;
|
|
116
|
+
/** Start the daemon if it is not already running. No-op if already up, or if
|
|
117
|
+
* autostart is suppressed for this invocation.
|
|
103
118
|
* Silently swallows spawn errors (the canvas still works without the daemon;
|
|
104
119
|
* nodes just won't be auto-revived). */
|
|
105
120
|
export declare function ensureDaemon(): void;
|
package/dist/daemon/manage.js
CHANGED
|
@@ -379,10 +379,29 @@ export async function spawnDaemon() {
|
|
|
379
379
|
// ---------------------------------------------------------------------------
|
|
380
380
|
// ensureDaemon — fire-and-forget front-door helper
|
|
381
381
|
// ---------------------------------------------------------------------------
|
|
382
|
-
/**
|
|
382
|
+
/** Whether autostart is suppressed for this invocation. `--no-autostart` or
|
|
383
|
+
* `CRTR_NO_DAEMON_AUTOSTART=1` forces fail-loud on a cold socket (spec §7.1) —
|
|
384
|
+
* for environments that manage crtrd externally and want the diagnostic, and
|
|
385
|
+
* for test lanes, which point `CRTR_HOME` at a throwaway directory: an
|
|
386
|
+
* autostarted daemon there is detached and resident, so it outlives both the
|
|
387
|
+
* test process and the deleted temp home with nothing left to stop it.
|
|
388
|
+
*
|
|
389
|
+
* This is the ONE gate. `ensureDaemon` below is the only path that spawns a
|
|
390
|
+
* daemon implicitly, so the check belongs here rather than at each caller —
|
|
391
|
+
* `cliClient()` reads it too, but the in-process spawn/recycle/promote callers
|
|
392
|
+
* reach `ensureDaemon` without going through the CLI client at all. An
|
|
393
|
+
* explicit `crtr sys daemon start` calls `spawnDaemon` directly and is
|
|
394
|
+
* deliberately unaffected. */
|
|
395
|
+
export function autostartDisabled() {
|
|
396
|
+
return process.env['CRTR_NO_DAEMON_AUTOSTART'] === '1' || process.argv.includes('--no-autostart');
|
|
397
|
+
}
|
|
398
|
+
/** Start the daemon if it is not already running. No-op if already up, or if
|
|
399
|
+
* autostart is suppressed for this invocation.
|
|
383
400
|
* Silently swallows spawn errors (the canvas still works without the daemon;
|
|
384
401
|
* nodes just won't be auto-revived). */
|
|
385
402
|
export function ensureDaemon() {
|
|
403
|
+
if (autostartDisabled())
|
|
404
|
+
return;
|
|
386
405
|
if (!isDaemonRunning()) {
|
|
387
406
|
void spawnDaemon().catch(() => {
|
|
388
407
|
// Intentionally silent — a missing dist/daemon/crtrd-cli.js (dev mode,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Arm a pending park for `nodeId`. The caller delivers the parking prompt. */
|
|
2
|
+
export declare function beginPendingPark(nodeId: string, now: number): void;
|
|
3
|
+
export declare function isPendingPark(nodeId: string): boolean;
|
|
4
|
+
/** When the pending park was armed, or undefined when none is armed. */
|
|
5
|
+
export declare function pendingParkStartedAt(nodeId: string): number | undefined;
|
|
6
|
+
/** Delete-and-report: true only for the caller that actually took the pending
|
|
7
|
+
* park. Both the supervision tick and the settle handler run on the daemon
|
|
8
|
+
* thread with no await between their check and this call, so exactly one of
|
|
9
|
+
* them ever enacts a given park. */
|
|
10
|
+
export declare function consumePendingPark(nodeId: string): boolean;
|
|
11
|
+
/** Every node with a parking turn in flight — the reconciler's sweep drops the
|
|
12
|
+
* ones whose broker has left the fleet. */
|
|
13
|
+
export declare function pendingParkIds(): string[];
|