@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
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
// `crtr sys context resolve` — the forward question: given a configuration
|
|
2
|
+
// snapshot and one surface event, what does the memory substrate deliver, at
|
|
3
|
+
// which rung, and why. Reads the same plan the live renderers deliver from
|
|
4
|
+
// (`planDelivery`, src/core/substrate/plan.ts), so a reported delivery can
|
|
5
|
+
// never differ from an actual one.
|
|
6
|
+
//
|
|
7
|
+
// The snapshot builders and record formatters here are shared with
|
|
8
|
+
// `sys context doc`, the reverse half of the same read surface.
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { resolve as resolvePath } from 'node:path';
|
|
11
|
+
import { defineLeaf } from '../../../core/command.js';
|
|
12
|
+
import { InputError } from '../../../core/io.js';
|
|
13
|
+
import { ApiError } from '../../../api/index.js';
|
|
14
|
+
import { cliClient } from '../../api-client.js';
|
|
15
|
+
import { envNodeId, envProfileId } from '../../../shared/env.js';
|
|
16
|
+
import { realpathOrSelf } from '../../../core/fs-utils.js';
|
|
17
|
+
import { ambientMemoryTarget, resolveMemoryDocForTarget, } from '../../../core/memory-resolver.js';
|
|
18
|
+
import { planDelivery, } from '../../../core/substrate/plan.js';
|
|
19
|
+
import { emptyContextExposureState, exposureTarget } from '../../../core/substrate/injected-store.js';
|
|
20
|
+
import { renderPreferencesForSubject, renderKnowledgeForSubject } from '../../../core/substrate/render.js';
|
|
21
|
+
import { memoryReadDocBlocks, renderOnCommandDocsForSubject, renderOnReadDocsForSubject, renderPreCommandDocsForSubject, renderWorkspaceOpenDocsForSubject, } from '../../../core/substrate/on-read.js';
|
|
22
|
+
import { clearSessionCache } from '../../../core/substrate/session-cache.js';
|
|
23
|
+
import { rungAtLeast, SURFACE_EVENTS, } from '../../../core/substrate/schema.js';
|
|
24
|
+
import { scopeForCwd, profileNameFor } from '../../../core/substrate/subject-fields.js';
|
|
25
|
+
import { resetScopeCache } from '../../../core/scope.js';
|
|
26
|
+
export const resolveLeaf = defineLeaf({
|
|
27
|
+
name: 'resolve',
|
|
28
|
+
description: 'resolve what a configuration snapshot loads for one surface event',
|
|
29
|
+
whenToUse: 'you are asking what a node (or a hypothetical configuration) actually receives — at boot, on opening a workspace, on reading a file, on a memory read, or around a command',
|
|
30
|
+
help: {
|
|
31
|
+
name: 'sys context resolve',
|
|
32
|
+
summary: 'resolve the documents a configuration snapshot loads for one surface event',
|
|
33
|
+
params: [
|
|
34
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Resolve for this live node — derives kind, mode, lifecycle, has-manager, profile, and cwd from it. A node id that does not exist degrades to a synthetic subject and says so.' },
|
|
35
|
+
{ kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Node kind of the synthetic subject. Defaults to the --node value, else `general`.' },
|
|
36
|
+
{ kind: 'flag', name: 'mode', type: 'enum', choices: ['base', 'orchestrator'], required: false, constraint: 'Node mode of the synthetic subject. Defaults to `base`.' },
|
|
37
|
+
{ kind: 'flag', name: 'lifecycle', type: 'enum', choices: ['terminal', 'resident'], required: false, constraint: 'Node lifecycle of the synthetic subject. Defaults to `terminal` for base, `resident` for orchestrator.' },
|
|
38
|
+
{ kind: 'flag', name: 'has-manager', type: 'enum', choices: ['true', 'false'], required: false, constraint: 'Treat the subject as having a manager (a subscriber above it). Defaults to the --node value, else true.' },
|
|
39
|
+
{ kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Profile id whose memory store and project-memory caps apply. Defaults to the selected profile.' },
|
|
40
|
+
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Directory the project stores resolve from. Defaults to the current directory.' },
|
|
41
|
+
{ kind: 'flag', name: 'event', type: 'enum', choices: [...SURFACE_EVENTS], required: false, constraint: 'The surface event to resolve. Defaults to `boot`. Ignored when --all-events is set.' },
|
|
42
|
+
{ kind: 'flag', name: 'all-events', type: 'bool', required: false, constraint: 'Resolve every surface event instead of one, and report each plan separately.' },
|
|
43
|
+
{ kind: 'flag', name: 'file', type: 'path', required: false, constraint: 'Payload for `--event read`: the file that was read. Without it, no read entry matches.' },
|
|
44
|
+
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Payload for `--event memory-read`: the exact canonical name the read resolved to.' },
|
|
45
|
+
{ kind: 'flag', name: 'command', type: 'string', required: false, constraint: 'Payload for `--event command` / `--event pre-command`: the command string.' },
|
|
46
|
+
{ kind: 'flag', name: 'granularity', type: 'enum', choices: ['summary', 'docs', 'assembled'], required: false, constraint: '`summary` folds to per-rung counts; `docs` reports one row per document; `assembled` returns the real rendered text by calling the event\u2019s renderer. Defaults to `docs`. `--all` applies only at `docs` granularity; with `assembled` it is reported as ignored.' },
|
|
47
|
+
{ kind: 'flag', name: 'all', type: 'bool', required: false, constraint: 'At `docs` granularity, include every considered document, not just the ones that deliver — shadowed, gated-out, and capped-to-none documents with the reason each was excluded. Ignored at `summary` and `assembled` granularity.' },
|
|
48
|
+
],
|
|
49
|
+
output: [
|
|
50
|
+
{ name: 'event', type: 'string', required: false, constraint: 'The resolved event. Absent with --all-events, where every entry in `plans` carries its own.' },
|
|
51
|
+
{ name: 'subject', type: 'object', required: true, constraint: 'The configuration snapshot used: kind, mode, lifecycle, hasManager, profile, scope.' },
|
|
52
|
+
{ name: 'target', type: 'object', required: true, constraint: 'Store resolution inputs: cwd, profileId, nodeId.' },
|
|
53
|
+
{ name: 'docs', type: 'array', required: false, constraint: 'One entry per document, at `docs`/`all` granularity: name, kind, scope, store_path, authored_rung, project_memory_cap, capped_rung, final_rung, winner, shadowed_by, doc_gate, entry_gate, matched_entry, and excluded when --all includes a non-delivering document.' },
|
|
54
|
+
{ name: 'counts', type: 'object', required: false, constraint: 'At `summary` granularity: {considered, delivered: {content, preview, name}, delivered_total, excluded: {<reason>: count}}.' },
|
|
55
|
+
{ name: 'all_ignored', type: 'boolean', required: false, constraint: 'Present only when --all is supplied with --granularity assembled; true means assembled text is unchanged because --all applies only to docs granularity.' },
|
|
56
|
+
{ name: 'assembled', type: 'string', required: false, constraint: 'At `assembled` granularity: the exact text the node would receive for this event.' },
|
|
57
|
+
{ name: 'plans', type: 'array', required: false, constraint: 'With --all-events: one plan result per surface event, each shaped as above.' },
|
|
58
|
+
{ name: 'node_missing', type: 'boolean', required: false, constraint: 'Present when --node named a node that does not exist and a synthetic subject was used instead.' },
|
|
59
|
+
],
|
|
60
|
+
outputKind: 'object',
|
|
61
|
+
effects: ['None. Read-only.'],
|
|
62
|
+
},
|
|
63
|
+
run: async (input) => {
|
|
64
|
+
const snapshot = await buildSnapshot(input);
|
|
65
|
+
const granularity = input['granularity'] ?? 'docs';
|
|
66
|
+
const includeAll = input['all'] === true;
|
|
67
|
+
const events = input['allEvents'] === true
|
|
68
|
+
? [...SURFACE_EVENTS]
|
|
69
|
+
: [input['event'] ?? 'boot'];
|
|
70
|
+
const flags = {
|
|
71
|
+
file: optionalString(input['file']),
|
|
72
|
+
readName: optionalString(input['name']),
|
|
73
|
+
command: optionalString(input['command']),
|
|
74
|
+
};
|
|
75
|
+
const sections = events.map((event) => planSection(snapshot, event, payloadFor(event, flags, snapshot.target), granularity, includeAll));
|
|
76
|
+
const head = {
|
|
77
|
+
subject: subjectSummary(snapshot.subject),
|
|
78
|
+
target: snapshot.target,
|
|
79
|
+
...(snapshot.nodeMissing ? { node_missing: true } : {}),
|
|
80
|
+
...(granularity === 'assembled' && includeAll ? { all_ignored: true } : {}),
|
|
81
|
+
};
|
|
82
|
+
return events.length === 1 ? { ...head, ...sections[0] } : { ...head, plans: sections };
|
|
83
|
+
},
|
|
84
|
+
render: (result) => renderResolve(result),
|
|
85
|
+
});
|
|
86
|
+
/** Build the subject and store target from `--node` and the axis flags. A flag
|
|
87
|
+
* always wins over the value derived from the node, so a live node is a
|
|
88
|
+
* starting point an engineer can dial away from. */
|
|
89
|
+
export async function buildSnapshot(input) {
|
|
90
|
+
const nodeId = optionalString(input['node']);
|
|
91
|
+
const node = await fetchNodeOrNull(nodeId);
|
|
92
|
+
const derived = node === null ? null : await fetchSubjectOrNull(node.node_id);
|
|
93
|
+
const mode = input['mode'] ?? derived?.mode ?? 'base';
|
|
94
|
+
const lifecycle = input['lifecycle']
|
|
95
|
+
?? derived?.lifecycle
|
|
96
|
+
?? (mode === 'orchestrator' ? 'resident' : 'terminal');
|
|
97
|
+
const cwd = resolveCwd(optionalString(input['cwd']) ?? derived?.cwd ?? process.cwd());
|
|
98
|
+
const profileId = optionalString(input['profile']) ?? node?.profile_id ?? ambientMemoryTarget().profileId;
|
|
99
|
+
const hasManagerFlag = optionalString(input['hasManager']);
|
|
100
|
+
return {
|
|
101
|
+
subject: {
|
|
102
|
+
kind: optionalString(input['kind']) ?? derived?.kind ?? 'general',
|
|
103
|
+
mode,
|
|
104
|
+
lifecycle,
|
|
105
|
+
hasManager: hasManagerFlag === null ? derived?.hasManager ?? true : hasManagerFlag === 'true',
|
|
106
|
+
cwd,
|
|
107
|
+
scope: scopeForCwd(cwd),
|
|
108
|
+
orchestration: { depth: derived?.orchestration.depth ?? 0 },
|
|
109
|
+
profile: profileNameFor(profileId),
|
|
110
|
+
},
|
|
111
|
+
target: { cwd, profileId, nodeId: node?.node_id ?? null },
|
|
112
|
+
nodeMissing: nodeId !== null && node === null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** The event's match payload from the flags that carry it. An event invoked
|
|
116
|
+
* without its payload matches nothing — the planner's own contract. */
|
|
117
|
+
export function payloadFor(event, flags, target) {
|
|
118
|
+
if (event === 'read')
|
|
119
|
+
return flags.file === null ? {} : { absoluteFile: resolvePath(flags.file) };
|
|
120
|
+
if (event === 'memory-read') {
|
|
121
|
+
if (flags.readName === null)
|
|
122
|
+
return {};
|
|
123
|
+
return {
|
|
124
|
+
resolvedCanonicalName: flags.readName,
|
|
125
|
+
resolvedDocPath: resolvedDocRealpath(flags.readName, target),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (event === 'command' || event === 'pre-command') {
|
|
129
|
+
return flags.command === null ? {} : { command: flags.command };
|
|
130
|
+
}
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
export function subjectSummary(subject) {
|
|
134
|
+
return {
|
|
135
|
+
kind: subject.kind,
|
|
136
|
+
mode: subject.mode,
|
|
137
|
+
lifecycle: subject.lifecycle,
|
|
138
|
+
hasManager: subject.hasManager,
|
|
139
|
+
scope: subject.scope,
|
|
140
|
+
profile: subject.profile,
|
|
141
|
+
depth: subject.orchestration.depth,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function resolveCwd(raw) {
|
|
145
|
+
const abs = resolvePath(raw);
|
|
146
|
+
if (!existsSync(abs)) {
|
|
147
|
+
throw new InputError({
|
|
148
|
+
error: 'invalid_field',
|
|
149
|
+
message: `cwd does not exist: ${abs}`,
|
|
150
|
+
field: 'cwd',
|
|
151
|
+
next: 'Pass --cwd a directory that exists, or omit it to use the current directory.',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return realpathOrSelf(abs);
|
|
155
|
+
}
|
|
156
|
+
export function optionalString(value) {
|
|
157
|
+
return typeof value === 'string' && value.trim() !== '' ? value : null;
|
|
158
|
+
}
|
|
159
|
+
/** A 404 means the id names no node: `--node` degrades to a synthetic subject
|
|
160
|
+
* rather than failing, the recovery `sys context prompt-review` also makes. */
|
|
161
|
+
async function fetchNodeOrNull(nodeId) {
|
|
162
|
+
if (nodeId === null)
|
|
163
|
+
return null;
|
|
164
|
+
try {
|
|
165
|
+
return await cliClient().getNode(nodeId);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (err instanceof ApiError && err.status === 404)
|
|
169
|
+
return null;
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** The authoritative subject the daemon gates against, so an inspection never
|
|
174
|
+
* re-derives axes the runtime already resolves. */
|
|
175
|
+
async function fetchSubjectOrNull(nodeId) {
|
|
176
|
+
try {
|
|
177
|
+
return await cliClient().nodeSubject(nodeId);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (err instanceof ApiError && err.status === 404)
|
|
181
|
+
return null;
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function planSection(snapshot, event, payload, granularity, includeAll) {
|
|
186
|
+
if (granularity === 'assembled') {
|
|
187
|
+
return { event, assembled: assembleEvent(snapshot, event, payload) };
|
|
188
|
+
}
|
|
189
|
+
const plan = planDelivery(snapshot.subject, snapshot.target, event, payload);
|
|
190
|
+
if (granularity === 'summary')
|
|
191
|
+
return { event, counts: countsOf(plan) };
|
|
192
|
+
const docs = plan.docs.filter((r) => includeAll || delivers(r)).map((r) => docRow(r, includeAll));
|
|
193
|
+
return { event, docs };
|
|
194
|
+
}
|
|
195
|
+
/** A document delivers when it won its canonical name AND resolved to a rung
|
|
196
|
+
* above the no-delivery floor — boot keeps rung-`none` winners in the plan so
|
|
197
|
+
* the catalog can still count them. */
|
|
198
|
+
export function delivers(record) {
|
|
199
|
+
return record.winner && rungAtLeast(record.finalRung, 'name');
|
|
200
|
+
}
|
|
201
|
+
export function docRow(record, includeAll) {
|
|
202
|
+
const row = {
|
|
203
|
+
name: record.name,
|
|
204
|
+
kind: record.kind,
|
|
205
|
+
scope: record.scope,
|
|
206
|
+
store_path: record.storePath,
|
|
207
|
+
authored_rung: record.authoredRung,
|
|
208
|
+
project_memory_cap: record.projectMemoryCap,
|
|
209
|
+
capped_rung: record.cappedRung,
|
|
210
|
+
final_rung: record.finalRung,
|
|
211
|
+
winner: record.winner,
|
|
212
|
+
shadowed_by: record.shadowedBy === null ? null : `${record.shadowedBy.scope}:${record.shadowedBy.path}`,
|
|
213
|
+
doc_gate: gateText(record.docGate),
|
|
214
|
+
entry_gate: record.entryGate === null ? null : gateText(record.entryGate),
|
|
215
|
+
matched_entry: entryText(record.matchedEntry),
|
|
216
|
+
};
|
|
217
|
+
if (includeAll && !delivers(record))
|
|
218
|
+
row.excluded = exclusionReason(record);
|
|
219
|
+
return row;
|
|
220
|
+
}
|
|
221
|
+
export function gateText(gate) {
|
|
222
|
+
return gate.pass ? 'pass' : `fail: ${gate.reason}`;
|
|
223
|
+
}
|
|
224
|
+
export function entryText(entry) {
|
|
225
|
+
if (entry === null)
|
|
226
|
+
return null;
|
|
227
|
+
const parts = [`at=${entry.at}`];
|
|
228
|
+
if (entry.match !== undefined)
|
|
229
|
+
parts.push(`match=${entry.match.join(',')}`);
|
|
230
|
+
if (entry.matchFrontmatter !== undefined)
|
|
231
|
+
parts.push('match-frontmatter');
|
|
232
|
+
if (entry.gate !== undefined)
|
|
233
|
+
parts.push('gate');
|
|
234
|
+
return parts.join(' ');
|
|
235
|
+
}
|
|
236
|
+
/** Why a considered document delivers nothing, in the order the planner applies
|
|
237
|
+
* its checks — the first failing one is the answer an engineer acts on. */
|
|
238
|
+
export function exclusionReason(record) {
|
|
239
|
+
if (!record.docGate.pass)
|
|
240
|
+
return `document gate: ${record.docGate.reason}`;
|
|
241
|
+
if (record.shadowedBy !== null)
|
|
242
|
+
return `shadowed by ${record.shadowedBy.scope}:${record.shadowedBy.path}`;
|
|
243
|
+
if (record.authoredEntries.length === 0)
|
|
244
|
+
return 'no surface entry for this event';
|
|
245
|
+
if (record.matchedEntry === null) {
|
|
246
|
+
return record.entryGate !== null && !record.entryGate.pass
|
|
247
|
+
? `entry gate: ${record.entryGate.reason}`
|
|
248
|
+
: 'no authored entry matched this event\u2019s payload';
|
|
249
|
+
}
|
|
250
|
+
if (record.cappedRung === 'none') {
|
|
251
|
+
return `capped to none by the profile\u2019s project-memory setting (authored ${record.authoredRung})`;
|
|
252
|
+
}
|
|
253
|
+
return `resolved to rung none (authored ${record.authoredRung})`;
|
|
254
|
+
}
|
|
255
|
+
function countsOf(plan) {
|
|
256
|
+
const delivered = { content: 0, preview: 0, name: 0 };
|
|
257
|
+
const excluded = {};
|
|
258
|
+
for (const record of plan.docs) {
|
|
259
|
+
if (delivers(record)) {
|
|
260
|
+
delivered[record.finalRung] = (delivered[record.finalRung] ?? 0) + 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const bucket = exclusionBucket(record);
|
|
264
|
+
excluded[bucket] = (excluded[bucket] ?? 0) + 1;
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
considered: plan.docs.length,
|
|
268
|
+
delivered,
|
|
269
|
+
delivered_total: delivered['content'] + delivered['preview'] + delivered['name'],
|
|
270
|
+
excluded,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function exclusionBucket(record) {
|
|
274
|
+
if (!record.docGate.pass)
|
|
275
|
+
return 'document_gate';
|
|
276
|
+
if (record.shadowedBy !== null)
|
|
277
|
+
return 'shadowed';
|
|
278
|
+
if (record.authoredEntries.length === 0)
|
|
279
|
+
return 'no_entry';
|
|
280
|
+
if (record.matchedEntry === null) {
|
|
281
|
+
return record.entryGate !== null && !record.entryGate.pass ? 'entry_gate' : 'no_match';
|
|
282
|
+
}
|
|
283
|
+
return record.cappedRung === 'none' ? 'capped' : 'rung_none';
|
|
284
|
+
}
|
|
285
|
+
// Assembled granularity — the real renderers, so the highest-fidelity view is
|
|
286
|
+
// byte-identical to what the node receives.
|
|
287
|
+
/** Every renderer resolves its corpus from the AMBIENT process (cwd plus the
|
|
288
|
+
* two env seams), not from a target argument, so a snapshot dialed away from
|
|
289
|
+
* this process is only honored by standing in its place for the call. The
|
|
290
|
+
* session parse cache is cleared on both sides so neither the emulated corpus
|
|
291
|
+
* nor the real one is served from the other's scan. */
|
|
292
|
+
function assembleEvent(snapshot, event, payload) {
|
|
293
|
+
const restore = {
|
|
294
|
+
cwd: process.cwd(),
|
|
295
|
+
profileId: envProfileId(),
|
|
296
|
+
nodeId: envNodeId(),
|
|
297
|
+
};
|
|
298
|
+
clearSessionCache();
|
|
299
|
+
resetScopeCache();
|
|
300
|
+
try {
|
|
301
|
+
process.chdir(snapshot.target.cwd);
|
|
302
|
+
setEnv('CRTR_PROFILE_ID', snapshot.target.profileId ?? undefined);
|
|
303
|
+
setEnv('CRTR_NODE_ID', snapshot.target.nodeId ?? undefined);
|
|
304
|
+
return renderEvent(snapshot, event, payload);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
process.chdir(restore.cwd);
|
|
308
|
+
setEnv('CRTR_PROFILE_ID', restore.profileId);
|
|
309
|
+
setEnv('CRTR_NODE_ID', restore.nodeId);
|
|
310
|
+
clearSessionCache();
|
|
311
|
+
resetScopeCache();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function setEnv(name, value) {
|
|
315
|
+
if (value === undefined)
|
|
316
|
+
delete process.env[name];
|
|
317
|
+
else
|
|
318
|
+
process.env[name] = value;
|
|
319
|
+
}
|
|
320
|
+
function renderEvent(snapshot, event, payload) {
|
|
321
|
+
const { subject, target } = snapshot;
|
|
322
|
+
switch (event) {
|
|
323
|
+
case 'boot': {
|
|
324
|
+
const { block } = renderPreferencesForSubject(subject);
|
|
325
|
+
const knowledge = target.nodeId === null ? '' : renderKnowledgeForSubject(subject, target.nodeId);
|
|
326
|
+
return knowledge === '' ? block : `${block}\n\n${knowledge}`;
|
|
327
|
+
}
|
|
328
|
+
case 'workspace-open':
|
|
329
|
+
return renderWorkspaceOpenDocsForSubject(subject, target.cwd, target.profileId);
|
|
330
|
+
case 'read':
|
|
331
|
+
return payload.absoluteFile === undefined ? '' : renderOnReadDocsForSubject(subject, payload.absoluteFile);
|
|
332
|
+
case 'memory-read': {
|
|
333
|
+
const name = payload.resolvedCanonicalName;
|
|
334
|
+
if (name === undefined)
|
|
335
|
+
return '';
|
|
336
|
+
const blocks = memoryReadDocBlocks(subject, payload.resolvedDocPath ?? '', name, exposureTarget(emptyContextExposureState(), 'transcript'));
|
|
337
|
+
return blocks.length === 0 ? '' : `<auto-loaded-context>\n${blocks.join('\n')}\n</auto-loaded-context>`;
|
|
338
|
+
}
|
|
339
|
+
case 'command':
|
|
340
|
+
return payload.command === undefined ? '' : renderOnCommandDocsForSubject(subject, payload.command);
|
|
341
|
+
case 'pre-command':
|
|
342
|
+
return payload.command === undefined ? '' : renderPreCommandDocsForSubject(subject, payload.command);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** The read document never delivers to itself, so the plan needs its physical
|
|
346
|
+
* path. A name that resolves to nothing excludes nothing. */
|
|
347
|
+
function resolvedDocRealpath(name, target) {
|
|
348
|
+
try {
|
|
349
|
+
return realpathOrSelf(resolveMemoryDocForTarget(name, target).path);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return '';
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Rendering — agent-ready markdown.
|
|
356
|
+
function renderResolve(result) {
|
|
357
|
+
const lines = [];
|
|
358
|
+
if (result.node_missing === true) {
|
|
359
|
+
lines.push('The id passed to --node names no node; a synthetic subject was used instead.', '');
|
|
360
|
+
}
|
|
361
|
+
lines.push(`subject: ${describeSubject(result.subject)}`);
|
|
362
|
+
lines.push(`target: cwd=${result.target.cwd} profile=${result.target.profileId ?? '(none)'} node=${result.target.nodeId ?? '(none)'}`);
|
|
363
|
+
if (result.all_ignored === true) {
|
|
364
|
+
lines.push('Note: --all applies only at docs granularity and was ignored for assembled output.');
|
|
365
|
+
}
|
|
366
|
+
const sections = result.plans ?? [{
|
|
367
|
+
event: result.event,
|
|
368
|
+
docs: result.docs,
|
|
369
|
+
counts: result.counts,
|
|
370
|
+
assembled: result.assembled,
|
|
371
|
+
}];
|
|
372
|
+
for (const section of sections) {
|
|
373
|
+
lines.push('', `## ${section.event}`, '', renderSection(section));
|
|
374
|
+
}
|
|
375
|
+
return lines.join('\n');
|
|
376
|
+
}
|
|
377
|
+
export function describeSubject(subject) {
|
|
378
|
+
return ['kind', 'mode', 'lifecycle', 'hasManager', 'scope', 'profile', 'depth']
|
|
379
|
+
.map((k) => `${k}=${String(subject[k])}`)
|
|
380
|
+
.join(' ');
|
|
381
|
+
}
|
|
382
|
+
function renderSection(section) {
|
|
383
|
+
if (section.assembled !== undefined) {
|
|
384
|
+
return section.assembled === ''
|
|
385
|
+
? 'Nothing is delivered for this event.'
|
|
386
|
+
: ['```', section.assembled, '```'].join('\n');
|
|
387
|
+
}
|
|
388
|
+
if (section.counts !== undefined)
|
|
389
|
+
return renderCounts(section.counts);
|
|
390
|
+
const docs = section.docs ?? [];
|
|
391
|
+
if (docs.length === 0)
|
|
392
|
+
return 'No document delivers for this event.';
|
|
393
|
+
const showExcluded = docs.some((d) => d.excluded !== undefined);
|
|
394
|
+
const cols = ['document', 'scope', 'kind', 'authored', 'cap', 'final', 'matched', 'gate', ...(showExcluded ? ['excluded'] : [])];
|
|
395
|
+
const rows = docs.map((d) => [
|
|
396
|
+
d.name,
|
|
397
|
+
d.scope,
|
|
398
|
+
d.kind,
|
|
399
|
+
d.authored_rung,
|
|
400
|
+
d.project_memory_cap ?? '—',
|
|
401
|
+
d.final_rung,
|
|
402
|
+
d.matched_entry ?? '—',
|
|
403
|
+
d.doc_gate,
|
|
404
|
+
...(showExcluded ? [d.excluded ?? ''] : []),
|
|
405
|
+
]);
|
|
406
|
+
return [
|
|
407
|
+
`${docs.length} document${docs.length === 1 ? '' : 's'}:`,
|
|
408
|
+
'',
|
|
409
|
+
`| ${cols.join(' | ')} |`,
|
|
410
|
+
`| ${cols.map(() => '---').join(' | ')} |`,
|
|
411
|
+
...rows.map((r) => `| ${r.map(cell).join(' | ')} |`),
|
|
412
|
+
].join('\n');
|
|
413
|
+
}
|
|
414
|
+
function renderCounts(counts) {
|
|
415
|
+
const delivered = counts['delivered'];
|
|
416
|
+
const excluded = counts['excluded'];
|
|
417
|
+
const excludedText = Object.keys(excluded).length === 0
|
|
418
|
+
? 'none'
|
|
419
|
+
: Object.entries(excluded).map(([k, v]) => `${k}=${v}`).join(' ');
|
|
420
|
+
return [
|
|
421
|
+
`- considered: ${String(counts['considered'])}`,
|
|
422
|
+
`- delivered: ${String(counts['delivered_total'])} (content=${delivered['content']} preview=${delivered['preview']} name=${delivered['name']})`,
|
|
423
|
+
`- excluded: ${excludedText}`,
|
|
424
|
+
].join('\n');
|
|
425
|
+
}
|
|
426
|
+
export function cell(v) {
|
|
427
|
+
return String(v).replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
428
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sysSyspromptLeaf: import("../../../core/command.js").LeafDef;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { envNodeId } from '
|
|
1
|
+
import { envNodeId } from '../../../shared/env.js';
|
|
2
2
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { defineLeaf } from '
|
|
6
|
-
import { InputError } from '
|
|
7
|
-
import { jobDir } from '
|
|
8
|
-
import { openNodeWindow } from '
|
|
9
|
-
import { ApiError } from '
|
|
10
|
-
import { cliClient } from '
|
|
11
|
-
import { shellQuote } from '
|
|
5
|
+
import { defineLeaf } from '../../../core/command.js';
|
|
6
|
+
import { InputError } from '../../../core/io.js';
|
|
7
|
+
import { jobDir } from '../../../core/canvas/paths.js';
|
|
8
|
+
import { openNodeWindow } from '../../../core/runtime/placement-tmux.js';
|
|
9
|
+
import { ApiError } from '../../../api/index.js';
|
|
10
|
+
import { cliClient } from '../../api-client.js';
|
|
11
|
+
import { shellQuote } from '../../../shared/shell-quote.js';
|
|
12
12
|
export const sysSyspromptLeaf = defineLeaf({
|
|
13
13
|
name: 'sysprompt',
|
|
14
14
|
description: 'print a node\'s assembled system prompt',
|
|
15
15
|
whenToUse: 'you want to inspect a node\'s assembled system prompt headlessly, without paying conversation/context cost — the broker writes it to disk while live, and this command reads that artifact back as markdown.',
|
|
16
16
|
help: {
|
|
17
|
-
name: 'sys sysprompt',
|
|
17
|
+
name: 'sys context sysprompt',
|
|
18
18
|
summary: 'print a node\'s assembled system prompt',
|
|
19
19
|
params: [
|
|
20
20
|
{ kind: 'positional', name: 'node', required: false, constraint: 'Node id. Defaults to the calling node (CRTR_NODE_ID).' },
|
|
@@ -54,7 +54,7 @@ export const sysSyspromptLeaf = defineLeaf({
|
|
|
54
54
|
error: 'not_found',
|
|
55
55
|
message: `system prompt not captured yet for ${nodeId}`,
|
|
56
56
|
field: 'node',
|
|
57
|
-
next: 'Focus or revive the node once while its broker is live, then rerun `crtr sys sysprompt <node>`.',
|
|
57
|
+
next: 'Focus or revive the node once while its broker is live, then rerun `crtr sys context sysprompt <node>`.',
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
const prompt = readFileSync(path, 'utf8');
|
|
@@ -66,7 +66,7 @@ export const sysSyspromptLeaf = defineLeaf({
|
|
|
66
66
|
error: 'not_focused',
|
|
67
67
|
message: `node ${nodeId} is not currently shown in tmux`,
|
|
68
68
|
field: 'node',
|
|
69
|
-
next: `Focus the node first with \`crtr surface node focus ${nodeId}\`, then rerun \`crtr sys sysprompt --window ${nodeId}\`.`,
|
|
69
|
+
next: `Focus the node first with \`crtr surface node focus ${nodeId}\`, then rerun \`crtr sys context sysprompt --window ${nodeId}\`.`,
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
const tmpPath = join(tmpdir(), `crtr-sysprompt-${nodeId}.md`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// `crtr sys context` — inspect and shape the memory context surface: what a
|
|
2
|
+
// node's configuration actually loads, for which event, at which rung, and why.
|
|
3
|
+
// The leaves are landed elsewhere; this file only assembles them.
|
|
4
|
+
import { defineBranch } from '../../core/command.js';
|
|
5
|
+
import { resolveLeaf } from './context/resolve.js';
|
|
6
|
+
import { docLeaf } from './context/doc.js';
|
|
7
|
+
import { sysSyspromptLeaf } from './context/sysprompt.js';
|
|
8
|
+
import { promptReviewLeaf } from './context/prompt-review.js';
|
|
9
|
+
import { adminLeaf } from './context/admin.js';
|
|
10
|
+
export const contextBranch = defineBranch({
|
|
11
|
+
name: 'context',
|
|
12
|
+
description: 'inspect and shape what a node\u2019s memory context surface delivers',
|
|
13
|
+
whenToUse: 'you are asking what a node loads, why a document does or does not reach it, or you want to change the surfaces and caps that decide it',
|
|
14
|
+
help: {
|
|
15
|
+
name: 'sys context',
|
|
16
|
+
summary: 'inspect and shape the memory context a node\u2019s configuration delivers',
|
|
17
|
+
model: 'A document reaches a node when one of its authored surface entries fires for an event (boot, workspace-open, read, memory-read, command, pre-command), its gates pass, and it wins its canonical name against nearer stores; the profile relationship caps the rung at boot and workspace-open only. resolve answers that forward — snapshot plus event to the delivered set. doc answers it in reverse — one document across every event. sysprompt and prompt-review read assembled prompts (a live node\u2019s captured one, and a hypothetical configuration\u2019s). admin is the interactive surface over all of it.',
|
|
18
|
+
},
|
|
19
|
+
children: [resolveLeaf, docLeaf, sysSyspromptLeaf, promptReviewLeaf, adminLeaf],
|
|
20
|
+
});
|
package/dist/commands/sys.js
CHANGED
|
@@ -8,8 +8,7 @@ import { sysLogsLeaf } from './sys/logs.js';
|
|
|
8
8
|
import { sysSettingsLeaf } from './sys/settings.js';
|
|
9
9
|
import { sysSetupLeaf } from './sys/setup.js';
|
|
10
10
|
import { supportBranch } from './sys/support.js';
|
|
11
|
-
import {
|
|
12
|
-
import { promptReviewLeaf } from './sys/prompt-review.js';
|
|
11
|
+
import { contextBranch } from './sys/context.js';
|
|
13
12
|
import { sysSyncBranch } from './sys/sync.js';
|
|
14
13
|
import { sysMigrateLeaf } from './sys/migrate.js';
|
|
15
14
|
import { sysUpdateLeaf, sysVersionLeaf } from './sys/update.js';
|
|
@@ -18,7 +17,7 @@ export function registerSys() {
|
|
|
18
17
|
name: 'sys',
|
|
19
18
|
rootEntry: {
|
|
20
19
|
concept: 'crtr configuration, diagnostics, and self-management',
|
|
21
|
-
desc: 'settings, config, setup, doctor,
|
|
20
|
+
desc: 'settings, config, setup, doctor, context, sync, migrate, update, version, feedback, logs, support, daemon',
|
|
22
21
|
useWhen: 'managing the crtr installation, inspecting diagnostics, or operating the crtrd supervisor process',
|
|
23
22
|
},
|
|
24
23
|
help: {
|
|
@@ -26,6 +25,6 @@ export function registerSys() {
|
|
|
26
25
|
summary: 'crtr system configuration, settings, diagnostics, and self-management',
|
|
27
26
|
model: 'Configuration and settings own durable behavior; diagnostics and support observe it. Setup, migration, sync, and update change installed or generated state, while daemon control owns the supervisor lifecycle.',
|
|
28
27
|
},
|
|
29
|
-
children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf,
|
|
28
|
+
children: [sysSettingsLeaf, configBranch, sysSetupLeaf, sysDoctorLeaf, contextBranch, sysFeedbackLeaf, sysLogsLeaf, supportBranch, sysUpdateLeaf, sysVersionLeaf, sysMigrateLeaf, sysSyncBranch, daemonBranch],
|
|
30
29
|
});
|
|
31
30
|
}
|
|
@@ -38,7 +38,9 @@ import { closeNode } from '../runtime/close.js';
|
|
|
38
38
|
import { superviseTick } from '../../daemon/crtrd.js';
|
|
39
39
|
import { bindTestFleet } from './helpers/fleet.js';
|
|
40
40
|
import { resetFleetForTests } from '../runtime/fleet.js';
|
|
41
|
+
import { headlessBrokerHost } from '../runtime/host.js';
|
|
41
42
|
let home;
|
|
43
|
+
let tf;
|
|
42
44
|
function node(id, over = {}) {
|
|
43
45
|
return {
|
|
44
46
|
node_id: id,
|
|
@@ -96,9 +98,9 @@ const wakeFromChild = (parent, child) => readInboxSince(parent).find((e) => e.fr
|
|
|
96
98
|
before(() => {
|
|
97
99
|
home = mkdtempSync(join(tmpdir(), 'crtr-child-death-wake-'));
|
|
98
100
|
process.env['CRTR_HOME'] = home;
|
|
99
|
-
// superviseTick requires a bound fleet (D-2).
|
|
100
|
-
//
|
|
101
|
-
bindTestFleet();
|
|
101
|
+
// superviseTick requires a bound fleet (D-2). It starts empty: none of these
|
|
102
|
+
// fabricated rows has a live broker.
|
|
103
|
+
tf = bindTestFleet();
|
|
102
104
|
});
|
|
103
105
|
beforeEach(() => {
|
|
104
106
|
closeDb();
|
|
@@ -111,27 +113,41 @@ after(() => {
|
|
|
111
113
|
resetFleetForTests();
|
|
112
114
|
});
|
|
113
115
|
// BROKER CUT — exit-event authority REPLACES daemon pane-gone reaping. A booted
|
|
114
|
-
// child whose engine pid is dead (and whose viewer pane is gone) is
|
|
116
|
+
// child whose engine pid is dead (and whose viewer pane is gone) is RECOVERED,
|
|
115
117
|
// not reaped, and the daemon's tick fans NO wake to the dormant parent. This
|
|
116
118
|
// single case subsumes the four deleted scenarios (crash / quiet-finalize /
|
|
117
|
-
// awaiting a live grandchild / pending self-wake): with pane state ignored
|
|
118
|
-
// surfaceChildDeath gone,
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
test('exit-event authority: a dead-engine booted child is
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
119
|
+
// awaiting a live grandchild / pending self-wake): with pane state ignored and
|
|
120
|
+
// surfaceChildDeath gone, the daemon treats them all identically — no reap, no
|
|
121
|
+
// liveness wake — so the old CRUX (no spurious wake on healthy dormancy) holds
|
|
122
|
+
// by construction. The node-lifecycle tick relaunches the row through the same
|
|
123
|
+
// policy table its exit event would have used (D-12); that recovery is silent.
|
|
124
|
+
test('exit-event authority: a dead-engine booted child is recovered, NOT reaped, and the tick fans NO wake to its dormant parent', async () => {
|
|
125
|
+
const originalLaunch = headlessBrokerHost.launch;
|
|
126
|
+
const launches = [];
|
|
127
|
+
headlessBrokerHost.launch = (nodeId) => {
|
|
128
|
+
launches.push(nodeId);
|
|
129
|
+
const handle = { pid: 424_242, exited: new Promise(() => { }) };
|
|
130
|
+
tf.fleet.register(nodeId, handle); // the real host contract: registration at the launch seam
|
|
131
|
+
return handle;
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
inboxWaitingParent('PARENT');
|
|
135
|
+
deadEngineChild('CHILD', { cwd: home }); // a cwd that exists — reviveNode's preflight refuses a missing one
|
|
136
|
+
subscribe('PARENT', 'CHILD', true); // the spawn-time spine edge
|
|
137
|
+
await superviseTick();
|
|
138
|
+
// A stale recorded pid is metadata, not a verdict: the row is relaunched on
|
|
139
|
+
// its saved session, never reaped to dead/done/idle.
|
|
140
|
+
assert.deepEqual(launches, ['CHILD'], 'the tick recovers the dead engine through the policy table');
|
|
141
|
+
assert.equal(getNode('CHILD').status, 'active', 'CHILD is recovered, NOT reaped');
|
|
142
|
+
// surfaceChildDeath is deleted: the daemon raises no liveness wake. The
|
|
143
|
+
// doctrine wake now rides the child's own `push final` (or close.ts), so a
|
|
144
|
+
// purely-inbox-waiting parent is never spuriously woken on dormancy/crash.
|
|
145
|
+
assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY — no daemon liveness wake');
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
headlessBrokerHost.launch = originalLaunch;
|
|
149
|
+
tf.fleet.forget('CHILD');
|
|
150
|
+
}
|
|
135
151
|
});
|
|
136
152
|
test('node close of a child wakes its SURVIVING manager (the parent outside the closing set)', async () => {
|
|
137
153
|
inboxWaitingParent('PARENT');
|