@north-light/crouter 0.3.214 → 0.3.216
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/dto/nodes.d.ts +2 -1
- package/dist/api/dto/profiles.d.ts +13 -1
- package/dist/builtin-memory/00-runtime-base.md +12 -11
- package/dist/builtin-memory/01-spine/00-has-manager.md +1 -11
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +4 -10
- package/dist/builtin-memory/04-orchestration-kernel.md +9 -35
- package/dist/builtin-memory/05-kinds/advisor/{00-base.md → advice-contract.md} +1 -1
- package/dist/builtin-memory/05-kinds/design/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/developer/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/general/00-base.md +2 -0
- package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/{00-base.md → lens-contract.md} +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -1
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -3
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/review/security-findings.md +12 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -1
- package/dist/clients/attach/__tests__/context-message.test.js +28 -1
- package/dist/clients/attach/chrome/canvas-panels.d.ts +0 -4
- package/dist/clients/attach/chrome/canvas-panels.js +5 -7
- package/dist/clients/attach/chrome/inbox-strip.d.ts +31 -0
- package/dist/clients/attach/chrome/inbox-strip.js +187 -0
- package/dist/clients/attach/chrome/roster.d.ts +6 -2
- package/dist/clients/attach/chrome/roster.js +13 -52
- package/dist/clients/attach/chrome/ticket-panel.d.ts +29 -0
- package/dist/clients/attach/chrome/ticket-panel.js +236 -0
- package/dist/clients/attach/render/card-presentation.d.ts +8 -3
- package/dist/clients/attach/render/card-presentation.js +48 -10
- package/dist/clients/attach/render/context-message.d.ts +5 -0
- package/dist/clients/attach/render/context-message.js +20 -14
- package/dist/clients/attach/session/context.d.ts +3 -0
- package/dist/clients/attach/session/frame.d.ts +4 -0
- package/dist/clients/attach/session/frame.js +8 -3
- package/dist/clients/attach/session/keys.d.ts +7 -0
- package/dist/clients/attach/session/keys.js +7 -0
- package/dist/clients/attach/session/layout.js +6 -3
- package/dist/clients/attach/session/pane-focus.d.ts +8 -0
- package/dist/clients/attach/session/pane-focus.js +42 -0
- package/dist/clients/attach/viewer.js +793 -789
- package/dist/clients/inbox/controller.d.ts +9 -0
- package/dist/clients/inbox/controller.js +57 -2
- package/dist/clients/inbox/surface.d.ts +2 -0
- package/dist/clients/inbox/surface.js +18 -3
- package/dist/clients/inbox/tui.d.ts +2 -0
- package/dist/clients/inbox/tui.js +11 -1
- package/dist/commands/node/create.js +3 -3
- package/dist/commands/profile/kind.d.ts +1 -0
- package/dist/commands/profile/kind.js +39 -0
- package/dist/commands/profile/list.js +5 -1
- package/dist/commands/profile/meta.d.ts +4 -0
- package/dist/commands/profile/meta.js +69 -0
- package/dist/commands/profile/new.js +34 -4
- package/dist/commands/profile/pause.d.ts +2 -0
- package/dist/commands/profile/pause.js +60 -0
- package/dist/commands/profile/show.js +10 -2
- package/dist/commands/profile.js +5 -8
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +40 -0
- package/dist/core/__tests__/parse-argv-stdin-secret.test.js +28 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +7 -0
- package/dist/core/command.js +23 -23
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +15 -0
- package/dist/core/help.d.ts +1 -1
- package/dist/core/keybindings/attach-control.d.ts +3 -0
- package/dist/core/keybindings/attach-control.js +1 -0
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/profiles/deletion-reservation.d.ts +7 -3
- package/dist/core/profiles/deletion-reservation.js +9 -5
- package/dist/core/profiles/manifest.d.ts +39 -1
- package/dist/core/profiles/manifest.js +171 -4
- package/dist/core/runtime/boot-root.js +3 -3
- package/dist/core/runtime/close.js +10 -5
- package/dist/core/runtime/revive.js +2 -0
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +17 -1
- package/dist/core/termrender/termrender.d.ts +7 -2
- package/dist/core/termrender/termrender.js +11 -6
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/daemon/api/__tests__/profile-launch-gates.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +111 -0
- package/dist/daemon/api/handlers/messages.js +9 -2
- package/dist/daemon/api/handlers/nodes.d.ts +2 -0
- package/dist/daemon/api/handlers/nodes.js +15 -8
- package/dist/daemon/api/handlers/profiles.js +12 -2
- package/dist/daemon/api/map.js +4 -0
- package/dist/daemon/fleet.js +1 -6
- package/dist/daemon/reconcilers/broker-supervision.js +15 -25
- package/dist/daemon/reconcilers/dormant-inbox.js +10 -10
- package/dist/daemon/reconcilers/live-obligation.d.ts +5 -1
- package/dist/daemon/reconcilers/live-obligation.js +11 -19
- package/dist/pi-extensions/canvas-inbox-watcher.js +15 -0
- package/dist/types.d.ts +9 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/01-spine/01-no-manager.md +0 -11
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +0 -8
|
@@ -46,11 +46,16 @@ import { appendPassive } from '../feed/passive.js';
|
|
|
46
46
|
* crash path (revive.ts). */
|
|
47
47
|
export function fanDoctrineWake(fromId, subscribers, label, data) {
|
|
48
48
|
for (const sub of subscribers) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
try {
|
|
50
|
+
const notice = { from: fromId, tier: 'normal', kind: 'message', label, data };
|
|
51
|
+
if (sub.active)
|
|
52
|
+
appendInbox(sub.node_id, notice);
|
|
53
|
+
else
|
|
54
|
+
appendPassive(sub.node_id, notice);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* one unavailable subscriber never blocks the rest */
|
|
58
|
+
}
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
/** The set of nodes to close: the root plus every descendant reachable down the
|
|
@@ -36,6 +36,7 @@ import { clearFault, beginBootFaultAttempt } from './fault.js';
|
|
|
36
36
|
import { clearInjectedDocs } from '../substrate/injected-store.js';
|
|
37
37
|
import { rootOfSpine } from './nodes.js';
|
|
38
38
|
import { isReviewCompanionBound } from '../review/companion.js';
|
|
39
|
+
import { assertProfileActive } from '../profiles/manifest.js';
|
|
39
40
|
// ---------------------------------------------------------------------------
|
|
40
41
|
// resumeArgs — which session source a revive resumes from
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
@@ -108,6 +109,7 @@ export function reviveNode(nodeId, opts) {
|
|
|
108
109
|
if (meta.final_report !== null) {
|
|
109
110
|
throw new Error(`reviveNode: refusing to revive ${nodeId} — its finalization latch is set (final_report=${meta.final_report}). A final landed between the caller's reopen-gate check and this revive; re-run with --reopen if this retask is still wanted.`);
|
|
110
111
|
}
|
|
112
|
+
assertProfileActive(meta.profile_id);
|
|
111
113
|
// Double-launch guard: a fleet entry IS liveness (design D-1). reviveNode
|
|
112
114
|
// runs fully SYNCHRONOUSLY on the daemon thread — this check, the launch,
|
|
113
115
|
// and the fleet registration inside `headlessBrokerHost.launch` complete
|
|
@@ -22,6 +22,12 @@ export declare function buildOperationalEnvBase(opts: {
|
|
|
22
22
|
targetProfileId: string | null;
|
|
23
23
|
host?: NodeJS.ProcessEnv;
|
|
24
24
|
}): NodeJS.ProcessEnv;
|
|
25
|
+
/** Source F — the TARGET profile's manifest `metadata`, each entry surfaced
|
|
26
|
+
* as `CRTR_PROFILE_META_<KEY>` (key uppercased, every non-alphanumeric run
|
|
27
|
+
* → `_`). Identity facts rather than secrets — unlike the env store these
|
|
28
|
+
* are readable back (`profile show`, ProfileDTO). Read straight off the
|
|
29
|
+
* manifest at every launch; never throws (a bad profile id yields `{}`). */
|
|
30
|
+
export declare function profileMetadataEnv(profileId: string | null): NodeJS.ProcessEnv;
|
|
25
31
|
/** The one authoritative broker child env: sources A–C (the operational
|
|
26
32
|
* base, resolved from the TARGET node's own cwd/profile — `inv.env`'s
|
|
27
33
|
* `CRTR_NODE_CWD`/`CRTR_PROFILE_ID`, never this process's ambient
|
|
@@ -30,7 +36,9 @@ export declare function buildOperationalEnvBase(opts: {
|
|
|
30
36
|
* own `profile env` store (`readProfileEnvVars`, `core/profiles/env-store.ts`)
|
|
31
37
|
* read directly off disk and injected regardless of the host env or any
|
|
32
38
|
* `spawnEnv.allow` entry (setting a value there IS the consent to cross this
|
|
33
|
-
* boundary),
|
|
39
|
+
* boundary), F — the TARGET profile's manifest metadata as
|
|
40
|
+
* `CRTR_PROFILE_META_*` (`profileMetadataEnv` above), plus D — the trusted,
|
|
41
|
+
* crtr-constructed `inv.env` overlay and the
|
|
34
42
|
* fork-bomb recursion guard `FRONT_DOOR_ENV=1`. D is layered last so crtr's
|
|
35
43
|
* own constructed env always wins a name collision with a stored profile
|
|
36
44
|
* value. Every broker launch (front-door root, managed child, `--root`,
|
|
@@ -33,6 +33,7 @@ import { statSync } from 'node:fs';
|
|
|
33
33
|
import { delimiter, resolve } from 'node:path';
|
|
34
34
|
import { readMergedLaunchConfig } from '../config.js';
|
|
35
35
|
import { readProfileEnvVars } from '../profiles/env-store.js';
|
|
36
|
+
import { metadataEnvName, readProfileMetadata } from '../profiles/manifest.js';
|
|
36
37
|
import { binShimDirFor } from './bin-contributions.js';
|
|
37
38
|
import { FRONT_DOOR_ENV } from './front-door-env.js';
|
|
38
39
|
/** A1 — literal env-var names copied verbatim (no value inspection): a login
|
|
@@ -211,6 +212,18 @@ export function buildOperationalEnvBase(opts) {
|
|
|
211
212
|
}
|
|
212
213
|
return out;
|
|
213
214
|
}
|
|
215
|
+
/** Source F — the TARGET profile's manifest `metadata`, each entry surfaced
|
|
216
|
+
* as `CRTR_PROFILE_META_<KEY>` (key uppercased, every non-alphanumeric run
|
|
217
|
+
* → `_`). Identity facts rather than secrets — unlike the env store these
|
|
218
|
+
* are readable back (`profile show`, ProfileDTO). Read straight off the
|
|
219
|
+
* manifest at every launch; never throws (a bad profile id yields `{}`). */
|
|
220
|
+
export function profileMetadataEnv(profileId) {
|
|
221
|
+
const out = {};
|
|
222
|
+
for (const [key, value] of Object.entries(readProfileMetadata(profileId))) {
|
|
223
|
+
out[metadataEnvName(key)] = value;
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
214
227
|
/** The one authoritative broker child env: sources A–C (the operational
|
|
215
228
|
* base, resolved from the TARGET node's own cwd/profile — `inv.env`'s
|
|
216
229
|
* `CRTR_NODE_CWD`/`CRTR_PROFILE_ID`, never this process's ambient
|
|
@@ -219,7 +232,9 @@ export function buildOperationalEnvBase(opts) {
|
|
|
219
232
|
* own `profile env` store (`readProfileEnvVars`, `core/profiles/env-store.ts`)
|
|
220
233
|
* read directly off disk and injected regardless of the host env or any
|
|
221
234
|
* `spawnEnv.allow` entry (setting a value there IS the consent to cross this
|
|
222
|
-
* boundary),
|
|
235
|
+
* boundary), F — the TARGET profile's manifest metadata as
|
|
236
|
+
* `CRTR_PROFILE_META_*` (`profileMetadataEnv` above), plus D — the trusted,
|
|
237
|
+
* crtr-constructed `inv.env` overlay and the
|
|
223
238
|
* fork-bomb recursion guard `FRONT_DOOR_ENV=1`. D is layered last so crtr's
|
|
224
239
|
* own constructed env always wins a name collision with a stored profile
|
|
225
240
|
* value. Every broker launch (front-door root, managed child, `--root`,
|
|
@@ -231,6 +246,7 @@ export function buildBrokerEnv(inv) {
|
|
|
231
246
|
const operational = buildOperationalEnvBase({ targetCwd, targetProfileId });
|
|
232
247
|
return {
|
|
233
248
|
...operational,
|
|
249
|
+
...profileMetadataEnv(targetProfileId),
|
|
234
250
|
...readProfileEnvVars(targetProfileId),
|
|
235
251
|
...inv.env,
|
|
236
252
|
[ADMITTED_HOST_PATH_ENV]: operational[ADMITTED_HOST_PATH_ENV],
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
export declare function ensureRenderer(): void;
|
|
16
16
|
/** Cheap predicate — true when the pinned managed binary is verified ready. Does not install or spawn. */
|
|
17
17
|
export declare function isRendererReady(): boolean;
|
|
18
|
-
/** Render markdown to terminal lines via the pinned binary; plaintext fallback.
|
|
18
|
+
/** Render markdown to terminal lines via the pinned binary; plaintext fallback.
|
|
19
|
+
* Callers own the returned array — every return is a fresh copy, so a caller
|
|
20
|
+
* that appends to it cannot grow the cached render on the next hit. */
|
|
19
21
|
export declare function renderMarkdown(md: string, width: number): string[];
|
|
20
22
|
export interface RenderedDoc {
|
|
21
23
|
/** Rendered ANSI rows — byte-identical to the plain `doc render` output. */
|
|
@@ -50,7 +52,10 @@ export declare function renderMarkdownWithMap(md: string, width: number): Render
|
|
|
50
52
|
* unchanged.
|
|
51
53
|
*/
|
|
52
54
|
export declare function renderMarkdownBlockAware(md: string, proseWidth: number, paneWidth: number): RenderedDoc;
|
|
53
|
-
/** Block-aware render as plain rows, for surfaces that do not anchor.
|
|
55
|
+
/** Block-aware render as plain rows, for surfaces that do not anchor. The rows
|
|
56
|
+
* are the caller's own array: the block-aware render can pass the mapped
|
|
57
|
+
* cache's line array straight through, and a caller appending to it would
|
|
58
|
+
* otherwise poison every later render of the same document. */
|
|
54
59
|
export declare function renderMarkdownBlockAwareLines(md: string, proseWidth: number, paneWidth: number): string[];
|
|
55
60
|
/** Validate markdown via `termrender doc check`. */
|
|
56
61
|
export declare function checkMarkdown(md: string): {
|
|
@@ -410,12 +410,14 @@ class BoundedLRUCache {
|
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
412
|
const _bodyCache = new BoundedLRUCache(20);
|
|
413
|
-
/** Render markdown to terminal lines via the pinned binary; plaintext fallback.
|
|
413
|
+
/** Render markdown to terminal lines via the pinned binary; plaintext fallback.
|
|
414
|
+
* Callers own the returned array — every return is a fresh copy, so a caller
|
|
415
|
+
* that appends to it cannot grow the cached render on the next hit. */
|
|
414
416
|
export function renderMarkdown(md, width) {
|
|
415
417
|
const key = `${md}\0${width}`;
|
|
416
418
|
const cached = _bodyCache.get(key);
|
|
417
419
|
if (cached)
|
|
418
|
-
return cached;
|
|
420
|
+
return [...cached];
|
|
419
421
|
ensureRenderer();
|
|
420
422
|
if (rendererState === 'ready') {
|
|
421
423
|
try {
|
|
@@ -429,7 +431,7 @@ export function renderMarkdown(md, width) {
|
|
|
429
431
|
if (lines.length > 0 && lines[lines.length - 1] === '')
|
|
430
432
|
lines.pop();
|
|
431
433
|
_bodyCache.set(key, lines);
|
|
432
|
-
return lines;
|
|
434
|
+
return [...lines];
|
|
433
435
|
}
|
|
434
436
|
catch (err) {
|
|
435
437
|
// `doc render` is best-effort, so a non-timeout failure implicates the
|
|
@@ -441,7 +443,7 @@ export function renderMarkdown(md, width) {
|
|
|
441
443
|
}
|
|
442
444
|
const fallback = wrap(sanitize(md), width);
|
|
443
445
|
_bodyCache.set(key, fallback);
|
|
444
|
-
return fallback;
|
|
446
|
+
return [...fallback];
|
|
445
447
|
}
|
|
446
448
|
/** Block types allowed to use the full pane width instead of the prose cap:
|
|
447
449
|
* a diagram is a picture, not prose, so a readability column limit only
|
|
@@ -657,9 +659,12 @@ export function renderMarkdownBlockAware(md, proseWidth, paneWidth) {
|
|
|
657
659
|
}
|
|
658
660
|
return { lines, rows, spans, blocks: base.blocks };
|
|
659
661
|
}
|
|
660
|
-
/** Block-aware render as plain rows, for surfaces that do not anchor.
|
|
662
|
+
/** Block-aware render as plain rows, for surfaces that do not anchor. The rows
|
|
663
|
+
* are the caller's own array: the block-aware render can pass the mapped
|
|
664
|
+
* cache's line array straight through, and a caller appending to it would
|
|
665
|
+
* otherwise poison every later render of the same document. */
|
|
661
666
|
export function renderMarkdownBlockAwareLines(md, proseWidth, paneWidth) {
|
|
662
|
-
return renderMarkdownBlockAware(md, proseWidth, paneWidth).lines;
|
|
667
|
+
return [...renderMarkdownBlockAware(md, proseWidth, paneWidth).lines];
|
|
663
668
|
}
|
|
664
669
|
/** Validate markdown via `termrender doc check`. */
|
|
665
670
|
export function checkMarkdown(md) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const TERMRENDER_VERSION = "4.12.
|
|
1
|
+
export declare const TERMRENDER_VERSION = "4.12.8";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const TERMRENDER_VERSION = '4.12.
|
|
1
|
+
export const TERMRENDER_VERSION = '4.12.8';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { after, afterEach, beforeEach, test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { createNode, getNode, listNodes } from '../../../core/canvas/canvas.js';
|
|
7
|
+
import { closeDb } from '../../../core/canvas/db.js';
|
|
8
|
+
import { CrtrError } from '../../../core/errors.js';
|
|
9
|
+
import { createProfile, loadProfileManifest, pauseProfile, setProfileDefaultKind } from '../../../core/profiles/manifest.js';
|
|
10
|
+
import { resetScopeCache } from '../../../core/scope.js';
|
|
11
|
+
import { reviveNode } from '../../../core/runtime/revive.js';
|
|
12
|
+
import { spawnNode } from '../../../core/runtime/nodes.js';
|
|
13
|
+
import { handleCreate, resolveCreateKind } from '../handlers/nodes.js';
|
|
14
|
+
const previousCrtrHome = process.env['CRTR_HOME'];
|
|
15
|
+
const previousHome = process.env.HOME;
|
|
16
|
+
let home = '';
|
|
17
|
+
let cwd = '';
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
closeDb();
|
|
20
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-profile-launch-home-'));
|
|
21
|
+
cwd = mkdtempSync(join(tmpdir(), 'crtr-profile-launch-cwd-'));
|
|
22
|
+
process.env.HOME = home;
|
|
23
|
+
process.env['CRTR_HOME'] = join(home, 'canvas');
|
|
24
|
+
mkdirSync(process.env['CRTR_HOME'], { recursive: true });
|
|
25
|
+
resetScopeCache();
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
closeDb();
|
|
29
|
+
rmSync(home, { recursive: true, force: true });
|
|
30
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
31
|
+
resetScopeCache();
|
|
32
|
+
});
|
|
33
|
+
after(() => {
|
|
34
|
+
if (previousHome === undefined)
|
|
35
|
+
delete process.env.HOME;
|
|
36
|
+
else
|
|
37
|
+
process.env.HOME = previousHome;
|
|
38
|
+
if (previousCrtrHome === undefined)
|
|
39
|
+
delete process.env['CRTR_HOME'];
|
|
40
|
+
else
|
|
41
|
+
process.env['CRTR_HOME'] = previousCrtrHome;
|
|
42
|
+
resetScopeCache();
|
|
43
|
+
});
|
|
44
|
+
test('create rejects a paused profile before a node exists', async () => {
|
|
45
|
+
const profile = createProfile('paused profile', [cwd]);
|
|
46
|
+
pauseProfile(profile.profileId);
|
|
47
|
+
const ctx = {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
path: '/v1/nodes',
|
|
50
|
+
params: {},
|
|
51
|
+
query: new URLSearchParams(),
|
|
52
|
+
body: { root: true, profile: profile.profileId, cwd, prompt: 'must not launch' },
|
|
53
|
+
};
|
|
54
|
+
const beforeRows = listNodes().length;
|
|
55
|
+
await assert.rejects(() => handleCreate(ctx), (error) => {
|
|
56
|
+
assert.ok(error instanceof CrtrError);
|
|
57
|
+
assert.equal(error.code, 'usage');
|
|
58
|
+
assert.match(error.message, /paused profile/);
|
|
59
|
+
assert.match(error.details?.['next'], new RegExp(`crtr profile resume ${profile.profileId}`));
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
assert.equal(listNodes().length, beforeRows);
|
|
63
|
+
});
|
|
64
|
+
test('revive refuses a paused profile before launching a broker', () => {
|
|
65
|
+
const profile = createProfile('paused revive', [cwd]);
|
|
66
|
+
pauseProfile(profile.profileId);
|
|
67
|
+
const nodeId = 'paused-revive-node';
|
|
68
|
+
createNode({
|
|
69
|
+
node_id: nodeId,
|
|
70
|
+
name: nodeId,
|
|
71
|
+
created: new Date().toISOString(),
|
|
72
|
+
cwd,
|
|
73
|
+
kind: 'developer',
|
|
74
|
+
mode: 'base',
|
|
75
|
+
lifecycle: 'resident',
|
|
76
|
+
status: 'active',
|
|
77
|
+
parent: null,
|
|
78
|
+
profile_id: profile.profileId,
|
|
79
|
+
});
|
|
80
|
+
assert.throws(() => reviveNode(nodeId, { resume: true }), (error) => {
|
|
81
|
+
assert.ok(error instanceof CrtrError);
|
|
82
|
+
assert.equal(error.code, 'usage');
|
|
83
|
+
assert.match(error.details?.['next'], new RegExp(`crtr profile resume ${profile.profileId}`));
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
assert.equal(getNode(nodeId)?.status, 'active');
|
|
87
|
+
});
|
|
88
|
+
// The API create gate phrases the refusal; this one is what every OTHER birth
|
|
89
|
+
// (fork, cron spawn, warm-pool mint, `/new` relaunch, recycle) actually hits,
|
|
90
|
+
// because they all reach the canvas through spawnNode and none of them call
|
|
91
|
+
// handleCreate.
|
|
92
|
+
test('the birth gate refuses a paused profile for a daemon-side spawn', () => {
|
|
93
|
+
const profile = createProfile('paused spawn', [cwd]);
|
|
94
|
+
pauseProfile(profile.profileId);
|
|
95
|
+
const beforeRows = listNodes().length;
|
|
96
|
+
assert.throws(() => spawnNode({ kind: 'general', cwd, parent: null, profile_id: profile.profileId }), (error) => {
|
|
97
|
+
assert.ok(error instanceof CrtrError);
|
|
98
|
+
assert.equal(error.code, 'usage');
|
|
99
|
+
assert.match(error.details?.['next'], new RegExp(`crtr profile resume ${profile.profileId}`));
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
assert.equal(listNodes().length, beforeRows);
|
|
103
|
+
});
|
|
104
|
+
test('create kind resolves from the profile default when omitted', () => {
|
|
105
|
+
const profile = createProfile('default kind', [cwd]);
|
|
106
|
+
setProfileDefaultKind(profile.profileId, 'developer');
|
|
107
|
+
const entry = loadProfileManifest(profile.profileId);
|
|
108
|
+
assert.equal(resolveCreateKind(undefined, entry), 'developer');
|
|
109
|
+
assert.equal(resolveCreateKind('general', entry), 'general');
|
|
110
|
+
assert.equal(resolveCreateKind(undefined, null), 'general');
|
|
111
|
+
});
|
|
@@ -6,6 +6,7 @@ import { BrokerUnreachableError } from '../../../core/runtime/broker-request.js'
|
|
|
6
6
|
import { isBrokerLive } from '../../../core/runtime/model-swap.js';
|
|
7
7
|
import { deliverLive, interruptLive } from '../../../core/runtime/interactive-deliver.js';
|
|
8
8
|
import { cancelPendingHumanMessages } from '../../../core/feed/inbox.js';
|
|
9
|
+
import { isProfilePaused } from '../../../core/profiles/manifest.js';
|
|
9
10
|
import { withFreshTerminalGuard } from '../../../core/canvas/canvas.js';
|
|
10
11
|
import { assertNotFinalized, assertFinalizedForReopen, commitReopen } from '../../../core/runtime/reopen.js';
|
|
11
12
|
import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
|
|
@@ -194,6 +195,12 @@ async function handleMessage(ctx) {
|
|
|
194
195
|
throw notFound(`unknown node: ${id}`, { received: id });
|
|
195
196
|
const req = parseSendBody(ctx.body);
|
|
196
197
|
const tier = (req.tier ?? 'normal');
|
|
198
|
+
// A paused profile takes no new work through either delivery route: the
|
|
199
|
+
// interactive route hands the body to the live broker instead of queueing it,
|
|
200
|
+
// and a wake is a revive by another name. So a paused target always takes the
|
|
201
|
+
// durable path and its entry waits, at every tier, for `crtr profile resume`
|
|
202
|
+
// (the live broker's watcher holds it; a dormant one is not woken).
|
|
203
|
+
const paused = isProfilePaused(meta.profile_id);
|
|
197
204
|
const from = req.from ?? null;
|
|
198
205
|
const hasBody = req.body.trim() !== '';
|
|
199
206
|
// One sidecar slot: the caller's rendered card, or prose rendered as one.
|
|
@@ -235,7 +242,7 @@ async function handleMessage(ctx) {
|
|
|
235
242
|
// ordering. A mid-revive race (liveness said live, connection refused)
|
|
236
243
|
// falls back to the durable path below; a genuinely dormant target skips
|
|
237
244
|
// straight there (append + revive, watcher delivers post-boot). ---
|
|
238
|
-
if (req.delivery === 'interactive' && isBrokerLive(meta)) {
|
|
245
|
+
if (req.delivery === 'interactive' && !paused && isBrokerLive(meta)) {
|
|
239
246
|
try {
|
|
240
247
|
if (hasSidecar)
|
|
241
248
|
appendSituationalContext(id, sidecar);
|
|
@@ -310,7 +317,7 @@ async function handleMessage(ctx) {
|
|
|
310
317
|
// A wake-capable tier revives a dormant target so its inbox-watcher delivers
|
|
311
318
|
// this; deferred never wakes — it rides the target's next natural cycle.
|
|
312
319
|
let revived = false;
|
|
313
|
-
if (tier !== 'deferred' && !isBrokerLive(meta)) {
|
|
320
|
+
if (tier !== 'deferred' && !paused && !isBrokerLive(meta)) {
|
|
314
321
|
try {
|
|
315
322
|
const result = reviveNode(id, { resume: true });
|
|
316
323
|
revived = result.launch !== undefined;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type ModelRegistryLike } from '../../../core/model-routes.js';
|
|
2
|
+
import { assertProfileActive } from '../../../core/profiles/manifest.js';
|
|
2
3
|
import type { HandlerResult, RequestContext, RouteTable } from '../router.js';
|
|
3
4
|
interface NodeHandlerDeps {
|
|
4
5
|
modelRegistry?: ModelRegistryLike;
|
|
5
6
|
}
|
|
7
|
+
export declare function resolveCreateKind(kind: string | undefined, profile: ReturnType<typeof assertProfileActive>): string;
|
|
6
8
|
export declare function handleCreate(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
7
9
|
export declare function handlePromote(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
8
10
|
export declare function handleYield(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
|
|
@@ -32,7 +32,7 @@ import { BrokerUnreachableError } from '../../../core/runtime/broker-request.js'
|
|
|
32
32
|
import { isBrokerLive, persistDormantModel, setModelLive } from '../../../core/runtime/model-swap.js';
|
|
33
33
|
import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
|
|
34
34
|
import { claimWarmNode, refillWarmPool, warmPoolEnabled } from '../../../core/runtime/warm-pool.js';
|
|
35
|
-
import { profileHome } from '../../../core/profiles/manifest.js';
|
|
35
|
+
import { assertProfileActive, profileHome } from '../../../core/profiles/manifest.js';
|
|
36
36
|
import { fullName } from '../../../core/canvas/labels.js';
|
|
37
37
|
import { getCron } from '../../../core/canvas/crons.js';
|
|
38
38
|
import { cronWakeOrigin } from '../../../core/runtime/bearings.js';
|
|
@@ -81,11 +81,11 @@ function countFiles(id) {
|
|
|
81
81
|
/** Validate + narrow the create body (spec §6.2). */
|
|
82
82
|
function parseCreateBody(body) {
|
|
83
83
|
if (typeof body !== 'object' || body === null) {
|
|
84
|
-
throw usage('a node create requires a JSON body
|
|
84
|
+
throw usage('a node create requires a JSON body');
|
|
85
85
|
}
|
|
86
86
|
const b = body;
|
|
87
|
-
if (typeof b['kind'] !== 'string' || b['kind'].trim() === '') {
|
|
88
|
-
throw usage('a
|
|
87
|
+
if (b['kind'] !== undefined && (typeof b['kind'] !== 'string' || b['kind'].trim() === '')) {
|
|
88
|
+
throw usage('kind must be a non-empty string when provided');
|
|
89
89
|
}
|
|
90
90
|
const rootLifecycle = b['root_lifecycle'];
|
|
91
91
|
if (rootLifecycle !== undefined &&
|
|
@@ -184,6 +184,9 @@ function warmRequestFor(req, cwd, profileId) {
|
|
|
184
184
|
name: req.name ?? null,
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
|
+
export function resolveCreateKind(kind, profile) {
|
|
188
|
+
return kind ?? profile?.manifest.default_kind ?? 'general';
|
|
189
|
+
}
|
|
187
190
|
function requestFromLaunch(launch) {
|
|
188
191
|
const intent = launch.modelIntent === undefined
|
|
189
192
|
? undefined
|
|
@@ -227,8 +230,12 @@ export async function handleCreate(ctx, deps = {}) {
|
|
|
227
230
|
// to this daemon, never to the possibly-remote CLI caller.
|
|
228
231
|
const contextCwd = req.cwd ?? process.cwd();
|
|
229
232
|
const profileId = await resolveProfileId(req.profile, parent ?? null, contextCwd);
|
|
230
|
-
const
|
|
231
|
-
const
|
|
233
|
+
const profile = assertProfileActive(profileId);
|
|
234
|
+
const kind = resolveCreateKind(req.kind, profile);
|
|
235
|
+
assertKindServer(kind);
|
|
236
|
+
const resolvedReq = { ...req, kind };
|
|
237
|
+
const cwd = resolveNodeCwd(resolvedReq, parent, profileId, contextCwd);
|
|
238
|
+
const launch = buildLaunchSpec(kind, (resolvedReq.mode ?? 'base'), {
|
|
232
239
|
lifecycle: req.root === true ? (req.root_lifecycle ?? 'resident') : 'terminal',
|
|
233
240
|
hasManager: req.root !== true,
|
|
234
241
|
...(req.model !== undefined ? { model: req.model } : {}),
|
|
@@ -239,7 +246,7 @@ export async function handleCreate(ctx, deps = {}) {
|
|
|
239
246
|
// --- Warm path: hand out an already-booted spare instead of paying a full
|
|
240
247
|
// engine boot. A miss is not a failure — it falls through to the cold
|
|
241
248
|
// spawn below and mints the spare that serves the NEXT create. ---
|
|
242
|
-
const warm = warmRequestFor(
|
|
249
|
+
const warm = warmRequestFor(resolvedReq, cwd, profileId);
|
|
243
250
|
if (warm !== null) {
|
|
244
251
|
const claimed = await claimWarmNode(warm);
|
|
245
252
|
refillWarmPool(warm);
|
|
@@ -249,7 +256,7 @@ export async function handleCreate(ctx, deps = {}) {
|
|
|
249
256
|
return { status: 201, body: detailOf(claimed) };
|
|
250
257
|
}
|
|
251
258
|
}
|
|
252
|
-
const recipe = buildRecipe(
|
|
259
|
+
const recipe = buildRecipe(resolvedReq, parent, cwd);
|
|
253
260
|
recipe.readiness = req.root === true ? 'await' : 'launch';
|
|
254
261
|
const result = await spawnChild(recipe);
|
|
255
262
|
if (schema !== null)
|
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
// GET /v1/profiles list all profiles
|
|
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
|
+
import { assertInstalledKind } from '../../../core/config.js';
|
|
9
10
|
import { createProfile, ensureRootProfile, listProfiles, loadProfileManifest, } from '../../../core/profiles/manifest.js';
|
|
10
11
|
import { requestProfileDeletion } from '../../profile-delete.js';
|
|
11
12
|
import { toProfileDTO } from '../map.js';
|
|
12
13
|
const ROOT_PROFILE_NAME = 'root';
|
|
13
14
|
/** `PUT /v1/profiles/:name` — idempotent ensure. `root` maps to the singleton
|
|
14
15
|
* root profile; any other name returns the existing same-named profile if one
|
|
15
|
-
* exists (so a re-PUT yields the same id), else creates it.
|
|
16
|
+
* exists (so a re-PUT yields the same id), else creates it. `default_kind`
|
|
17
|
+
* and `metadata` apply only at create — an existing profile is returned
|
|
18
|
+
* untouched; mutation stays on `crtr profile kind` / `crtr profile meta`. */
|
|
16
19
|
function handleEnsure(ctx) {
|
|
17
20
|
const name = ctx.params['name'];
|
|
18
21
|
const req = (ctx.body ?? {});
|
|
@@ -23,7 +26,14 @@ function handleEnsure(ctx) {
|
|
|
23
26
|
if (existing !== undefined) {
|
|
24
27
|
return { status: 200, body: toProfileDTO(existing) };
|
|
25
28
|
}
|
|
26
|
-
|
|
29
|
+
// Same gate as `profile new`/`profile kind`: a persisted unknown kind would
|
|
30
|
+
// poison every future omitted-kind create under this profile.
|
|
31
|
+
if (req.default_kind !== undefined)
|
|
32
|
+
assertInstalledKind(req.default_kind);
|
|
33
|
+
const created = createProfile(name, req.projects ?? [], {
|
|
34
|
+
defaultKind: req.default_kind,
|
|
35
|
+
metadata: req.metadata,
|
|
36
|
+
});
|
|
27
37
|
return { status: 200, body: toProfileDTO(created) };
|
|
28
38
|
}
|
|
29
39
|
/** `GET /v1/profiles` — every profile with a readable manifest. */
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// reasonable projections; this module is what makes them concrete, so any DTO
|
|
11
11
|
// field that projection proves wrong is corrected here at the source.
|
|
12
12
|
import { ReviewOperationError as ReviewOperationErrorClass } from '../../core/review/types.js';
|
|
13
|
+
import { sanitizeProfileMetadata } from '../../core/profiles/manifest.js';
|
|
13
14
|
import { contextDir, inboxPath, nodeDir, nodeMetaPath, reportsDir, reviewResultPath, transcriptPath, viewSocketPath, } from '../../core/canvas/paths.js';
|
|
14
15
|
import { FinalizationError } from '../../core/feed/feed.js';
|
|
15
16
|
import { NodeIdConflictError } from '../../core/runtime/nodes.js';
|
|
@@ -264,6 +265,9 @@ export function toProfileDTO(entry) {
|
|
|
264
265
|
name: entry.manifest.name,
|
|
265
266
|
projects: [...entry.manifest.projects],
|
|
266
267
|
home: entry.manifest.home,
|
|
268
|
+
paused_at: entry.manifest.paused_at,
|
|
269
|
+
default_kind: entry.manifest.default_kind ?? 'general',
|
|
270
|
+
metadata: sanitizeProfileMetadata(entry.manifest.metadata),
|
|
267
271
|
default_dir: null,
|
|
268
272
|
};
|
|
269
273
|
}
|
package/dist/daemon/fleet.js
CHANGED
|
@@ -425,11 +425,6 @@ export class DaemonFleet {
|
|
|
425
425
|
error: err,
|
|
426
426
|
});
|
|
427
427
|
}
|
|
428
|
-
|
|
429
|
-
fanDoctrineWake(nodeId, subscribersOf(nodeId), opts.wake, { reason: 'child-crashed', child: nodeId });
|
|
430
|
-
}
|
|
431
|
-
catch {
|
|
432
|
-
/* best-effort, mirrors every other fan-out in the daemon */
|
|
433
|
-
}
|
|
428
|
+
fanDoctrineWake(nodeId, subscribersOf(nodeId), opts.wake, { reason: 'child-crashed', child: nodeId });
|
|
434
429
|
}
|
|
435
430
|
}
|
|
@@ -21,7 +21,7 @@ import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
|
|
|
21
21
|
import { activeBackgroundBashJobs } from '../../core/bash-jobs.js';
|
|
22
22
|
import { focusOf } from '../../core/runtime/placement.js';
|
|
23
23
|
import { hasAttachedBrokerViewers } from '../../core/runtime/broker/client-registry.js';
|
|
24
|
-
import { parkEvent } from './live-obligation.js';
|
|
24
|
+
import { applyParkEvent, parkEvent } from './live-obligation.js';
|
|
25
25
|
const execFileAsync = promisify(execFile);
|
|
26
26
|
function configuredPositiveInteger(name, fallback) {
|
|
27
27
|
const raw = process.env[name];
|
|
@@ -216,7 +216,7 @@ export class BrokerSupervisionReconciler {
|
|
|
216
216
|
if (now - since < unattendedParkMsForDaemon())
|
|
217
217
|
return;
|
|
218
218
|
this.unattendedSince.delete(id);
|
|
219
|
-
|
|
219
|
+
applyParkEvent(id, parkEvent(id, row.lifecycle));
|
|
220
220
|
headlessBrokerHost.teardown(id);
|
|
221
221
|
}
|
|
222
222
|
async warnOverCapInTmux(count, warning, max) {
|
|
@@ -406,18 +406,13 @@ export class BrokerSupervisionReconciler {
|
|
|
406
406
|
catch {
|
|
407
407
|
/* best-effort — the doctrine wake below is the load-bearing notice */
|
|
408
408
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
catch {
|
|
419
|
-
/* best-effort, mirrors every other fan-out in this file */
|
|
420
|
-
}
|
|
409
|
+
fanDoctrineWake(id, subscribersOf(id), label, {
|
|
410
|
+
reason: 'child-wedged',
|
|
411
|
+
child: id,
|
|
412
|
+
quiet_ms: quietForMs,
|
|
413
|
+
cpu_percent: cpuPercent,
|
|
414
|
+
kicked: !hasDescendants,
|
|
415
|
+
});
|
|
421
416
|
}
|
|
422
417
|
handleFatalFault(id) {
|
|
423
418
|
const fault = readFault(id);
|
|
@@ -464,17 +459,12 @@ export class BrokerSupervisionReconciler {
|
|
|
464
459
|
terminal_overflow: terminalOverflow,
|
|
465
460
|
},
|
|
466
461
|
});
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
catch {
|
|
476
|
-
/* best-effort, mirrors every other fan-out in this file */
|
|
477
|
-
}
|
|
462
|
+
fanDoctrineWake(id, subscribersOf(id), label, {
|
|
463
|
+
reason: terminalOverflow ? 'child-context-overflow' : 'child-fault-fatal',
|
|
464
|
+
child: id,
|
|
465
|
+
kind: fault.kind,
|
|
466
|
+
message: firstLine,
|
|
467
|
+
});
|
|
478
468
|
}
|
|
479
469
|
async handleAuthReload(now, fleet) {
|
|
480
470
|
void now;
|
|
@@ -7,7 +7,8 @@ import { appendControllerDeathNotice, readCursor, readInboxSince, } from '../../
|
|
|
7
7
|
import { fanDoctrineWake } from '../../core/runtime/close.js';
|
|
8
8
|
import { transition } from '../../core/runtime/lifecycle.js';
|
|
9
9
|
import { reviveNode } from '../../core/runtime/revive.js';
|
|
10
|
-
import {
|
|
10
|
+
import { isProfilePaused } from '../../core/profiles/manifest.js';
|
|
11
|
+
import { applyParkEvent, parkEvent } from './live-obligation.js';
|
|
11
12
|
// Idle-release/inbox retry cap (2026-07-10 diagnosis, round 2): the SAME
|
|
12
13
|
// livelock shape as the stranded-relaunch case, but on the separate pass that
|
|
13
14
|
// resumes a dormant idle-released node when its inbox gains an unseen entry.
|
|
@@ -59,6 +60,10 @@ export class DormantInboxReconciler {
|
|
|
59
60
|
continue;
|
|
60
61
|
if (ctx.fleet.has(id))
|
|
61
62
|
continue;
|
|
63
|
+
if (isProfilePaused(current.profile_id)) {
|
|
64
|
+
this.relaunchState.delete(id);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
62
67
|
}
|
|
63
68
|
catch (err) {
|
|
64
69
|
operationIdContext.fresh(() => {
|
|
@@ -89,7 +94,7 @@ export class DormantInboxReconciler {
|
|
|
89
94
|
if (meta !== null) {
|
|
90
95
|
const event = parkEvent(id, meta.lifecycle);
|
|
91
96
|
if (event !== 'release')
|
|
92
|
-
|
|
97
|
+
applyParkEvent(id, event);
|
|
93
98
|
}
|
|
94
99
|
return;
|
|
95
100
|
}
|
|
@@ -111,14 +116,9 @@ export class DormantInboxReconciler {
|
|
|
111
116
|
});
|
|
112
117
|
transition(id, 'crash');
|
|
113
118
|
if (meta !== null) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
`dead until you revive it — \`crtr node lifecycle revive ${id}\`.`, { reason: 'child-crashed', child: id });
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
/* best-effort, mirrors the other doctrine fan-outs */
|
|
121
|
-
}
|
|
119
|
+
fanDoctrineWake(id, subscribersOf(id), `Child crashed — ${fullName(meta)} (${id}) failed to relaunch after ${attempts} attempts while trying to ` +
|
|
120
|
+
`deliver a pending inbox message (no delivery progress across any attempt), and is now dead. It stays ` +
|
|
121
|
+
`dead until you revive it — \`crtr node lifecycle revive ${id}\`.`, { reason: 'child-crashed', child: id });
|
|
122
122
|
}
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Lifecycle } from '../../core/canvas/index.js';
|
|
2
|
+
import { type LifecycleEvent } from '../../core/runtime/lifecycle.js';
|
|
2
3
|
/** Durable work that requires a node to remain automatically wakeable. */
|
|
3
4
|
export declare function hasLiveObligation(nodeId: string): boolean;
|
|
4
5
|
/** Whether anything would drive a turn if this node's engine came back right
|
|
@@ -28,4 +29,7 @@ export declare function hasPendingWake(nodeId: string): boolean;
|
|
|
28
29
|
* terminalized row no longer takes that cycle, so writing one off would drop
|
|
29
30
|
* content the sender already handed over.
|
|
30
31
|
*/
|
|
31
|
-
export
|
|
32
|
+
export type ParkEvent = Extract<LifecycleEvent, 'release' | 'finish' | 'cancel'>;
|
|
33
|
+
export declare function parkEvent(nodeId: string, lifecycle: Lifecycle): ParkEvent;
|
|
34
|
+
/** Apply an unattended decision and notify subscribers of automatic completion. */
|
|
35
|
+
export declare function applyParkEvent(nodeId: string, event: ParkEvent): void;
|