@north-light/crouter 0.3.235 → 0.3.237
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 +2 -1
- package/dist/api/client.js +3 -0
- package/dist/api/dto/broker-ops.d.ts +1 -1
- package/dist/api/dto/common.d.ts +2 -0
- package/dist/api/dto/lifecycle.d.ts +9 -2
- package/dist/api/dto/messages.d.ts +5 -0
- package/dist/api/dto/nodes.d.ts +14 -5
- package/dist/api/dto/worktree.d.ts +10 -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 +1 -1
- package/dist/builtin-memory/internal/agent-shaping.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +1 -1
- package/dist/clients/attach/render/tool-calls.js +18 -3
- package/dist/clients/attach/viewer.js +399 -399
- package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +6 -0
- package/dist/clients/inbox/review/roundtrip.js +1 -1
- package/dist/commands/__tests__/node-new.test.js +58 -0
- package/dist/commands/memory/lint.js +22 -13
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/node/create.d.ts +1 -1
- package/dist/commands/node/create.js +4 -1
- package/dist/commands/node/lifecycle.js +2 -2
- package/dist/commands/node-worktree.js +39 -8
- package/dist/commands/revive.js +11 -2
- package/dist/commands/sys/context/admin/actions.d.ts +48 -0
- package/dist/commands/sys/context/admin/actions.js +246 -0
- package/dist/commands/sys/context/admin/axes-panel.d.ts +21 -0
- package/dist/commands/sys/context/admin/axes-panel.js +117 -0
- package/dist/commands/sys/context/admin/detail-panel.d.ts +53 -0
- package/dist/commands/sys/context/admin/detail-panel.js +178 -0
- package/dist/commands/sys/context/admin/docs-panel.d.ts +45 -0
- package/dist/commands/sys/context/admin/docs-panel.js +186 -0
- package/dist/commands/sys/context/admin/model.d.ts +75 -0
- package/dist/commands/sys/context/admin/model.js +209 -0
- package/dist/commands/sys/context/admin/shell.d.ts +82 -0
- package/dist/commands/sys/context/admin/shell.js +669 -0
- package/dist/commands/sys/context/admin.d.ts +1 -0
- package/dist/commands/sys/context/admin.js +124 -0
- package/dist/commands/sys/context/doc.d.ts +1 -0
- package/dist/commands/sys/context/doc.js +196 -0
- package/dist/commands/sys/context/prompt-review.d.ts +1 -0
- package/dist/commands/sys/{prompt-review.js → context/prompt-review.js} +10 -10
- package/dist/commands/sys/context/resolve.d.ts +55 -0
- package/dist/commands/sys/context/resolve.js +428 -0
- package/dist/commands/sys/context/sysprompt.d.ts +1 -0
- package/dist/commands/sys/{sysprompt.js → context/sysprompt.js} +11 -11
- package/dist/commands/sys/context.d.ts +2 -0
- package/dist/commands/sys/context.js +20 -0
- package/dist/commands/sys.js +3 -4
- package/dist/core/__tests__/canvas-inbox-watcher-naming.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-inbox-watcher-naming.test.js +258 -0
- package/dist/core/__tests__/child-death-wake.test.js +38 -22
- package/dist/core/__tests__/cron-self-cancel-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/cron-self-cancel-delivery.test.js +72 -0
- package/dist/core/__tests__/daemon-boot.test.js +40 -28
- package/dist/core/__tests__/helpers/harness.js +1 -0
- package/dist/core/__tests__/human-deliver.test.js +1 -1
- package/dist/core/__tests__/human-node-not-supervised.test.js +10 -15
- package/dist/core/__tests__/integration/revive.test.js +12 -12
- package/dist/core/__tests__/integration/spawn-root.test.js +6 -0
- package/dist/core/__tests__/integration/worktree-reap.test.js +32 -0
- package/dist/core/__tests__/lifecycle-terminal-reasons.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +61 -0
- package/dist/core/__tests__/migration.test.js +38 -1
- package/dist/core/__tests__/push-final-guard.test.js +1 -1
- package/dist/core/__tests__/revive-capacity.test.d.ts +1 -0
- package/dist/core/__tests__/revive-capacity.test.js +127 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +7 -7
- package/dist/core/__tests__/seam/broker-cap-freeze.test.d.ts +1 -0
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +81 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +76 -7
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +1 -1
- package/dist/core/canvas/browse/render.js +11 -3
- package/dist/core/canvas/canvas.d.ts +15 -3
- package/dist/core/canvas/canvas.js +52 -16
- package/dist/core/canvas/migrations.js +21 -0
- package/dist/core/canvas/nav-render.js +3 -1
- package/dist/core/canvas/remote-canvas-source.js +2 -0
- package/dist/core/canvas/render-source.d.ts +3 -1
- package/dist/core/canvas/render-source.js +7 -3
- package/dist/core/canvas/status-glyph.d.ts +10 -1
- package/dist/core/canvas/status-glyph.js +19 -3
- package/dist/core/canvas/types.d.ts +22 -3
- package/dist/core/feed/feed.js +1 -1
- package/dist/core/human/feedback-companion.js +5 -2
- package/dist/core/memory/lint.d.ts +19 -0
- package/dist/core/memory/lint.js +97 -0
- package/dist/core/memory-resolver.d.ts +4 -0
- package/dist/core/memory-resolver.js +7 -7
- package/dist/core/preview-registry.js +4 -4
- package/dist/core/review/realize.js +4 -2
- package/dist/core/runtime/close.js +1 -1
- package/dist/core/runtime/fleet.d.ts +29 -1
- package/dist/core/runtime/fleet.js +23 -0
- package/dist/core/runtime/host.js +32 -11
- package/dist/core/runtime/launch-prompt.d.ts +1 -1
- package/dist/core/runtime/launch-prompt.js +2 -2
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/lifecycle.d.ts +4 -2
- package/dist/core/runtime/lifecycle.js +12 -1
- package/dist/core/runtime/memory.js +1 -1
- package/dist/core/runtime/reopen.js +10 -1
- package/dist/core/runtime/reset.js +1 -1
- package/dist/core/runtime/revive-all.d.ts +3 -0
- package/dist/core/runtime/revive-all.js +10 -4
- package/dist/core/runtime/revive.d.ts +12 -0
- package/dist/core/runtime/revive.js +62 -3
- package/dist/core/runtime/spawn.d.ts +9 -13
- package/dist/core/runtime/spawn.js +40 -17
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/index.d.ts +6 -3
- package/dist/core/substrate/index.js +4 -3
- package/dist/core/substrate/on-read.d.ts +0 -8
- package/dist/core/substrate/on-read.js +45 -124
- package/dist/core/substrate/plan.d.ts +94 -0
- package/dist/core/substrate/plan.js +266 -0
- package/dist/core/substrate/render.d.ts +5 -6
- package/dist/core/substrate/render.js +76 -129
- package/dist/core/substrate/schema.d.ts +1 -6
- package/dist/core/substrate/schema.js +1 -15
- package/dist/core/substrate/session-cache.d.ts +2 -10
- package/dist/core/substrate/session-cache.js +9 -47
- package/dist/core/substrate/subject-fields.d.ts +1 -4
- package/dist/core/substrate/subject-fields.js +2 -5
- package/dist/core/substrate/surface-match.d.ts +37 -17
- package/dist/core/substrate/surface-match.js +44 -53
- package/dist/core/tui/page-host.d.ts +6 -0
- package/dist/core/tui/page-host.js +12 -3
- package/dist/core/worktree.d.ts +8 -0
- package/dist/core/worktree.js +65 -0
- package/dist/daemon/__tests__/reconciler-signature.test.js +3 -2
- package/dist/daemon/__tests__/thaw-order.test.d.ts +1 -0
- package/dist/daemon/__tests__/thaw-order.test.js +55 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +6 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +6 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +1 -1
- package/dist/daemon/api/bridge.js +5 -1
- package/dist/daemon/api/handlers/attach.js +5 -1
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/messages.js +10 -2
- package/dist/daemon/api/handlers/nodes.js +8 -4
- package/dist/daemon/api/handlers/worktree.js +10 -1
- package/dist/daemon/api/map.js +7 -0
- package/dist/daemon/companion-retire.js +1 -1
- package/dist/daemon/cron/passes.d.ts +0 -5
- package/dist/daemon/cron/passes.js +0 -20
- package/dist/daemon/cron/sinks.js +1 -1
- package/dist/daemon/cron-run.d.ts +5 -6
- package/dist/daemon/cron-run.js +7 -16
- package/dist/daemon/crtrd.d.ts +0 -10
- package/dist/daemon/crtrd.js +23 -61
- package/dist/daemon/fleet.d.ts +10 -7
- package/dist/daemon/fleet.js +71 -12
- package/dist/daemon/messaging/node-message.js +3 -1
- package/dist/daemon/profile-delete.js +1 -1
- package/dist/daemon/reconcilers/broker-supervision.d.ts +26 -9
- package/dist/daemon/reconcilers/broker-supervision.js +39 -52
- package/dist/daemon/reconcilers/controller-death.d.ts +7 -0
- package/dist/daemon/reconcilers/controller-death.js +26 -0
- package/dist/daemon/reconcilers/cron-lane.d.ts +0 -3
- package/dist/daemon/reconcilers/cron-lane.js +0 -3
- package/dist/daemon/reconcilers/live-obligation.js +3 -1
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +53 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +170 -0
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +5 -0
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +16 -0
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -0
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +13 -0
- package/dist/daemon/reconcilers/node-lifecycle/tick.d.ts +26 -0
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +140 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +12 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/commands/sys/prompt-review.d.ts +0 -1
- package/dist/commands/sys/sysprompt.d.ts +0 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +0 -119
- package/dist/daemon/reconcilers/dormant-inbox.d.ts +0 -18
- package/dist/daemon/reconcilers/dormant-inbox.js +0 -157
- /package/dist/{core/__tests__/cron-broker-capacity.test.d.ts → commands/__tests__/node-new.test.d.ts} +0 -0
|
@@ -33,6 +33,9 @@ export type MemoryLintRule =
|
|
|
33
33
|
| 'dangling-link'
|
|
34
34
|
/** A `memory-read` route that matches no canonical name. */
|
|
35
35
|
| 'memory-read-route'
|
|
36
|
+
/** A document no reachability path routes an agent to — no surface delivers
|
|
37
|
+
* it, no reached document links it, and no reached directory lists it. */
|
|
38
|
+
| 'orphan'
|
|
36
39
|
/** A project store's workspace front door is missing or not its root doc. */
|
|
37
40
|
| 'front-door'
|
|
38
41
|
/** Boot entries in a nested store, which never rides the boot catalog. */
|
|
@@ -91,6 +94,22 @@ export declare function lintShortPreviewBody(fm: Record<string, unknown>, body:
|
|
|
91
94
|
* answers with its own document and its listing. Derived from the canonical
|
|
92
95
|
* tree, never from string prefixes of a name. */
|
|
93
96
|
export declare function resolvableCanonicalNames(docs: readonly MemoryDoc[]): Set<string>;
|
|
97
|
+
/** The orphan rule. A document is reachable when an agent is routed to it
|
|
98
|
+
* without knowing its name: a surface delivers it (a `memory-read` surface is
|
|
99
|
+
* deliberate companion wiring and counts), it is invocable as a slash command,
|
|
100
|
+
* a reached document's real `[[link]]` names it, or a reached CURATED INDEX
|
|
101
|
+
* document's listing includes it. The listing edge routes only through a
|
|
102
|
+
* directory that carries its own non-root index document — a bare directory
|
|
103
|
+
* has no curator, and a store's root front door is delivered with the whole
|
|
104
|
+
* boot tree, so treating either listing as a route would reach every doc and
|
|
105
|
+
* defeat the rule (the same reason the corpus root is never seeded). Curated
|
|
106
|
+
* BODY LINKS on those docs still route. Reachability is computed over the
|
|
107
|
+
* WHOLE target view — a doc linked only from another store is still reachable.
|
|
108
|
+
* `flagStoreRoot` restricts which store's unreachable docs are flagged while
|
|
109
|
+
* every store still participates in the graph as a potential linker. */
|
|
110
|
+
export declare function lintOrphans(viewDocs: readonly MemoryDoc[], options?: {
|
|
111
|
+
flagStoreRoot?: string;
|
|
112
|
+
}): MemoryLintFinding[];
|
|
94
113
|
export interface MemoryLintStoreOptions {
|
|
95
114
|
/** Canonical names a `[[link]]` or route may target — the target view the
|
|
96
115
|
* store's documents are read in. Omit to skip reference checks entirely
|
package/dist/core/memory/lint.js
CHANGED
|
@@ -133,6 +133,98 @@ export function resolvableCanonicalNames(docs) {
|
|
|
133
133
|
walk(buildMemoryTree(docs).root);
|
|
134
134
|
return names;
|
|
135
135
|
}
|
|
136
|
+
/** The orphan rule. A document is reachable when an agent is routed to it
|
|
137
|
+
* without knowing its name: a surface delivers it (a `memory-read` surface is
|
|
138
|
+
* deliberate companion wiring and counts), it is invocable as a slash command,
|
|
139
|
+
* a reached document's real `[[link]]` names it, or a reached CURATED INDEX
|
|
140
|
+
* document's listing includes it. The listing edge routes only through a
|
|
141
|
+
* directory that carries its own non-root index document — a bare directory
|
|
142
|
+
* has no curator, and a store's root front door is delivered with the whole
|
|
143
|
+
* boot tree, so treating either listing as a route would reach every doc and
|
|
144
|
+
* defeat the rule (the same reason the corpus root is never seeded). Curated
|
|
145
|
+
* BODY LINKS on those docs still route. Reachability is computed over the
|
|
146
|
+
* WHOLE target view — a doc linked only from another store is still reachable.
|
|
147
|
+
* `flagStoreRoot` restricts which store's unreachable docs are flagged while
|
|
148
|
+
* every store still participates in the graph as a potential linker. */
|
|
149
|
+
export function lintOrphans(viewDocs, options = {}) {
|
|
150
|
+
const tree = buildMemoryTree(viewDocs);
|
|
151
|
+
const index = new Map();
|
|
152
|
+
const indexWalk = (node) => {
|
|
153
|
+
if (node.name !== '')
|
|
154
|
+
index.set(node.name, node);
|
|
155
|
+
for (const child of node.childDirectories)
|
|
156
|
+
indexWalk(child);
|
|
157
|
+
for (const child of node.childDocuments)
|
|
158
|
+
index.set(child.name, child);
|
|
159
|
+
};
|
|
160
|
+
indexWalk(tree.root);
|
|
161
|
+
const reachable = new Set();
|
|
162
|
+
const queue = [];
|
|
163
|
+
const enqueue = (name) => {
|
|
164
|
+
if (name === '' || reachable.has(name) || !index.has(name))
|
|
165
|
+
return;
|
|
166
|
+
reachable.add(name);
|
|
167
|
+
queue.push(name);
|
|
168
|
+
};
|
|
169
|
+
// Roots: a doc an agent reaches without a name — any surface entry, or a
|
|
170
|
+
// slash command. A `memory-read` surface counts (deliberate companion wiring).
|
|
171
|
+
for (const doc of viewDocs) {
|
|
172
|
+
const fm = doc.frontmatter ?? {};
|
|
173
|
+
if (parsedSubstrateSurfaces(fm).length > 0 || fm['slash'] === true)
|
|
174
|
+
enqueue(doc.name);
|
|
175
|
+
}
|
|
176
|
+
// Fixpoint: reading a reachable node routes to its real links, and — when the
|
|
177
|
+
// node is a curated non-root index — to the members its listing renders.
|
|
178
|
+
while (queue.length > 0) {
|
|
179
|
+
const node = index.get(queue.shift());
|
|
180
|
+
const doc = node.document;
|
|
181
|
+
if (doc !== null) {
|
|
182
|
+
for (const target of docLinkNames(doc.body))
|
|
183
|
+
if (index.has(target))
|
|
184
|
+
enqueue(target);
|
|
185
|
+
}
|
|
186
|
+
if (doc === null || doc.localName === '')
|
|
187
|
+
continue;
|
|
188
|
+
for (const child of node.childDirectories) {
|
|
189
|
+
if (child.document?.frontmatter?.['unlisted'] === true)
|
|
190
|
+
continue;
|
|
191
|
+
enqueue(child.name);
|
|
192
|
+
}
|
|
193
|
+
for (const child of node.childDocuments) {
|
|
194
|
+
if (child.document.frontmatter?.['unlisted'] === true)
|
|
195
|
+
continue;
|
|
196
|
+
enqueue(child.name);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const findings = [];
|
|
200
|
+
for (const doc of viewDocs) {
|
|
201
|
+
if (options.flagStoreRoot !== undefined && doc.store.storeRoot !== options.flagStoreRoot)
|
|
202
|
+
continue;
|
|
203
|
+
if (reachable.has(doc.name))
|
|
204
|
+
continue;
|
|
205
|
+
// Not the running user's to fix, but still a linker in the graph above.
|
|
206
|
+
if (doc.scope === 'builtin' || doc.plugin !== undefined)
|
|
207
|
+
continue;
|
|
208
|
+
// A store's root document is its entry point by definition.
|
|
209
|
+
if (doc.localName === '')
|
|
210
|
+
continue;
|
|
211
|
+
// Persona layers are read by construction, same as the length rule.
|
|
212
|
+
if (doc.name === 'kinds' || doc.name.startsWith('kinds/'))
|
|
213
|
+
continue;
|
|
214
|
+
if (ignoresRule(doc.frontmatter ?? {}, 'orphan'))
|
|
215
|
+
continue;
|
|
216
|
+
findings.push({
|
|
217
|
+
severity: 'warning',
|
|
218
|
+
rule: 'orphan',
|
|
219
|
+
path: doc.path,
|
|
220
|
+
name: doc.name,
|
|
221
|
+
scope: doc.scope,
|
|
222
|
+
...(doc.store.storeRoot === '' ? {} : { storeRoot: doc.store.storeRoot }),
|
|
223
|
+
message: `\`${doc.name}\` is unreachable: no surface delivers it, no reached document links it, and no reached directory lists it — it is findable only by deliberate search. Give it a surface, link it from a reached doc (a cluster index, or its directory's own ${INDEX_SEGMENT}.md), or accept it with \`lint-ignore: orphan\`.`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return findings;
|
|
227
|
+
}
|
|
136
228
|
const FRONT_DOOR_REMEDY = 'the store\'s root INDEX.md is the workspace front door: give it `surfaces: [{on: workspace-open, at: content}, {on: read, match: "./**", at: content}]` (run `crtr memory write -h` first)';
|
|
137
229
|
const GLOB_META_RE = /[*?[\]{}]/;
|
|
138
230
|
function safeGlob(target, glob) {
|
|
@@ -566,6 +658,11 @@ export function lintMemoryTarget(options = {}) {
|
|
|
566
658
|
walk(child);
|
|
567
659
|
};
|
|
568
660
|
walk(tree.root);
|
|
661
|
+
// Reachability is a whole-view property, so it runs once over the target view
|
|
662
|
+
// and only in the unfiltered view — a `--scope` slice hides the surfaced or
|
|
663
|
+
// linking docs that make a doc reachable and would report false orphans.
|
|
664
|
+
if (options.scope === undefined)
|
|
665
|
+
findings.push(...lintOrphans(view.docs));
|
|
569
666
|
return {
|
|
570
667
|
...makeResult(findings, checked, documents, storesLinted),
|
|
571
668
|
stores: view.stores,
|
|
@@ -184,6 +184,10 @@ export interface MemoryTarget {
|
|
|
184
184
|
* or null outside a node. */
|
|
185
185
|
nodeId: string | null;
|
|
186
186
|
}
|
|
187
|
+
/** The target the ambient process describes: cwd + the two env seams every
|
|
188
|
+
* profile/node-aware memory path reads through (`CRTR_PROFILE_ID`,
|
|
189
|
+
* `CRTR_NODE_ID`). */
|
|
190
|
+
export declare function ambientMemoryTarget(): MemoryTarget;
|
|
187
191
|
/** The exact project store owned by `ownerDir` — the `--dir` selector's store,
|
|
188
192
|
* loaded whatever its mount status so lint and migration can inspect a store
|
|
189
193
|
* the runtime refuses to mount. */
|
|
@@ -18,7 +18,7 @@ import { associateRepository } from './memory/repository-association.js';
|
|
|
18
18
|
/** The target the ambient process describes: cwd + the two env seams every
|
|
19
19
|
* profile/node-aware memory path reads through (`CRTR_PROFILE_ID`,
|
|
20
20
|
* `CRTR_NODE_ID`). */
|
|
21
|
-
function
|
|
21
|
+
export function ambientMemoryTarget() {
|
|
22
22
|
return {
|
|
23
23
|
cwd: process.cwd(),
|
|
24
24
|
profileId: envProfileId() || null,
|
|
@@ -27,7 +27,7 @@ function ambientTarget() {
|
|
|
27
27
|
}
|
|
28
28
|
/** The selected profile id from the process env, or '' when none. */
|
|
29
29
|
function selectedProfileId() {
|
|
30
|
-
return
|
|
30
|
+
return ambientMemoryTarget().profileId ?? '';
|
|
31
31
|
}
|
|
32
32
|
// Store discovery: which physical stores a target sees, in what order, and
|
|
33
33
|
// whether each one's namespace lets it mount.
|
|
@@ -287,7 +287,7 @@ export function memoryStoresInPrecedence(target, scope, includeDescendants = fal
|
|
|
287
287
|
* how a DELETED doc's revision log is still found: history outlives the doc,
|
|
288
288
|
* so its lookup cannot go through document resolution. */
|
|
289
289
|
export function nativeMemoryStoresInPrecedence(scope, includeDescendants = false) {
|
|
290
|
-
return memoryStoresInPrecedence(
|
|
290
|
+
return memoryStoresInPrecedence(ambientMemoryTarget(), scope, includeDescendants)
|
|
291
291
|
.filter((store) => store.mountStatus === 'ready' && store.scope !== 'builtin' && store.plugin === undefined)
|
|
292
292
|
.map((store) => ({ scope: store.scope, memoryDir: store.storeRoot, store }));
|
|
293
293
|
}
|
|
@@ -475,9 +475,9 @@ export function listProjectMemoryDocs(startDir = process.cwd(), profileId = sele
|
|
|
475
475
|
* candidates stay in source order, so a caller's first-wins dedup yields the
|
|
476
476
|
* address winners. */
|
|
477
477
|
export function listAllMemoryDocs(scope, quiet = false, includeDescendants = false) {
|
|
478
|
-
return [...loadMemoryTargetView(
|
|
478
|
+
return [...loadMemoryTargetView(ambientMemoryTarget(), { ...(scope ? { scope } : {}), quiet, includeDescendants }).docs];
|
|
479
479
|
}
|
|
480
|
-
export function createMemoryDocSnapshot(target =
|
|
480
|
+
export function createMemoryDocSnapshot(target = ambientMemoryTarget()) {
|
|
481
481
|
// Quiet: a snapshot feeds command registration and migration planning, where
|
|
482
482
|
// another doc's frontmatter health is noise — corpus health is `lint`'s job.
|
|
483
483
|
const view = loadMemoryTargetView(target, { quiet: true });
|
|
@@ -513,7 +513,7 @@ export function readMemoryDocContent(path, includeFrontmatter = false) {
|
|
|
513
513
|
}
|
|
514
514
|
/** The address winner for an exact canonical name in the ambient target. */
|
|
515
515
|
export function resolveMemoryDoc(rawName, opts = {}) {
|
|
516
|
-
return resolveMemoryDocForTarget(rawName,
|
|
516
|
+
return resolveMemoryDocForTarget(rawName, ambientMemoryTarget(), opts);
|
|
517
517
|
}
|
|
518
518
|
/** Resolve a memory document as ANOTHER node would see it — the same precedence
|
|
519
519
|
* chain (node-local > project stack > profile > user > builtin), read from the
|
|
@@ -527,7 +527,7 @@ export function resolveMemoryDocForTarget(rawName, target, opts = {}) {
|
|
|
527
527
|
* in source order — what path-aware inventory, diagnostics, and automatic
|
|
528
528
|
* event eligibility consume instead of the winner alone. */
|
|
529
529
|
export function resolveMemoryCandidates(rawName, opts = {}) {
|
|
530
|
-
return resolveMemoryCandidatesForTarget(rawName,
|
|
530
|
+
return resolveMemoryCandidatesForTarget(rawName, ambientMemoryTarget(), opts);
|
|
531
531
|
}
|
|
532
532
|
export function resolveMemoryCandidatesForTarget(rawName, target, opts = {}) {
|
|
533
533
|
const view = loadMemoryTargetView(target, { ...opts, quiet: true });
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// Source of truth: this Set must mirror every leaf that declares
|
|
12
12
|
// `help.preview.suppressOutput: true` in its own command file (currently
|
|
13
13
|
// canvas roster, canvas snapshot, node inspect snapshot, node inspect show,
|
|
14
|
-
// and sys sysprompt). There is no automated sync — when you add or remove a leaf's
|
|
14
|
+
// and sys context sysprompt). There is no automated sync — when you add or remove a leaf's
|
|
15
15
|
// `preview.suppressOutput`, update this Set in the same commit.
|
|
16
16
|
// `preview-registry-sync.test.ts` (which imports the real command tree — safe
|
|
17
17
|
// in a test, never in the attach bundle) asserts the two stay identical.
|
|
@@ -22,7 +22,7 @@ export const SUPPRESS_OUTPUT_LEAVES = new Set([
|
|
|
22
22
|
'canvas snapshot',
|
|
23
23
|
'node inspect snapshot',
|
|
24
24
|
'node inspect show',
|
|
25
|
-
'sys sysprompt',
|
|
25
|
+
'sys context sysprompt',
|
|
26
26
|
]);
|
|
27
27
|
/** True when the leaf at this full help-name path (e.g. "canvas snapshot")
|
|
28
28
|
* declares `preview.suppressOutput: true`. */
|
|
@@ -162,8 +162,8 @@ export const LEAF_ICON_CP = {
|
|
|
162
162
|
'sys settings': 0xf013,
|
|
163
163
|
'sys setup': 0xf0ad,
|
|
164
164
|
'sys doctor': 0xf0f1,
|
|
165
|
-
'sys sysprompt': 0xf15c,
|
|
166
|
-
'sys prompt-review': 0xf002,
|
|
165
|
+
'sys context sysprompt': 0xf15c,
|
|
166
|
+
'sys context prompt-review': 0xf002,
|
|
167
167
|
'sys feedback': 0xf075,
|
|
168
168
|
'sys update': 0xf021,
|
|
169
169
|
'sys version': 0xf02b,
|
|
@@ -83,7 +83,7 @@ function cancelInvisibleBridge(nodeId) {
|
|
|
83
83
|
// cannot produce a final report or notify the origin.
|
|
84
84
|
const node = getNode(nodeId);
|
|
85
85
|
if (node !== null && node.status !== 'done' && node.status !== 'canceled')
|
|
86
|
-
transition(nodeId, 'cancel');
|
|
86
|
+
transition(nodeId, 'cancel', { reason: 'retired' });
|
|
87
87
|
}
|
|
88
88
|
/** Assert the only route a bridge has back to its origin before ticket exposure. */
|
|
89
89
|
function ensureBridgeSubscription(review, bridgeNodeId) {
|
|
@@ -168,8 +168,10 @@ export async function realizeReview(reviewId) {
|
|
|
168
168
|
const companion = getNode(review.companion_node_id);
|
|
169
169
|
if (companion === null)
|
|
170
170
|
throw companionUnbound(review.review_id, 0);
|
|
171
|
+
// Realization is bounded by the bind deadline below, which only a live broker
|
|
172
|
+
// can meet: refuse at the cap rather than freeze into a certain timeout.
|
|
171
173
|
if (companion.pi_session_id === null)
|
|
172
|
-
reviveNode(companion.node_id, { resume: false });
|
|
174
|
+
reviveNode(companion.node_id, { resume: false, capacity: 'refuse' });
|
|
173
175
|
// R4: session_start is the durable bind boundary.
|
|
174
176
|
if (!await awaitCompanionBind(review.companion_node_id, COMPANION_BIND_DEADLINE_MS)) {
|
|
175
177
|
throw companionUnbound(review.review_id, COMPANION_BIND_DEADLINE_MS);
|
|
@@ -188,7 +188,7 @@ export function closeNode(rootId, opts = {}) {
|
|
|
188
188
|
transition(id, 'finish');
|
|
189
189
|
}
|
|
190
190
|
else {
|
|
191
|
-
transition(id, 'cancel');
|
|
191
|
+
transition(id, 'cancel', { reason: 'closed' });
|
|
192
192
|
}
|
|
193
193
|
// 2) Tear the node's ENGINE down: send the `shutdown` frame so the broker
|
|
194
194
|
// PROCESS exits and releases the sole .jsonl writer. Then proactively
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
import { InputError } from '../io.js';
|
|
1
2
|
import type { HostHandle } from './host.js';
|
|
3
|
+
/** The daemon's live-broker thresholds. The hard cap is the ledger's admission
|
|
4
|
+
* bound; `warning` is the saturation notice supervision fires below it. Lives
|
|
5
|
+
* here — beside the ledger that enforces it — so the ledger, a refusal
|
|
6
|
+
* message, and supervision's warning all read ONE definition. */
|
|
7
|
+
export declare function brokerThresholdsForDaemon(): {
|
|
8
|
+
warning: number;
|
|
9
|
+
automaticReviveCap: number;
|
|
10
|
+
};
|
|
11
|
+
/** The typed refusal a caller that cannot wait gets when the ledger is full.
|
|
12
|
+
* An InputError so its details round-trip through crtrd's error body and reach
|
|
13
|
+
* a CLI caller as the real cap text instead of an opaque 5xx. */
|
|
14
|
+
export declare function brokerCapReached(cap: number, live: number): InputError;
|
|
2
15
|
/** Real child-exit status, exactly as Node reports it. Codes/signals are
|
|
3
16
|
* diagnostics only — never policy inputs (design D-4). */
|
|
4
17
|
export interface FleetExitStatus {
|
|
@@ -32,8 +45,19 @@ export interface FleetRegistry {
|
|
|
32
45
|
has(nodeId: string): boolean;
|
|
33
46
|
/** Live-fleet checks pass (yield-stall, wedge, fatal-fault). */
|
|
34
47
|
get(nodeId: string): FleetEntry | undefined;
|
|
35
|
-
/** The capacity count
|
|
48
|
+
/** The capacity count: registered brokers only, excluding reservations. */
|
|
36
49
|
size(): number;
|
|
50
|
+
/** Every slot the cap accounts for — registered brokers plus in-flight
|
|
51
|
+
* reservations (a launch that has claimed a slot but not yet registered, and
|
|
52
|
+
* an exited broker whose exit-policy job has not yet run). */
|
|
53
|
+
occupancy(): number;
|
|
54
|
+
/** Claim a slot for `nodeId`. True means the caller may launch; false means
|
|
55
|
+
* the cap is reached and the caller must freeze or refuse. Idempotent — a
|
|
56
|
+
* node that already holds a slot (reserved or registered) gets true without
|
|
57
|
+
* consuming a second one. */
|
|
58
|
+
reserveSlot(nodeId: string): boolean;
|
|
59
|
+
/** Give back a slot claimed by `reserveSlot` and not consumed by a launch. */
|
|
60
|
+
releaseReservation(nodeId: string): void;
|
|
37
61
|
entries(): Iterable<[string, FleetEntry]>;
|
|
38
62
|
/** Drop an entry without policy (terminal exits + close paths). */
|
|
39
63
|
forget(nodeId: string): void;
|
|
@@ -45,6 +69,10 @@ export interface FleetRegistry {
|
|
|
45
69
|
/** Test seam — delivers a synthetic exit through the SAME path as
|
|
46
70
|
* onChildExit, so fabricated-row tests drive policy without a process. */
|
|
47
71
|
deliverExit(nodeId: string, status: FleetExitStatus): void;
|
|
72
|
+
/** The node-lifecycle tick's enactment entry: classify a row known to have no
|
|
73
|
+
* live engine and enact through the SAME policy table exit events use, so
|
|
74
|
+
* tick recovery and exit recovery can never diverge. */
|
|
75
|
+
applyDeadRowPolicy(nodeId: string): Promise<void>;
|
|
48
76
|
}
|
|
49
77
|
/** Inject the daemon's fleet registry. Idempotent for the same instance;
|
|
50
78
|
* throws on a conflicting rebind (one registry per process, ever). */
|
|
@@ -11,6 +11,29 @@
|
|
|
11
11
|
// the structural enforcement that broker launches happen only inside crtrd
|
|
12
12
|
// (design D-2): a CLI or library process has no fleet and cannot launch an
|
|
13
13
|
// unowned broker.
|
|
14
|
+
import { readConfig } from '../config.js';
|
|
15
|
+
import { InputError } from '../io.js';
|
|
16
|
+
import { envMaxLiveBrokers, envWarnLiveBrokers } from '../../shared/env.js';
|
|
17
|
+
/** The daemon's live-broker thresholds. The hard cap is the ledger's admission
|
|
18
|
+
* bound; `warning` is the saturation notice supervision fires below it. Lives
|
|
19
|
+
* here — beside the ledger that enforces it — so the ledger, a refusal
|
|
20
|
+
* message, and supervision's warning all read ONE definition. */
|
|
21
|
+
export function brokerThresholdsForDaemon() {
|
|
22
|
+
const configured = readConfig('user').brokerThresholds;
|
|
23
|
+
const automaticReviveCap = envMaxLiveBrokers() ?? configured.automaticReviveCap;
|
|
24
|
+
const warning = envWarnLiveBrokers() ?? configured.warning;
|
|
25
|
+
return { warning: Math.min(warning, automaticReviveCap), automaticReviveCap };
|
|
26
|
+
}
|
|
27
|
+
/** The typed refusal a caller that cannot wait gets when the ledger is full.
|
|
28
|
+
* An InputError so its details round-trip through crtrd's error body and reach
|
|
29
|
+
* a CLI caller as the real cap text instead of an opaque 5xx. */
|
|
30
|
+
export function brokerCapReached(cap, live) {
|
|
31
|
+
return new InputError({
|
|
32
|
+
error: 'broker_cap_reached',
|
|
33
|
+
message: `no broker slot is free — ${live} live brokers at the cap of ${cap}.`,
|
|
34
|
+
next: `Finish or close a live node (\`crtr canvas render\` shows them), or raise the cap with CRTR_MAX_LIVE_BROKERS, then retry.`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
14
37
|
let bound;
|
|
15
38
|
/** Inject the daemon's fleet registry. Idempotent for the same instance;
|
|
16
39
|
* throws on a conflicting rebind (one registry per process, ever). */
|
|
@@ -22,7 +22,7 @@ import { buildBrokerEnv } from './spawn-env.js';
|
|
|
22
22
|
import { hostExecPath } from './branded-host.js';
|
|
23
23
|
import { isPidAlive, killProcessTreePids, isAnyPidAlive, captureTeardownSnapshot } from '../canvas/pid.js';
|
|
24
24
|
import { assertMandatoryCanvasExtensions } from './canvas-extensions.js';
|
|
25
|
-
import { boundFleet } from './fleet.js';
|
|
25
|
+
import { boundFleet, brokerCapReached, brokerThresholdsForDaemon } from './fleet.js';
|
|
26
26
|
import { emitEvent } from '../events/emit.js';
|
|
27
27
|
import { bindBrokerEventSource, eventSource } from '../events/source.js';
|
|
28
28
|
import { envBrokerEngine } from '../../shared/env.js';
|
|
@@ -193,6 +193,19 @@ export const headlessBrokerHost = {
|
|
|
193
193
|
launch(nodeId, inv, opts) {
|
|
194
194
|
// Fast-fail known launch errors before we detach a broker process.
|
|
195
195
|
preflightBrokerLaunch(nodeId, inv, opts.cwd);
|
|
196
|
+
// Resolve the fleet BEFORE opening any handle: an unbound-fleet launch must
|
|
197
|
+
// fail fast with nothing to clean up (design D-2 — broker launches happen
|
|
198
|
+
// only inside crtrd, or a test harness that explicitly bound a fleet).
|
|
199
|
+
const fleet = boundFleet();
|
|
200
|
+
// The ledger is the ONE admission gate for a broker process: every launch
|
|
201
|
+
// path (revive, spawn, recycle, reset) funnels through here, so claiming
|
|
202
|
+
// the slot before anything is opened — and giving it back on any path that
|
|
203
|
+
// does not end in a registered handle — is what makes the cap real rather
|
|
204
|
+
// than advisory. A caller that cannot proceed without a slot sees this
|
|
205
|
+
// typed refusal; a caller that can wait catches it and freezes instead.
|
|
206
|
+
if (!fleet.reserveSlot(nodeId)) {
|
|
207
|
+
throw brokerCapReached(brokerThresholdsForDaemon().automaticReviveCap, fleet.size());
|
|
208
|
+
}
|
|
196
209
|
// Redirect the detached broker's stdout+stderr to a per-node log under the
|
|
197
210
|
// node's existing job/ dir. It retains arbitrary engine/process residue and
|
|
198
211
|
// failed-canonical fatal fallback output. Append-mode preserves crash-revive history.
|
|
@@ -233,10 +246,6 @@ export const headlessBrokerHost = {
|
|
|
233
246
|
}
|
|
234
247
|
if (eventSource() === undefined)
|
|
235
248
|
bindBrokerEventSource(nodeId);
|
|
236
|
-
// Resolve the fleet BEFORE spawning: an unbound-fleet launch must fail
|
|
237
|
-
// fast with nothing to clean up (design D-2 — broker launches happen only
|
|
238
|
-
// inside crtrd, or a test harness that explicitly bound a fleet).
|
|
239
|
-
const fleet = boundFleet();
|
|
240
249
|
// `--canvas-home` declares broker ownership in argv so the daemon's
|
|
241
250
|
// startup epoch census can match its own canvas' brokers exactly (crtrd's
|
|
242
251
|
// brokerCommandDeclaresCanvasHome) instead of inferring from install
|
|
@@ -244,12 +253,20 @@ export const headlessBrokerHost = {
|
|
|
244
253
|
// daemon's census reaps every prior-epoch broker without adoption (D-8).
|
|
245
254
|
// Both precede the node id: the node id is by contract the FINAL argv
|
|
246
255
|
// token (both broker-cli and the ps census parse it that way).
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
256
|
+
let child;
|
|
257
|
+
try {
|
|
258
|
+
child = spawn(hostExecPath(), [resolveBrokerEntry(), '--canvas-home', crtrHome(), '--epoch', fleet.epoch(), nodeId], {
|
|
259
|
+
cwd: opts.cwd,
|
|
260
|
+
detached: true,
|
|
261
|
+
stdio: ['ignore', logFd, logFd],
|
|
262
|
+
env: childEnv,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
fleet.releaseReservation(nodeId);
|
|
267
|
+
closeSync(logFd);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
253
270
|
// The child holds its own dup of the fd; release the parent's copy so the
|
|
254
271
|
// launching process (CLI or daemon) never leaks it.
|
|
255
272
|
closeSync(logFd);
|
|
@@ -299,6 +316,10 @@ export const headlessBrokerHost = {
|
|
|
299
316
|
fleet.register(nodeId, handle);
|
|
300
317
|
void exited.then((status) => fleet.onChildExit(nodeId, status));
|
|
301
318
|
}
|
|
319
|
+
else {
|
|
320
|
+
// Nothing was registered, so nothing will ever free this slot at exit.
|
|
321
|
+
fleet.releaseReservation(nodeId);
|
|
322
|
+
}
|
|
302
323
|
return handle;
|
|
303
324
|
},
|
|
304
325
|
isAlive(node) {
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* before. ALL runtime/spine/lifecycle/kind prompting now lives as gated builtin
|
|
5
5
|
* memory docs (src/builtin-memory/), spliced in fresh every turn — this scaffold
|
|
6
6
|
* must never duplicate that prose. Imported by launch.ts (its own systemPrompt)
|
|
7
|
-
* and `sys prompt-review` so the two can never drift. */
|
|
7
|
+
* and `sys context prompt-review` so the two can never drift. */
|
|
8
8
|
export declare const LAUNCH_SYSTEM_PROMPT: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// launch-prompt.ts — the minimal static system-prompt scaffold, isolated from
|
|
2
|
-
// launch.ts so CLI consumers (`sys prompt-review`) can import the exact string
|
|
2
|
+
// launch.ts so CLI consumers (`sys context prompt-review`) can import the exact string
|
|
3
3
|
// boot uses WITHOUT pulling launch.ts's canvas-db reach (editorLabel et al.).
|
|
4
4
|
// Pure: zero imports.
|
|
5
5
|
/** The minimal static system prompt every node launches with — a thin launcher
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
* before. ALL runtime/spine/lifecycle/kind prompting now lives as gated builtin
|
|
9
9
|
* memory docs (src/builtin-memory/), spliced in fresh every turn — this scaffold
|
|
10
10
|
* must never duplicate that prose. Imported by launch.ts (its own systemPrompt)
|
|
11
|
-
* and `sys prompt-review` so the two can never drift. */
|
|
11
|
+
* and `sys context prompt-review` so the two can never drift. */
|
|
12
12
|
export const LAUNCH_SYSTEM_PROMPT = 'You are a node in the crtr canvas — an agentic runtime.\n\nGuidelines:\n' +
|
|
13
13
|
'- Use the crtr canvas and memory substrate according to injected context.';
|
|
@@ -32,7 +32,7 @@ export async function resolveLaunchTarget(req, options = {}) {
|
|
|
32
32
|
await assertLaunchKind(kind, cwd, profileId);
|
|
33
33
|
const mode = (req.mode ?? 'base');
|
|
34
34
|
const launch = (await buildLaunchSpecAsync(kind, mode, {
|
|
35
|
-
lifecycle: req.root === true ?
|
|
35
|
+
lifecycle: req.root_lifecycle ?? (req.root === true ? 'resident' : 'terminal'),
|
|
36
36
|
hasManager: req.root !== true,
|
|
37
37
|
...(req.model !== undefined ? { model: req.model } : {}),
|
|
38
38
|
cwd,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NodeMeta, NodeStatus, ExitIntent } from '../canvas/types.js';
|
|
1
|
+
import type { NodeMeta, NodeStatus, ExitIntent, TerminalReason } from '../canvas/types.js';
|
|
2
2
|
/** The lifecycle events — the only vocabulary for moving a node's status/intent.
|
|
3
3
|
* Each maps (in the table below) to a target status and/or intent plus the set
|
|
4
4
|
* of from-statuses it is legal from. */
|
|
@@ -9,7 +9,9 @@ export type LifecycleEvent = 'finish' | 'park' | 'cancel' | 'crash' | 'yield' |
|
|
|
9
9
|
*
|
|
10
10
|
* Throws on an unknown node or an illegal move. The conditional update makes
|
|
11
11
|
* legality authoritative at SQLite write time rather than at a stale read. */
|
|
12
|
-
export declare function transition(nodeId: string, event: LifecycleEvent
|
|
12
|
+
export declare function transition(nodeId: string, event: LifecycleEvent, opts?: {
|
|
13
|
+
reason?: TerminalReason;
|
|
14
|
+
}): NodeMeta;
|
|
13
15
|
/** Whether this row is a PARKED node — terminalized by the unattended clock
|
|
14
16
|
* rather than by finishing its own work. The one read of the park marker:
|
|
15
17
|
* `done` qualified by `intent='parked'`, both written in the same atomic
|
|
@@ -33,6 +33,14 @@
|
|
|
33
33
|
// inside `core/canvas/`).
|
|
34
34
|
import { getNode, cancelCronsOnWake, transitionStatusIntent } from '../canvas/index.js';
|
|
35
35
|
const ANY = '*';
|
|
36
|
+
const TERMINAL_REASONS = {
|
|
37
|
+
finish: 'finished',
|
|
38
|
+
park: 'parked',
|
|
39
|
+
cancel: 'stranded',
|
|
40
|
+
crash: 'crashed',
|
|
41
|
+
'refresh-launch': null,
|
|
42
|
+
revive: null,
|
|
43
|
+
};
|
|
36
44
|
/** The supervised statuses — a live node the daemon watches. */
|
|
37
45
|
const LIVE = ['active', 'idle'];
|
|
38
46
|
/** The legal transition table — derived directly from the (status, intent) pairs
|
|
@@ -69,15 +77,18 @@ const TRANSITIONS = {
|
|
|
69
77
|
*
|
|
70
78
|
* Throws on an unknown node or an illegal move. The conditional update makes
|
|
71
79
|
* legality authoritative at SQLite write time rather than at a stale read. */
|
|
72
|
-
export function transition(nodeId, event) {
|
|
80
|
+
export function transition(nodeId, event, opts) {
|
|
73
81
|
const spec = TRANSITIONS[event];
|
|
74
82
|
const writeStatus = Object.prototype.hasOwnProperty.call(spec, 'status');
|
|
75
83
|
const writeIntent = Object.prototype.hasOwnProperty.call(spec, 'intent');
|
|
84
|
+
const reason = opts?.reason ?? TERMINAL_REASONS[event];
|
|
76
85
|
const fields = {};
|
|
77
86
|
if (writeStatus)
|
|
78
87
|
fields.status = spec.status;
|
|
79
88
|
if (writeIntent)
|
|
80
89
|
fields.intent = spec.intent ?? null;
|
|
90
|
+
if (reason !== undefined)
|
|
91
|
+
fields.terminal_reason = reason;
|
|
81
92
|
// A committed canonical result is protected from a concurrent close.
|
|
82
93
|
const changes = transitionStatusIntent(nodeId, fields, {
|
|
83
94
|
fromStatuses: spec.from === ANY ? undefined : spec.from,
|
|
@@ -8,7 +8,7 @@ import { join } from 'node:path';
|
|
|
8
8
|
// Import the pure path helper from paths.ts directly, NOT the canvas/index.js
|
|
9
9
|
// barrel: the barrel re-exports `openDb`/canvas.ts accessors, so pulling it here
|
|
10
10
|
// would drag canvas.db into the runtime graph of every pure-client CLI leaf that
|
|
11
|
-
// resolves memory docs (e.g. `memory origin`, `sys prompt-review`). Stage B-9.
|
|
11
|
+
// resolves memory docs (e.g. `memory origin`, `sys context prompt-review`). Stage B-9.
|
|
12
12
|
import { contextDir } from '../canvas/paths.js';
|
|
13
13
|
/** The node-local memory directory in a node's context dir — holds substrate
|
|
14
14
|
* docs (.md files with kind/when/why frontmatter). */
|
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
// mutating the live pointer — the explicit operation must not retract a
|
|
39
39
|
// canonical final unless it can actually enact the requested re-task.
|
|
40
40
|
import { join } from 'node:path';
|
|
41
|
-
import { clearFinalReportIfMatches, getFinalReport, reportsDir, updateNode } from '../canvas/index.js';
|
|
41
|
+
import { clearFinalReportIfMatches, getFinalReport, getNode, reportsDir, updateNode } from '../canvas/index.js';
|
|
42
42
|
import { InputError } from '../io.js';
|
|
43
|
+
import { transition } from './lifecycle.js';
|
|
43
44
|
function currentFinalReport(nodeId) {
|
|
44
45
|
return getFinalReport(nodeId);
|
|
45
46
|
}
|
|
@@ -106,6 +107,14 @@ export function commitReopen(nodeId, expectedFinalReport) {
|
|
|
106
107
|
export function commitReopenResident(nodeId) {
|
|
107
108
|
const finalReport = currentFinalReport(nodeId);
|
|
108
109
|
updateNode(nodeId, { lifecycle: 'resident' });
|
|
110
|
+
// A reopened node that is still parked in a terminal status has to come back
|
|
111
|
+
// to `active`, or its row keeps claiming it is finished while a broker runs
|
|
112
|
+
// its new mandate. Gated on terminal so a message landing on a live node
|
|
113
|
+
// never rewrites the intent it is working under.
|
|
114
|
+
const meta = getNode(nodeId);
|
|
115
|
+
if (meta !== null && (meta.status === 'done' || meta.status === 'canceled' || meta.status === 'dead')) {
|
|
116
|
+
transition(nodeId, 'revive');
|
|
117
|
+
}
|
|
109
118
|
if (finalReport !== null)
|
|
110
119
|
commitReopen(nodeId, finalReport);
|
|
111
120
|
}
|
|
@@ -50,7 +50,7 @@ export function reapDescendants(rootId) {
|
|
|
50
50
|
// sole .jsonl writer; tearDownNode then proactively closes the on-screen
|
|
51
51
|
// viewer pane + registry row (attach auto-reconnects, so the viewer must be
|
|
52
52
|
// closed here or it lingers ~30s in a misleading "reconnecting…" state).
|
|
53
|
-
transition(id, 'cancel');
|
|
53
|
+
transition(id, 'cancel', { reason: 'closed' });
|
|
54
54
|
headlessBrokerHost.teardown(id);
|
|
55
55
|
tearDownNode(id);
|
|
56
56
|
reaped.push(id);
|
|
@@ -52,6 +52,9 @@ export interface ReviveAllResult {
|
|
|
52
52
|
node_id: string;
|
|
53
53
|
error: string;
|
|
54
54
|
}[];
|
|
55
|
+
/** Node ids the cap turned away. They are not failures: each row is frozen
|
|
56
|
+
* and the daemon's tick relaunches it as slots free. */
|
|
57
|
+
frozen: string[];
|
|
55
58
|
}
|
|
56
59
|
/** RESUME every disconnected node. reviveNode is the ONLY sanctioned launcher
|
|
57
60
|
* and self-guards the double-spawn (a node whose broker pid is already live is a
|
|
@@ -92,13 +92,19 @@ export function listDisconnected() {
|
|
|
92
92
|
* with resume:true. One failing node never aborts the sweep — its error is
|
|
93
93
|
* collected and the rest proceed. */
|
|
94
94
|
export function reviveAll() {
|
|
95
|
-
const result = { revived: [], failed: [] };
|
|
95
|
+
const result = { revived: [], failed: [], frozen: [] };
|
|
96
96
|
for (const meta of listDisconnected()) {
|
|
97
97
|
try {
|
|
98
98
|
// Reconnecting a disconnected broker is recovery, not a wake — a node
|
|
99
|
-
// waiting on an armed deadline keeps it across the sweep.
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
// waiting on an armed deadline keeps it across the sweep. A sweep over a
|
|
100
|
+
// whole canvas is the likeliest way to meet the cap, and every node in it
|
|
101
|
+
// is one the daemon wants back: freeze the overflow and let the tick
|
|
102
|
+
// bring them up as slots free.
|
|
103
|
+
const outcome = reviveNode(meta.node_id, { resume: true, recovery: true, capacity: 'freeze' }).outcome;
|
|
104
|
+
if (outcome === 'frozen')
|
|
105
|
+
result.frozen.push(meta.node_id);
|
|
106
|
+
else
|
|
107
|
+
result.revived.push(meta.node_id);
|
|
102
108
|
}
|
|
103
109
|
catch (err) {
|
|
104
110
|
result.failed.push({ node_id: meta.node_id, error: err.message });
|
|
@@ -51,7 +51,18 @@ export interface ReviveResult {
|
|
|
51
51
|
* Present only when a fresh launch actually happened (absent on the
|
|
52
52
|
* double-revive no-op, where nothing was cleared or relaunched). */
|
|
53
53
|
faultExplanation?: () => string | null;
|
|
54
|
+
/** What this call actually did. `already-live` means a broker was already
|
|
55
|
+
* running for the node and nothing was launched; `frozen` means no slot was
|
|
56
|
+
* free and the row is now waiting for one — the tick thaws it. */
|
|
57
|
+
outcome: ReviveOutcome;
|
|
54
58
|
}
|
|
59
|
+
export type ReviveOutcome = 'launched' | 'already-live' | 'frozen';
|
|
60
|
+
/** What a caller wants when the broker cap is full. `freeze` suits a caller
|
|
61
|
+
* whose reason to run survives waiting (durable mail, a cron sink, a recovery
|
|
62
|
+
* respawn): the row is marked frozen and the tick relaunches it when a slot
|
|
63
|
+
* frees. `refuse` suits a caller that must have a live broker NOW (an attach,
|
|
64
|
+
* a bridge, a companion bind) and can only report the refusal. */
|
|
65
|
+
export type ReviveCapacityPolicy = 'freeze' | 'refuse';
|
|
55
66
|
/** Relaunch `nodeId`'s broker engine from its persisted recipe and update canvas
|
|
56
67
|
* meta. Opens no viewer (engine-only).
|
|
57
68
|
*
|
|
@@ -66,6 +77,7 @@ export interface ReviveResult {
|
|
|
66
77
|
*/
|
|
67
78
|
export declare function reviveNode(nodeId: string, opts: {
|
|
68
79
|
resume: boolean;
|
|
80
|
+
capacity: ReviveCapacityPolicy;
|
|
69
81
|
wakeReason?: ReviveWakeReason;
|
|
70
82
|
recovery?: boolean;
|
|
71
83
|
}): ReviveResult;
|