@north-light/crouter 0.3.236 → 0.3.238
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 +10 -2
- 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 +442 -442
- package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +6 -0
- package/dist/clients/inbox/review/roundtrip.js +1 -1
- 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 +1 -1
- package/dist/commands/node/lifecycle.js +1 -1
- 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__/child-death-wake.test.js +38 -22
- 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__/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 +7 -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 +20 -1
- package/dist/core/feed/feed.js +1 -1
- package/dist/core/human/feedback-companion.js +5 -2
- package/dist/core/keybindings/catalog.d.ts +1 -1
- package/dist/core/keybindings/catalog.js +6 -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/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 +4 -0
- package/dist/core/runtime/spawn.js +36 -7
- package/dist/core/runtime/tmux-bindings.js +4 -0
- 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 -1
- 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/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 → cron-self-cancel-delivery.test.d.ts} +0 -0
|
@@ -18,7 +18,7 @@ import { RemoteCanvasSource } from './remote-canvas-source.js';
|
|
|
18
18
|
import { fullName } from './labels.js';
|
|
19
19
|
import { jobDir, contextDir } from './paths.js';
|
|
20
20
|
import { isPidAlive } from './pid.js';
|
|
21
|
-
import { resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
21
|
+
import { isFrozenRow, resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
22
22
|
import { readFault } from '../runtime/fault.js';
|
|
23
23
|
import { parseCard } from '../../shared/generated-context.js';
|
|
24
24
|
// Active-fault overlay: stale daemon-owned provider faults are hidden once the
|
|
@@ -63,7 +63,7 @@ async function nodeLineFromSource(source, nodeId, indent, connector, remote, tic
|
|
|
63
63
|
if (node === null)
|
|
64
64
|
return `${indent}${connector}? <missing meta: ${nodeId}>`;
|
|
65
65
|
const hanging = remote ? null : hangingFor(node);
|
|
66
|
-
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
66
|
+
const glyph = resolveNodeVisual(node.status, { hanging, frozen: isFrozenRow(node) }).glyph;
|
|
67
67
|
const ctx = remote ? '0k' : fmtCtx(readNodeTelemetry(nodeId).tokens_in);
|
|
68
68
|
const asks = tickets[nodeId] ?? 0;
|
|
69
69
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
@@ -114,7 +114,7 @@ export async function renderTreeFromSource(source, rootId) {
|
|
|
114
114
|
const asks = tickets[rootId] ?? 0;
|
|
115
115
|
const askSuffix = asks > 0 ? ` ⚑${asks}` : '';
|
|
116
116
|
const hanging = remote ? null : hangingFor(node);
|
|
117
|
-
const glyph = resolveNodeVisual(node.status, { hanging }).glyph;
|
|
117
|
+
const glyph = resolveNodeVisual(node.status, { hanging, frozen: isFrozenRow(node) }).glyph;
|
|
118
118
|
const faultSuffix = hanging !== null ? ` · ${faultSummary(hanging)}` : '';
|
|
119
119
|
const out = [];
|
|
120
120
|
out.push(`${glyph} ${fullName(node)} [${node.kind}/${node.mode}] ctx ${ctx}${askSuffix}${faultSuffix}`);
|
|
@@ -388,6 +388,8 @@ export function dashboardRowFromNodeRow(row, focusedNodeIds, remote) {
|
|
|
388
388
|
name: row.name, // handle only; enrichRow upgrades to fullName (meta.description)
|
|
389
389
|
handle: row.name,
|
|
390
390
|
status: row.status,
|
|
391
|
+
frozen_at: row.frozen_at ?? null,
|
|
392
|
+
terminal_reason: row.terminal_reason ?? null,
|
|
391
393
|
kind: row.kind,
|
|
392
394
|
mode: row.mode,
|
|
393
395
|
ctx_tokens: 0, // lazy: enrichRow
|
|
@@ -423,6 +425,8 @@ export async function dashboardRowsFromSource(source, rootId) {
|
|
|
423
425
|
handle: node.name,
|
|
424
426
|
description: node.description,
|
|
425
427
|
status: node.status,
|
|
428
|
+
frozen_at: node.frozen_at ?? null,
|
|
429
|
+
terminal_reason: node.terminal_reason ?? null,
|
|
426
430
|
kind: node.kind,
|
|
427
431
|
mode: node.mode,
|
|
428
432
|
ctx_tokens: tel.tokens_in ?? 0,
|
|
@@ -15,10 +15,19 @@ export interface NodeVisual {
|
|
|
15
15
|
word?: string;
|
|
16
16
|
bold?: boolean;
|
|
17
17
|
}
|
|
18
|
+
/** Whether a row is frozen: the broker ledger marked it waiting for a slot and
|
|
19
|
+
* no process holds it. Both halves matter — a launch clears `frozen_at`, so a
|
|
20
|
+
* mark sitting beside a live pid is stale rather than frozen. A projection
|
|
21
|
+
* that carries no pid at all (the dashboard row) reads on the mark alone. */
|
|
22
|
+
export declare function isFrozenRow(row: {
|
|
23
|
+
frozen_at?: string | null;
|
|
24
|
+
pi_pid?: number | null;
|
|
25
|
+
}): boolean;
|
|
18
26
|
/** Resolve a node's glyph + hue + overlay word, applying the precedence
|
|
19
|
-
* hanging > streaming > status. */
|
|
27
|
+
* hanging > frozen > streaming > status. */
|
|
20
28
|
export declare function resolveNodeVisual(status: NodeStatus, opts: {
|
|
21
29
|
streaming?: boolean;
|
|
30
|
+
frozen?: boolean;
|
|
22
31
|
hanging?: Fault | {
|
|
23
32
|
kind: FaultKind;
|
|
24
33
|
} | null;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// status-glyph.ts — the canonical status glyph and color map used by the canvas
|
|
2
2
|
// ASCII forest and interactive browser. The overlay precedence is
|
|
3
|
-
// hanging > streaming > status. `hanging` and `streaming` are
|
|
3
|
+
// hanging > frozen > streaming > status. `hanging` and `streaming` are
|
|
4
4
|
// mutually exclusive in practice (hanging means the turn ended, so `busy` is
|
|
5
|
-
// gone) but the precedence is defined regardless.
|
|
5
|
+
// gone) but the precedence is defined regardless. `frozen` outranks `streaming`
|
|
6
|
+
// because a frozen row has no process at all: any streaming read against it is
|
|
7
|
+
// a stale marker, and showing it as active is the lie the freeze state exists
|
|
8
|
+
// to stop telling.
|
|
6
9
|
import { FAULT_QUIET_MS } from '../runtime/fault.js';
|
|
7
10
|
// ── Status glyphs (the mono, color-free primary encoding) ─────────────────────
|
|
8
11
|
export const STATUS_GLYPH = {
|
|
@@ -24,6 +27,9 @@ export const STATUS_COLOR = {
|
|
|
24
27
|
// ── Streaming overlay (genuinely mid-turn) — browse's existing statusRail visual.
|
|
25
28
|
const STREAMING_GLYPH = '⟳';
|
|
26
29
|
const STREAMING_COLOR = 92; // bright green (brighter than active-status green)
|
|
30
|
+
// ── Frozen overlay (waiting on a broker slot: marked, but no process). ───────
|
|
31
|
+
const FROZEN_GLYPH = '◌';
|
|
32
|
+
const FROZEN_COLOR = 94; // bright blue
|
|
27
33
|
// ── Active-fault overlay (one glyph, kind label carries the detail). ─────────
|
|
28
34
|
export const HANGING_GLYPH = '⚠';
|
|
29
35
|
export const HANGING_COLOR = 33; // yellow
|
|
@@ -42,12 +48,22 @@ export function hangingLabel(kind) {
|
|
|
42
48
|
default: return 'other';
|
|
43
49
|
}
|
|
44
50
|
}
|
|
51
|
+
/** Whether a row is frozen: the broker ledger marked it waiting for a slot and
|
|
52
|
+
* no process holds it. Both halves matter — a launch clears `frozen_at`, so a
|
|
53
|
+
* mark sitting beside a live pid is stale rather than frozen. A projection
|
|
54
|
+
* that carries no pid at all (the dashboard row) reads on the mark alone. */
|
|
55
|
+
export function isFrozenRow(row) {
|
|
56
|
+
return row.frozen_at != null && row.pi_pid == null;
|
|
57
|
+
}
|
|
45
58
|
/** Resolve a node's glyph + hue + overlay word, applying the precedence
|
|
46
|
-
* hanging > streaming > status. */
|
|
59
|
+
* hanging > frozen > streaming > status. */
|
|
47
60
|
export function resolveNodeVisual(status, opts) {
|
|
48
61
|
if (opts.hanging != null) {
|
|
49
62
|
return { glyph: HANGING_GLYPH, color: HANGING_COLOR, word: hangingLabel(opts.hanging.kind), bold: true };
|
|
50
63
|
}
|
|
64
|
+
if (opts.frozen === true) {
|
|
65
|
+
return { glyph: FROZEN_GLYPH, color: FROZEN_COLOR, word: 'frozen' };
|
|
66
|
+
}
|
|
51
67
|
if (opts.streaming === true) {
|
|
52
68
|
return { glyph: STREAMING_GLYPH, color: STREAMING_COLOR, word: 'live', bold: true };
|
|
53
69
|
}
|
|
@@ -18,6 +18,7 @@ export type Lifecycle = 'terminal' | 'resident';
|
|
|
18
18
|
export type Mode = 'base' | 'orchestrator';
|
|
19
19
|
/** Why a node last stopped — drives the daemon's reap-vs-revive decision. */
|
|
20
20
|
export type ExitIntent = 'done' | 'refresh' | 'idle-release' | 'parked' | null;
|
|
21
|
+
export type TerminalReason = 'finalized' | 'finished' | 'parked' | 'closed' | 'retired' | 'crashed' | 'stranded';
|
|
21
22
|
/** The two structural edges. `subscribes_to` is the load-bearing spine (flow,
|
|
22
23
|
* org chart, views, completion routing). `spawned_by` is audit only. */
|
|
23
24
|
export type EdgeType = 'subscribes_to' | 'spawned_by';
|
|
@@ -48,7 +49,7 @@ export interface LaunchSpec {
|
|
|
48
49
|
env: Record<string, string>;
|
|
49
50
|
}
|
|
50
51
|
export interface ManagedWorktree {
|
|
51
|
-
state: 'open' | 'closed';
|
|
52
|
+
state: 'open' | 'closed' | 'abandoned';
|
|
52
53
|
/** A closed worktree remains registered until its owner has exited, so Git
|
|
53
54
|
* never deletes the caller's live checkout beneath it. */
|
|
54
55
|
cleanup?: 'pending' | 'complete';
|
|
@@ -59,6 +60,12 @@ export interface ManagedWorktree {
|
|
|
59
60
|
base_sha: string;
|
|
60
61
|
created: string;
|
|
61
62
|
closed?: string;
|
|
63
|
+
abandoned?: {
|
|
64
|
+
branch: string;
|
|
65
|
+
tip_sha: string;
|
|
66
|
+
at: string;
|
|
67
|
+
by: string;
|
|
68
|
+
};
|
|
62
69
|
}
|
|
63
70
|
/** Immutable companion provenance. The origin session/leaf/model coordinates
|
|
64
71
|
* remain canonical in the review row rather than being duplicated into node
|
|
@@ -181,6 +188,12 @@ export interface NodeRuntime {
|
|
|
181
188
|
/** Controller whose next inbox delivery permits this terminal node to sleep.
|
|
182
189
|
* Mutable durable runtime state: ancestry never implies a wait. */
|
|
183
190
|
waiting_for?: string | null;
|
|
191
|
+
frozen_at?: string | null;
|
|
192
|
+
respawn_failures?: number;
|
|
193
|
+
respawn_not_before?: string | null;
|
|
194
|
+
launched_at?: string | null;
|
|
195
|
+
last_settled_at?: string | null;
|
|
196
|
+
terminal_reason?: TerminalReason | null;
|
|
184
197
|
/** Immutable basename of the committed canonical final report, or null. */
|
|
185
198
|
final_report?: string | null;
|
|
186
199
|
/** ISO timestamp carried by the committed canonical final report, or null. */
|
|
@@ -244,6 +257,12 @@ export interface NodeRow {
|
|
|
244
257
|
/** Authoritative runtime columns (see NodeRuntime). */
|
|
245
258
|
intent: ExitIntent;
|
|
246
259
|
waiting_for: string | null;
|
|
260
|
+
frozen_at?: string | null;
|
|
261
|
+
respawn_failures?: number;
|
|
262
|
+
respawn_not_before?: string | null;
|
|
263
|
+
launched_at?: string | null;
|
|
264
|
+
last_settled_at?: string | null;
|
|
265
|
+
terminal_reason?: TerminalReason | null;
|
|
247
266
|
final_report?: string | null;
|
|
248
267
|
finalized_at?: string | null;
|
|
249
268
|
pi_pid: number | null;
|
package/dist/core/feed/feed.js
CHANGED
|
@@ -91,7 +91,7 @@ function commitCanonicalFinal(nodeId, staged) {
|
|
|
91
91
|
const subscribers = db.prepare(`SELECT from_id AS node_id, active, created FROM edges WHERE type = 'subscribes_to' AND to_id = ? ORDER BY created`).all(nodeId).map((row) => ({ node_id: row['node_id'], active: row['active'] === 1, created: row['created'] }));
|
|
92
92
|
install(staged);
|
|
93
93
|
installed = true;
|
|
94
|
-
db.prepare("UPDATE nodes SET status = 'done', intent = 'done', final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL").run(staged.basename, staged.ts, nodeId);
|
|
94
|
+
db.prepare("UPDATE nodes SET status = 'done', intent = 'done', terminal_reason = 'finalized', final_report = ?, finalized_at = ? WHERE node_id = ? AND final_report IS NULL").run(staged.basename, staged.ts, nodeId);
|
|
95
95
|
db.exec('COMMIT');
|
|
96
96
|
return { subscribers };
|
|
97
97
|
}
|
|
@@ -40,7 +40,7 @@ function materializeFeedbackBranch(dir, capture) {
|
|
|
40
40
|
* the first-comment race to a concurrently recorded companion. */
|
|
41
41
|
export function discardFeedbackCompanion(nodeId) {
|
|
42
42
|
try {
|
|
43
|
-
transition(nodeId, 'cancel');
|
|
43
|
+
transition(nodeId, 'cancel', { reason: 'retired' });
|
|
44
44
|
headlessBrokerHost.teardown(nodeId);
|
|
45
45
|
}
|
|
46
46
|
catch { /* the refusal is authoritative; a straggler node is inert */ }
|
|
@@ -88,7 +88,10 @@ export async function realizeFeedbackCompanion(args) {
|
|
|
88
88
|
});
|
|
89
89
|
try {
|
|
90
90
|
initializeForkContextExposure(node.node_id, args.senderNodeId);
|
|
91
|
-
|
|
91
|
+
// The bind deadline below only a live broker can meet, so a capacity
|
|
92
|
+
// refusal must surface here and discard the companion — freezing would
|
|
93
|
+
// burn the whole deadline waiting for a launch that is not coming.
|
|
94
|
+
reviveNode(node.node_id, { resume: false, capacity: 'refuse' });
|
|
92
95
|
if (!await awaitCompanionBind(node.node_id, COMPANION_BIND_DEADLINE_MS)) {
|
|
93
96
|
throw new Error(`feedback companion did not bind within ${COMPANION_BIND_DEADLINE_MS}ms`);
|
|
94
97
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BindingDefinition, SafetyRequirement } from './types.js';
|
|
2
2
|
import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
|
|
3
|
-
type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
|
|
3
|
+
type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'context-admin' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
|
|
4
4
|
type TmuxMenuEntryAction = Exclude<TmuxMenuAction, 'open' | 'attach'> | `issue.${'idea' | 'bug'}`;
|
|
5
5
|
type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
|
|
6
6
|
type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
|
|
@@ -55,7 +55,11 @@ export const BINDING_CATALOG = Object.freeze([
|
|
|
55
55
|
...menuEntry('metadata', 'Show metadata', 'Show the focused node’s metadata dossier.', ['m'], ['tmux.menu.show']),
|
|
56
56
|
...menuEntry('context', 'Browse context', 'Browse the focused node context.', ['c'], ['tmux.menu.show']),
|
|
57
57
|
...menuEntry('providers', 'Open settings', 'Open the crouter settings panel.', [','], ['tmux.menu']),
|
|
58
|
-
|
|
58
|
+
// j and k steer the menu cursor, so no selector may claim either.
|
|
59
|
+
...menuEntry('plugins', 'Browse plugins', 'Open the crouter plugin browser.', ['l'], ['tmux.menu']),
|
|
60
|
+
// Lowercase c opens the create submenu, so the context surface takes the
|
|
61
|
+
// shifted form of the word its command tree already uses.
|
|
62
|
+
...menuEntry('context-admin', 'Context surface', 'Open the context surface: what memory each event delivers, and the surfaces, gates, and caps that decide it.', ['shift+c'], ['tmux.menu']),
|
|
59
63
|
...menuEntry('graph', 'Canvas graph', 'Open the canvas graph.', ['g'], ['tmux.menu']),
|
|
60
64
|
...menuEntry('focus-manager', 'Go to manager', 'Focus this node’s manager.', ['h'], ['tmux.menu']),
|
|
61
65
|
...menuEntry('copy-id', 'Copy node id', 'Copy the focused node id to the system clipboard.', ['y'], ['tmux.menu']),
|
|
@@ -70,7 +74,7 @@ export const BINDING_CATALOG = Object.freeze([
|
|
|
70
74
|
...attachEntry('crtr.tmux.menu.attach.detach', 'crtr.attach.detach', 'Detach', 'Detach from the node.', ['d'], ['ctrl+d']),
|
|
71
75
|
...attachEntry('crtr.tmux.menu.attach.clear-or-detach', 'crtr.attach.clear-or-detach', 'Clear input (detach if empty)', 'Clear input or detach when input is empty.', ['c'], ['ctrl+c']),
|
|
72
76
|
...attachEntry('crtr.tmux.menu.attach.graph.toggle', 'crtr.attach.graph.toggle', 'Toggle graph', 'Open or close the graph overlay.', ['g'], ['alt+g']),
|
|
73
|
-
...attachEntry('crtr.tmux.menu.attach.help.toggle', 'crtr.attach.help.toggle', 'Help', 'Open the attach help overlay.', ['
|
|
77
|
+
...attachEntry('crtr.tmux.menu.attach.help.toggle', 'crtr.attach.help.toggle', 'Help', 'Open the attach help overlay.', ['?'], ['alt+shift+k']),
|
|
74
78
|
...attachEntry('crtr.tmux.menu.attach.inbox.toggle', 'crtr.attach.inbox.toggle', 'Toggle inbox', 'Open or close the human inbox surface in this viewer.', ['i'], []),
|
|
75
79
|
...attachEntry('crtr.tmux.menu.attach.inbox-strip', 'crtr.attach.inbox-strip', 'Pending requests', 'Jump to the pending human-request strip at the top of the pane.', ['u'], ['alt+u']),
|
|
76
80
|
...attachEntry('crtr.tmux.menu.attach.model-ladder.next', 'crtr.attach.model-ladder.next', 'Next model', 'Move to the next model ladder rung.', [']'], ['alt+m']),
|
|
@@ -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). */
|