@crouton-kit/crouter 0.3.36 → 0.3.38
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/build-root.js +3 -6
- package/dist/{builtin-personas/runtime-base.md → builtin-memory/00-runtime-base.md} +8 -1
- package/dist/{builtin-personas/spine/has-manager.md → builtin-memory/01-spine/00-has-manager.md} +8 -0
- package/dist/builtin-memory/01-spine/01-no-manager.md +10 -0
- package/dist/{builtin-personas/lifecycle/terminal.md → builtin-memory/02-lifecycle/00-terminal.md} +8 -0
- package/dist/{builtin-personas/lifecycle/resident.md → builtin-memory/02-lifecycle/01-resident.md} +8 -0
- package/dist/{builtin-personas/waiting.md → builtin-memory/03-waiting.md} +7 -0
- package/dist/{builtin-personas/orchestration-kernel.md → builtin-memory/04-orchestration-kernel.md} +8 -0
- package/dist/{builtin-personas/advisor/PERSONA.md → builtin-memory/05-kinds/advisor/00-base.md} +5 -2
- package/dist/{builtin-personas/design/PERSONA.md → builtin-memory/05-kinds/design/00-base.md} +5 -2
- package/dist/{builtin-personas/design/orchestrator.md → builtin-memory/05-kinds/design/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/developer/PERSONA.md → builtin-memory/05-kinds/developer/00-base.md} +5 -2
- package/dist/{builtin-personas/developer/orchestrator.md → builtin-memory/05-kinds/developer/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/explore/PERSONA.md → builtin-memory/05-kinds/explore/00-base.md} +5 -2
- package/dist/{builtin-personas/explore/orchestrator.md → builtin-memory/05-kinds/explore/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/general/PERSONA.md → builtin-memory/05-kinds/general/00-base.md} +5 -2
- package/dist/{builtin-personas/general/orchestrator.md → builtin-memory/05-kinds/general/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/plan/PERSONA.md → builtin-memory/05-kinds/plan/00-base.md} +5 -2
- package/dist/{builtin-personas/plan/orchestrator.md → builtin-memory/05-kinds/plan/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/plan/reviewers/architecture-fit/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/architecture-fit.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/code-smells/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/code-smells.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/security/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/security.md} +5 -2
- package/dist/{builtin-personas/product/PERSONA.md → builtin-memory/05-kinds/product/00-base.md} +5 -2
- package/dist/{builtin-personas/product/orchestrator.md → builtin-memory/05-kinds/product/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/product/teardown/PERSONA.md → builtin-memory/05-kinds/product/teardown.md} +5 -3
- package/dist/{builtin-personas/review/PERSONA.md → builtin-memory/05-kinds/review/00-base.md} +5 -2
- package/dist/{builtin-personas/review/orchestrator.md → builtin-memory/05-kinds/review/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/spec/PERSONA.md → builtin-memory/05-kinds/spec/00-base.md} +5 -2
- package/dist/{builtin-personas/spec/orchestrator.md → builtin-memory/05-kinds/spec/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/spec/requirements/PERSONA.md → builtin-memory/05-kinds/spec/requirements.md} +5 -2
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +4 -4
- package/dist/builtin-views/canvas/core.mjs +3 -2
- package/dist/builtin-views/prompt-review/core.mjs +6 -134
- package/dist/builtin-views/settings/core.mjs +26 -94
- package/dist/builtin-views/settings/text.mjs +0 -5
- package/dist/builtin-views/settings/tui.mjs +1 -7
- package/dist/builtin-views/settings/web.jsx +3 -49
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +21 -205
- package/dist/clients/attach/attach-cmd.js +429 -432
- package/dist/clients/web/web-cmd.js +7 -7
- package/dist/commands/__tests__/human.test.js +1 -15
- package/dist/commands/attention.js +9 -6
- package/dist/commands/canvas-history/search.js +2 -2
- package/dist/commands/canvas-history/show.js +3 -3
- package/dist/commands/canvas-history.js +1 -1
- package/dist/commands/canvas-issue.js +1 -0
- package/dist/commands/canvas-snapshot.js +1 -0
- package/dist/commands/canvas.js +10 -11
- package/dist/commands/chord.js +1 -0
- package/dist/commands/human/prompts.js +0 -1
- package/dist/commands/human/queue.js +1 -0
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +13 -8
- package/dist/commands/memory/shared.js +22 -8
- package/dist/commands/memory/write.js +5 -3
- package/dist/commands/node-snapshot.js +1 -0
- package/dist/commands/node.js +52 -22
- package/dist/commands/profile/add-project.d.ts +1 -0
- package/dist/commands/profile/add-project.js +42 -0
- package/dist/commands/profile/delete.d.ts +1 -0
- package/dist/commands/profile/delete.js +39 -0
- package/dist/commands/profile/list.d.ts +1 -0
- package/dist/commands/profile/list.js +35 -0
- package/dist/commands/profile/new.d.ts +1 -0
- package/dist/commands/profile/new.js +50 -0
- package/dist/commands/profile/remove-project.d.ts +1 -0
- package/dist/commands/profile/remove-project.js +42 -0
- package/dist/commands/profile/rename.d.ts +1 -0
- package/dist/commands/profile/rename.js +42 -0
- package/dist/commands/profile/show.d.ts +1 -0
- package/dist/commands/profile/show.js +50 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.js +33 -0
- package/dist/commands/push.d.ts +0 -1
- package/dist/commands/push.js +2 -228
- package/dist/commands/revive.js +2 -2
- package/dist/commands/{canvas-tmux-spread.js → surface-tmux-spread.js} +9 -6
- package/dist/commands/surface.js +3 -2
- package/dist/commands/sys/config.js +6 -21
- package/dist/commands/sys/daemon.d.ts +2 -0
- package/dist/commands/{daemon.js → sys/daemon.js} +12 -12
- package/dist/commands/sys/doctor.js +1 -0
- package/dist/commands/sys/prompt-review.js +90 -17
- package/dist/commands/sys.js +4 -3
- package/dist/core/__tests__/daemon-boot.test.js +1 -1
- package/dist/core/__tests__/daemon-wedge.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-wedge.test.js +87 -0
- package/dist/core/__tests__/hearth-bootstrap.test.js +2 -2
- package/dist/core/__tests__/history-inbox.test.d.ts +1 -0
- package/dist/core/__tests__/history-inbox.test.js +105 -0
- package/dist/core/__tests__/kickoff.test.js +3 -3
- package/dist/core/__tests__/revive.test.js +39 -1
- package/dist/core/canvas/canvas.js +10 -9
- package/dist/core/canvas/db.js +11 -0
- package/dist/core/canvas/history.d.ts +1 -1
- package/dist/core/canvas/history.js +91 -3
- package/dist/core/canvas/paths.d.ts +6 -0
- package/dist/core/canvas/paths.js +9 -0
- package/dist/core/canvas/status-glyph.js +1 -0
- package/dist/core/canvas/types.d.ts +10 -0
- package/dist/core/config.d.ts +44 -1
- package/dist/core/config.js +155 -12
- package/dist/core/fault-classifier.d.ts +2 -2
- package/dist/core/feed/inbox.d.ts +7 -12
- package/dist/core/feed/inbox.js +45 -29
- package/dist/core/hearth/providers/blaxel-bootstrap.js +1 -1
- package/dist/core/memory-resolver.d.ts +26 -15
- package/dist/core/memory-resolver.js +158 -40
- package/dist/core/preview-registry.js +14 -112
- package/dist/core/profiles/manifest.d.ts +33 -0
- package/dist/core/profiles/manifest.js +332 -0
- package/dist/core/profiles/select.d.ts +11 -0
- package/dist/core/profiles/select.js +115 -0
- package/dist/core/render.js +1 -1
- package/dist/core/runtime/bearings.d.ts +9 -0
- package/dist/core/runtime/bearings.js +31 -0
- package/dist/core/runtime/busy.d.ts +15 -0
- package/dist/core/runtime/busy.js +32 -1
- package/dist/core/runtime/close.d.ts +10 -0
- package/dist/core/runtime/close.js +18 -13
- package/dist/core/runtime/fault-recovery.js +4 -0
- package/dist/core/runtime/front-door.js +9 -2
- package/dist/core/runtime/kickoff.js +5 -7
- package/dist/core/runtime/launch.d.ts +25 -7
- package/dist/core/runtime/launch.js +42 -55
- package/dist/core/runtime/nodes.d.ts +7 -0
- package/dist/core/runtime/nodes.js +6 -0
- package/dist/core/runtime/persona.js +18 -32
- package/dist/core/runtime/placement.js +1 -1
- package/dist/core/runtime/revive.js +9 -1
- package/dist/core/runtime/spawn.d.ts +13 -0
- package/dist/core/runtime/spawn.js +25 -0
- package/dist/core/runtime/tmux.d.ts +2 -2
- package/dist/core/runtime/tmux.js +2 -2
- package/dist/core/scope.d.ts +14 -7
- package/dist/core/scope.js +76 -20
- package/dist/core/substrate/index.d.ts +3 -3
- package/dist/core/substrate/index.js +5 -3
- package/dist/core/substrate/on-read.js +3 -2
- package/dist/core/substrate/render.d.ts +16 -11
- package/dist/core/substrate/render.js +249 -62
- package/dist/core/substrate/schema.d.ts +11 -3
- package/dist/core/substrate/schema.js +25 -0
- package/dist/core/substrate/subject.d.ts +15 -2
- package/dist/core/substrate/subject.js +24 -2
- package/dist/daemon/crtrd-cli.js +1 -1
- package/dist/daemon/crtrd.d.ts +24 -0
- package/dist/daemon/crtrd.js +213 -5
- package/dist/daemon/manage.js +2 -2
- package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
- package/dist/pi-extensions/canvas-nav.js +2 -2
- package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
- package/dist/pi-extensions/canvas-stophook.js +59 -2
- package/dist/types.d.ts +57 -1
- package/dist/types.js +83 -1
- package/dist/web-client/assets/{index-IAJVtuVe.js → index-CbO8L0mN.js} +4 -4
- package/dist/web-client/assets/index-DwO46Cs5.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/package.json +2 -2
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +0 -56
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +0 -57
- package/dist/builtin-memory/crouter-development/personas.md +0 -112
- package/dist/builtin-personas/spine/no-manager.md +0 -2
- package/dist/commands/daemon.d.ts +0 -2
- package/dist/core/personas/index.d.ts +0 -13
- package/dist/core/personas/index.js +0 -11
- package/dist/core/personas/loader.d.ts +0 -159
- package/dist/core/personas/loader.js +0 -327
- package/dist/core/personas/resolve.d.ts +0 -59
- package/dist/core/personas/resolve.js +0 -376
- package/dist/web-client/assets/index-DUThOUzU.css +0 -2
- /package/dist/commands/{canvas-tmux-spread.d.ts → surface-tmux-spread.d.ts} +0 -0
package/dist/commands/sys.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Replaces old config.ts + doctor.ts + update.ts command files.
|
|
3
3
|
import { defineBranch } from '../core/command.js';
|
|
4
4
|
import { configBranch } from './sys/config.js';
|
|
5
|
+
import { daemonBranch } from './sys/daemon.js';
|
|
5
6
|
import { sysDoctorLeaf } from './sys/doctor.js';
|
|
6
7
|
import { sysFeedbackLeaf } from './sys/feedback.js';
|
|
7
8
|
import { sysSettingsLeaf } from './sys/settings.js';
|
|
@@ -16,13 +17,13 @@ export function registerSys() {
|
|
|
16
17
|
name: 'sys',
|
|
17
18
|
rootEntry: {
|
|
18
19
|
concept: 'crtr configuration, diagnostics, and self-management',
|
|
19
|
-
desc: 'config, settings, setup, doctor, sysprompt, prompt-review, promptstudio, sync, update, version, feedback',
|
|
20
|
-
useWhen: 'managing the crtr installation or opening built-in system views',
|
|
20
|
+
desc: 'config, settings, setup, doctor, sysprompt, prompt-review, promptstudio, sync, update, version, feedback, daemon',
|
|
21
|
+
useWhen: 'managing the crtr installation, the crtrd supervisor process, or opening built-in system views',
|
|
21
22
|
},
|
|
22
23
|
help: {
|
|
23
24
|
name: 'sys',
|
|
24
25
|
summary: 'crtr system configuration, settings, diagnostics, and self-management',
|
|
25
26
|
},
|
|
26
|
-
children: [configBranch, sysSettingsLeaf, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysPromptStudioLeaf, sysFeedbackLeaf, sysUpdateLeaf, sysVersionLeaf, sysSyncLeaf],
|
|
27
|
+
children: [configBranch, sysSettingsLeaf, sysSetupLeaf, sysDoctorLeaf, sysSyspromptLeaf, promptReviewLeaf, sysPromptStudioLeaf, sysFeedbackLeaf, sysUpdateLeaf, sysVersionLeaf, sysSyncLeaf, daemonBranch],
|
|
27
28
|
});
|
|
28
29
|
}
|
|
@@ -262,7 +262,7 @@ test('the daemon honours an explicit CRTR_PIDFILE override', () => {
|
|
|
262
262
|
// REGRESSION (bug #102 follow-up): with NO exit promise, the readiness loop must
|
|
263
263
|
// still observe a real `connect`. A buggy `?? Promise.resolve('retry')` race lost
|
|
264
264
|
// to the probe instantly, so live-broker callers (focus on an already-alive node,
|
|
265
|
-
//
|
|
265
|
+
// surface tmux-spread) could never return true and stalled the full 30s deadline.
|
|
266
266
|
test('waitForBrokerViewSocket returns true on a live socket when given no exit promise', async () => {
|
|
267
267
|
// CRTR_SOCK_DIR override keeps the AF_UNIX path short — the temp CRTR_HOME path
|
|
268
268
|
// would blow past macOS's 104-byte sun_path limit (listen EINVAL).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/daemon-wedge.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Issue #110 — pid-alive WEDGED node detection. The pure decision core:
|
|
4
|
+
// wedgeVerdict(busy, quietForMs, cpuPercent). No wall-clock sleeps, no ps
|
|
5
|
+
// shell-outs, no fake pids — every input is injected directly, mirroring the
|
|
6
|
+
// existing livenessVerdict/yieldStallVerdict suites in daemon-liveness.test.ts.
|
|
7
|
+
//
|
|
8
|
+
// LOCKS: (1) a routine long-running BUT ACTIVE turn (heartbeat recent, or CPU
|
|
9
|
+
// non-trivial) must never verdict 'wedged' — false positives are the failure
|
|
10
|
+
// mode this feature exists to avoid. (2) only busy+quiet+near-zero-CPU, all at
|
|
11
|
+
// once, past the generous grace, fires. (3) a null/failed CPU sample never
|
|
12
|
+
// fires on its own (fails toward 'pending', not 'wedged').
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { wedgeVerdict, WEDGE_QUIET_MS, WEDGE_CPU_MAX_PERCENT, sumTreeCpu } from '../../daemon/crtrd.js';
|
|
16
|
+
test('wedgeVerdict: a node whose turn already ended is never wedged, regardless of the other signals', () => {
|
|
17
|
+
assert.equal(wedgeVerdict({ busy: false, quietForMs: null, cpuPercent: null }), 'leave');
|
|
18
|
+
assert.equal(wedgeVerdict({ busy: false, quietForMs: 10 * 60 * 60_000, cpuPercent: 0 }), 'leave');
|
|
19
|
+
});
|
|
20
|
+
test('wedgeVerdict: a busy node with an unreadable heartbeat is left alone (not enough evidence)', () => {
|
|
21
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: null, cpuPercent: null }), 'leave');
|
|
22
|
+
});
|
|
23
|
+
test('wedgeVerdict: routine turns — even long ones — never trip before the grace elapses', () => {
|
|
24
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: 0, cpuPercent: 0 }), 'leave', 'just started');
|
|
25
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: 5 * 60_000, cpuPercent: 0 }), 'leave', 'a routine multi-minute tool call');
|
|
26
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS - 1, cpuPercent: 0 }), 'leave', 'just under the grace, even with 0% cpu');
|
|
27
|
+
});
|
|
28
|
+
test('wedgeVerdict: past the grace with NO cpu corroboration only pends — never fires on time alone', () => {
|
|
29
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS, cpuPercent: null }), 'pending', 'cpu sample failed/unavailable — never treated as confirmation');
|
|
30
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS * 10, cpuPercent: null }), 'pending', 'no amount of extra quiet time substitutes for the corroborating signal');
|
|
31
|
+
});
|
|
32
|
+
test('wedgeVerdict: a long-quiet turn that is STILL VISIBLY WORKING (real cpu) is exonerated, not wedged', () => {
|
|
33
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS * 3, cpuPercent: WEDGE_CPU_MAX_PERCENT + 0.1 }), 'pending', 'cpu just above the ceiling — a genuinely churning long build — must not fire');
|
|
34
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS * 3, cpuPercent: 40 }), 'pending', 'clearly-active cpu never fires regardless of how quiet the heartbeat looks');
|
|
35
|
+
});
|
|
36
|
+
test('wedgeVerdict: fires ONLY when busy + quiet-past-grace + near-zero cpu all corroborate', () => {
|
|
37
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS, cpuPercent: 0 }), 'wedged');
|
|
38
|
+
assert.equal(wedgeVerdict({ busy: true, quietForMs: WEDGE_QUIET_MS + 60_000, cpuPercent: WEDGE_CPU_MAX_PERCENT }), 'wedged', 'at the cpu ceiling (inclusive)');
|
|
39
|
+
});
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// sumTreeCpu (pure) — process-tree CPU sampling. Review fix for issue #110:
|
|
42
|
+
// sampling only the broker pid false-fired on a broker awaiting a busy CHILD
|
|
43
|
+
// subprocess. These lock the tree-sum behavior directly, independent of any
|
|
44
|
+
// `ps` shell-out.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
test('sumTreeCpu: a busy CHILD subprocess exonerates an idle broker (the false-positive this fixes)', () => {
|
|
47
|
+
const ps = [
|
|
48
|
+
' 100 1 0.0', // broker, idle
|
|
49
|
+
' 200 100 85.0', // child of the broker, busy
|
|
50
|
+
].join('\n');
|
|
51
|
+
assert.equal(sumTreeCpu(ps, 100), 85.0);
|
|
52
|
+
});
|
|
53
|
+
test('sumTreeCpu: broker + all descendants idle sums to ~0 (the real wedge is still detected)', () => {
|
|
54
|
+
const ps = [
|
|
55
|
+
' 100 1 0.0',
|
|
56
|
+
' 200 100 0.0',
|
|
57
|
+
' 201 100 0.0',
|
|
58
|
+
].join('\n');
|
|
59
|
+
assert.equal(sumTreeCpu(ps, 100), 0);
|
|
60
|
+
});
|
|
61
|
+
test('sumTreeCpu: sums across multiple generations (grandchild busy)', () => {
|
|
62
|
+
const ps = [
|
|
63
|
+
' 100 1 0.0', // broker
|
|
64
|
+
' 200 100 0.0', // child, idle
|
|
65
|
+
' 300 200 40.0', // grandchild, busy
|
|
66
|
+
].join('\n');
|
|
67
|
+
assert.equal(sumTreeCpu(ps, 100), 40.0);
|
|
68
|
+
});
|
|
69
|
+
test('sumTreeCpu: root pid absent from the ps output returns null (can\'t measure)', () => {
|
|
70
|
+
const ps = [
|
|
71
|
+
' 200 1 0.0',
|
|
72
|
+
' 300 200 10.0',
|
|
73
|
+
].join('\n');
|
|
74
|
+
assert.equal(sumTreeCpu(ps, 100), null);
|
|
75
|
+
});
|
|
76
|
+
test('sumTreeCpu: malformed/blank lines are skipped; empty output returns null', () => {
|
|
77
|
+
const ps = [
|
|
78
|
+
'',
|
|
79
|
+
' ',
|
|
80
|
+
'garbage line not ps shaped',
|
|
81
|
+
' 100 1 5.0',
|
|
82
|
+
' bad 100 1.0',
|
|
83
|
+
].join('\n');
|
|
84
|
+
assert.equal(sumTreeCpu(ps, 100), 5.0);
|
|
85
|
+
assert.equal(sumTreeCpu('', 100), null);
|
|
86
|
+
assert.equal(sumTreeCpu(' \n \n', 100), null);
|
|
87
|
+
});
|
|
@@ -91,7 +91,7 @@ test('every crtr invocation targets the baked binary by absolute path, never a b
|
|
|
91
91
|
assert.match(script, /'\/usr\/local\/bin\/crtr' --json sys version/);
|
|
92
92
|
assert.match(script, /'\/usr\/local\/bin\/crtr' surface web -h/);
|
|
93
93
|
assert.match(script, /'\/usr\/local\/bin\/crtr' surface web serve -h/);
|
|
94
|
-
assert.match(script, /'\/usr\/local\/bin\/crtr'
|
|
94
|
+
assert.match(script, /'\/usr\/local\/bin\/crtr' sys daemon start/);
|
|
95
95
|
assert.match(script, /'\/usr\/local\/bin\/crtr' surface web serve --host 0\.0\.0\.0/);
|
|
96
96
|
// No bare `crtr <subcommand>` invocation anywhere — only the `command -v
|
|
97
97
|
// crtr` shadow-detection probe and human-readable error-message text are
|
|
@@ -123,7 +123,7 @@ test('mkdirs the tmpfs CRTR_SOCK_DIR/CRTR_PIDFILE dirs strictly before the daemo
|
|
|
123
123
|
assert.doesNotMatch(script, /CRTR_CANVAS_DB=/);
|
|
124
124
|
assert.match(script, /unset CRTR_CANVAS_DB/);
|
|
125
125
|
const mkdirIdx = indexOf(script, '/run/crtr/socks');
|
|
126
|
-
const daemonStartIdx = indexOf(script, "'/usr/local/bin/crtr'
|
|
126
|
+
const daemonStartIdx = indexOf(script, "'/usr/local/bin/crtr' sys daemon start");
|
|
127
127
|
assert.ok(mkdirIdx < daemonStartIdx, 'tmpfs mkdir must precede daemon start');
|
|
128
128
|
const mkdirLine = script.split('\n').find((line) => line.startsWith('mkdir -p') && line.includes('/run/crtr/socks'));
|
|
129
129
|
assert.ok(mkdirLine, 'expected a single mkdir line covering the tmpfs dirs');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/history-inbox.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Locks in the invariants of the `inbox` history source: it stays opt-in
|
|
4
|
+
// (never part of the default corpus), its refs resolve/round-trip as
|
|
5
|
+
// `<node>:inbox/NNNNNN`, direct-message bodies stay full/unclipped, and the
|
|
6
|
+
// sender (`from`) is visible in both the read body and the artifact title.
|
|
7
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { createNode } from '../canvas/canvas.js';
|
|
13
|
+
import { closeDb } from '../canvas/db.js';
|
|
14
|
+
import { appendInbox } from '../feed/inbox.js';
|
|
15
|
+
import { buildCorpus, resolveRef } from '../canvas/history.js';
|
|
16
|
+
let home;
|
|
17
|
+
function node(id, cwd) {
|
|
18
|
+
return {
|
|
19
|
+
node_id: id,
|
|
20
|
+
name: id,
|
|
21
|
+
created: new Date().toISOString(),
|
|
22
|
+
cwd,
|
|
23
|
+
kind: 'general',
|
|
24
|
+
mode: 'base',
|
|
25
|
+
lifecycle: 'terminal',
|
|
26
|
+
status: 'active',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
before(() => {
|
|
30
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-history-inbox-'));
|
|
31
|
+
process.env['CRTR_HOME'] = home;
|
|
32
|
+
});
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
closeDb();
|
|
35
|
+
rmSync(home, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
after(() => {
|
|
38
|
+
closeDb();
|
|
39
|
+
rmSync(home, { recursive: true, force: true });
|
|
40
|
+
delete process.env['CRTR_HOME'];
|
|
41
|
+
});
|
|
42
|
+
test('default history excludes inbox; --type inbox includes it, with full sender-attributed bodies', () => {
|
|
43
|
+
const cwd = '/tmp/hist-work';
|
|
44
|
+
createNode(node('sender-1', cwd));
|
|
45
|
+
const receiver = createNode(node('receiver-1', cwd));
|
|
46
|
+
const longBody = 'a'.repeat(4000); // well past any old label-clip length
|
|
47
|
+
appendInbox(receiver.node_id, {
|
|
48
|
+
from: 'sender-1',
|
|
49
|
+
tier: 'urgent',
|
|
50
|
+
kind: 'message',
|
|
51
|
+
label: 'first line of a long message',
|
|
52
|
+
data: { body: longBody },
|
|
53
|
+
});
|
|
54
|
+
// (a) default corpus (no explicit types) excludes inbox entirely.
|
|
55
|
+
const defaultCorpus = buildCorpus({ nodes: [receiver.node_id] }, {});
|
|
56
|
+
assert.equal(defaultCorpus.some((a) => a.source === 'inbox'), false, 'inbox is opt-in, not part of the default corpus');
|
|
57
|
+
// (b) explicit --type inbox includes it.
|
|
58
|
+
const inboxCorpus = buildCorpus({ nodes: [receiver.node_id] }, { types: ['inbox'] });
|
|
59
|
+
assert.equal(inboxCorpus.length, 1, 'exactly the one appended entry');
|
|
60
|
+
const artifact = inboxCorpus[0];
|
|
61
|
+
assert.equal(artifact.source, 'inbox');
|
|
62
|
+
// (c) ref resolves as <node>:inbox/000000 and round-trips through resolveRef.
|
|
63
|
+
const expectedRef = `${receiver.node_id}:inbox/000000`;
|
|
64
|
+
assert.equal(artifact.ref, expectedRef);
|
|
65
|
+
const resolved = resolveRef(expectedRef);
|
|
66
|
+
assert.ok(resolved !== null, 'ref resolves back to the entry');
|
|
67
|
+
assert.equal(resolved.source, 'inbox');
|
|
68
|
+
// (d) the direct-message body is full/unclipped in both the corpus artifact
|
|
69
|
+
// and the resolveRef read path.
|
|
70
|
+
assert.ok(artifact.loadBody().includes(longBody), 'corpus artifact body is unclipped');
|
|
71
|
+
assert.ok(resolved.body.includes(longBody), 'resolveRef body is unclipped');
|
|
72
|
+
// (e) the sender is visible — in the read body (not just resolvable via the
|
|
73
|
+
// node the inbox belongs to) and in the artifact title used by list rows.
|
|
74
|
+
assert.ok(resolved.body.includes('from: sender-1'), 'read body names the sender');
|
|
75
|
+
assert.ok(artifact.title.includes('sender-1'), 'artifact title names the sender for list/search rows');
|
|
76
|
+
});
|
|
77
|
+
test('a spilled message body is read RAW — a leading frontmatter-shaped block is not stripped', () => {
|
|
78
|
+
const cwd = '/tmp/hist-work-3';
|
|
79
|
+
createNode(node('sender-3', cwd));
|
|
80
|
+
const receiver = createNode(node('receiver-3', cwd));
|
|
81
|
+
// Long enough to force the spill-to-messages/ path (past BODY_MAX_CHARS),
|
|
82
|
+
// and starts with a block that LOOKS like crouter report frontmatter but
|
|
83
|
+
// isn't one — a spilled message has no frontmatter contract, so this must
|
|
84
|
+
// survive intact, unlike a report ref (which always has real frontmatter
|
|
85
|
+
// stripped by fileBodyLoader).
|
|
86
|
+
const body = '---\nfoo: bar\n---\n' + 'a'.repeat(4000);
|
|
87
|
+
appendInbox(receiver.node_id, {
|
|
88
|
+
from: 'sender-3',
|
|
89
|
+
tier: 'normal',
|
|
90
|
+
kind: 'message',
|
|
91
|
+
label: 'first line',
|
|
92
|
+
data: { body },
|
|
93
|
+
});
|
|
94
|
+
const resolved = resolveRef(`${receiver.node_id}:inbox/000000`);
|
|
95
|
+
assert.ok(resolved !== null);
|
|
96
|
+
assert.ok(resolved.body.includes('---\nfoo: bar\n---'), 'leading frontmatter-shaped block is preserved, not stripped');
|
|
97
|
+
});
|
|
98
|
+
test('a null-from (system) inbox entry reads as "system", not blank or thrown', () => {
|
|
99
|
+
const cwd = '/tmp/hist-work-2';
|
|
100
|
+
const receiver = createNode(node('receiver-2', cwd));
|
|
101
|
+
appendInbox(receiver.node_id, { from: null, tier: 'normal', kind: 'update', label: 'system note' });
|
|
102
|
+
const resolved = resolveRef(`${receiver.node_id}:inbox/000000`);
|
|
103
|
+
assert.ok(resolved !== null);
|
|
104
|
+
assert.ok(resolved.body.includes('from: system'), 'null sender renders as "system"');
|
|
105
|
+
});
|
|
@@ -58,7 +58,7 @@ test('drainBearings consumes the yield note + advances the cursor exactly once',
|
|
|
58
58
|
assert.equal(b2.yieldMsg, null);
|
|
59
59
|
assert.equal(b2.unreadDigest, null);
|
|
60
60
|
});
|
|
61
|
-
test('the feed block frames awaiting workers as alive + auto-waking, so a fresh revive has no reason to
|
|
61
|
+
test('the feed block frames awaiting workers as alive + auto-waking, so a fresh revive has no reason to poll', () => {
|
|
62
62
|
const parent = createNode(node('p1'));
|
|
63
63
|
const child = createNode(node('c1')); // status 'active' by default
|
|
64
64
|
subscribe(parent.node_id, child.node_id); // parent awaits child
|
|
@@ -66,7 +66,7 @@ test('the feed block frames awaiting workers as alive + auto-waking, so a fresh
|
|
|
66
66
|
// The roster names the live child...
|
|
67
67
|
assert.ok(msg.includes(child.node_id), 'awaiting roster lists the live child');
|
|
68
68
|
// ...and asserts aliveness + the automatic wake at the source, so the node
|
|
69
|
-
//
|
|
69
|
+
// has nothing to check, poll, or verify separately.
|
|
70
70
|
assert.ok(/alive and running/.test(msg), 'states the worker is alive and running');
|
|
71
71
|
assert.ok(/wake you the moment/.test(msg), 'states the wake is automatic on push');
|
|
72
72
|
assert.ok(/still working, not stalled/.test(msg), 'frames the empty feed as expected, not a problem');
|
|
@@ -101,7 +101,7 @@ test('a fresh revive is pointed at its subscriptions\' on-disk report history (c
|
|
|
101
101
|
assert.ok(!msg.includes(newer), 'absolute report paths not rendered');
|
|
102
102
|
assert.ok(/canvas history read <ref>/.test(msg), 'points at one-report expansion');
|
|
103
103
|
assert.ok(/canvas history show <node-id> --type report/.test(msg), 'points at full per-node expansion');
|
|
104
|
-
assert.ok(/
|
|
104
|
+
assert.ok(/canvas history show <node-id> --type inbox/.test(msg), 'points at the cursor-independent full-history replay');
|
|
105
105
|
});
|
|
106
106
|
test('buildReviveKickoff is pure — building twice eats nothing', () => {
|
|
107
107
|
const id = 'n2';
|
|
@@ -34,7 +34,8 @@ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
34
34
|
import { tmpdir } from 'node:os';
|
|
35
35
|
import { join } from 'node:path';
|
|
36
36
|
import { spawn, spawnSync } from 'node:child_process';
|
|
37
|
-
import { createNode } from '../canvas/canvas.js';
|
|
37
|
+
import { createNode, subscribe } from '../canvas/canvas.js';
|
|
38
|
+
import { readInboxSince } from '../feed/inbox.js';
|
|
38
39
|
import { closeDb } from '../canvas/db.js';
|
|
39
40
|
import { buildPiArgv } from '../runtime/launch.js';
|
|
40
41
|
import { headlessBrokerHost } from '../runtime/host.js';
|
|
@@ -298,3 +299,40 @@ test('reviveNode crashes and clears pid when launch/preflight throws', async ()
|
|
|
298
299
|
await h.dispose();
|
|
299
300
|
}
|
|
300
301
|
});
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// Crash-to-dead doctrine wake — a launch-refusal marks the node dead; a
|
|
304
|
+
// manager waiting dormant on it must be told, or it hangs forever (the
|
|
305
|
+
// daemon no longer supervises a dead node). Mirrors closeNode's step-4 fan-out.
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
test('reviveNode fans a doctrine wake to subscribers when launch/preflight throws (crash-to-dead)', async () => {
|
|
308
|
+
const h = await createHarness({ headless: true, sessionPrefix: 'crtr-revive-crashwake' });
|
|
309
|
+
const originalLaunch = headlessBrokerHost.launch;
|
|
310
|
+
try {
|
|
311
|
+
const root = h.spawnRoot('crash-wake suite root');
|
|
312
|
+
const sessionFile = join(home, 'sessions', 'crtr-revive-crashwake.jsonl');
|
|
313
|
+
mkdirSync(join(home, 'sessions'), { recursive: true });
|
|
314
|
+
writeFileSync(sessionFile, '{}\n', 'utf8');
|
|
315
|
+
const M = h.fabricateBrokerNode({
|
|
316
|
+
parent: root,
|
|
317
|
+
status: 'active',
|
|
318
|
+
intent: null,
|
|
319
|
+
pi_pid: deadPid(),
|
|
320
|
+
pi_session_id: 'uuid-3',
|
|
321
|
+
pi_session_file: sessionFile,
|
|
322
|
+
});
|
|
323
|
+
// spawnChild-equivalent auto-subscription — done explicitly since this node
|
|
324
|
+
// was fabricated directly, not spawned through the CLI.
|
|
325
|
+
subscribe(root, M, true);
|
|
326
|
+
headlessBrokerHost.launch = (_nodeId, _inv, _opts) => { throw new Error('boom'); };
|
|
327
|
+
assert.throws(() => reviveNode(M, { resume: true }), /refusing to revive/i);
|
|
328
|
+
assert.equal(h.node(M).status, 'dead', 'failed revive marks the node dead');
|
|
329
|
+
const entries = readInboxSince(root, undefined);
|
|
330
|
+
const notice = entries.find((e) => e.data?.['reason'] === 'child-crashed' && e.data?.['child'] === M);
|
|
331
|
+
assert.ok(notice !== undefined, 'the surviving parent got a doctrine wake for the dead child');
|
|
332
|
+
assert.equal(notice.from, M, 'the notice is attributed to the crashed child');
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
headlessBrokerHost.launch = originalLaunch;
|
|
336
|
+
await h.dispose();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
@@ -21,7 +21,7 @@ import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, jobDir, }
|
|
|
21
21
|
/** The identity keys meta.json persists. Listed explicitly so no runtime field
|
|
22
22
|
* can ever leak onto disk even when a fully-hydrated NodeMeta is handed in. */
|
|
23
23
|
const IDENTITY_KEYS = [
|
|
24
|
-
'node_id', 'name', 'description', 'cycles', 'created', 'cwd', 'host_kind', 'kind', 'mode',
|
|
24
|
+
'node_id', 'name', 'description', 'cycles', 'created', 'cwd', 'host_kind', 'profile_id', 'kind', 'mode',
|
|
25
25
|
'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'fork_from', 'passive_default',
|
|
26
26
|
'pi_session_id', 'pi_session_file', 'model_override', 'launch',
|
|
27
27
|
];
|
|
@@ -101,13 +101,13 @@ function writeMeta(meta) {
|
|
|
101
101
|
* state. A fresh insert takes the schema defaults for runtime. */
|
|
102
102
|
function upsertRow(meta) {
|
|
103
103
|
openDb()
|
|
104
|
-
.prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, cwd, host_kind, parent, created)
|
|
105
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
104
|
+
.prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
106
106
|
ON CONFLICT(node_id) DO UPDATE SET
|
|
107
107
|
name=excluded.name, kind=excluded.kind, mode=excluded.mode,
|
|
108
108
|
lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
|
|
109
|
-
parent=excluded.parent`)
|
|
110
|
-
.run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.parent ?? null, meta.created);
|
|
109
|
+
profile_id=excluded.profile_id, parent=excluded.parent`)
|
|
110
|
+
.run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created);
|
|
111
111
|
}
|
|
112
112
|
/** Seed a node's row at BIRTH: identity columns + runtime columns taken from the
|
|
113
113
|
* incoming meta (defaults: status='active', the rest null). The only writer
|
|
@@ -116,17 +116,17 @@ function upsertRow(meta) {
|
|
|
116
116
|
function seedRow(meta) {
|
|
117
117
|
openDb()
|
|
118
118
|
.prepare(`INSERT INTO nodes
|
|
119
|
-
(node_id, name, kind, mode, lifecycle, cwd, host_kind, parent, created,
|
|
119
|
+
(node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created,
|
|
120
120
|
status, intent, pi_pid, "window", tmux_session, pane)
|
|
121
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
121
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
122
122
|
ON CONFLICT(node_id) DO UPDATE SET
|
|
123
123
|
name=excluded.name, kind=excluded.kind, mode=excluded.mode,
|
|
124
124
|
lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
|
|
125
|
-
parent=excluded.parent,
|
|
125
|
+
profile_id=excluded.profile_id, parent=excluded.parent,
|
|
126
126
|
status=excluded.status, intent=excluded.intent, pi_pid=excluded.pi_pid,
|
|
127
127
|
"window"=excluded."window", tmux_session=excluded.tmux_session,
|
|
128
128
|
pane=excluded.pane`)
|
|
129
|
-
.run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.parent ?? null, meta.created, meta.status ?? 'active', meta.intent ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
|
|
129
|
+
.run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created, meta.status ?? 'active', meta.intent ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
|
|
130
130
|
}
|
|
131
131
|
function rowFrom(r) {
|
|
132
132
|
return {
|
|
@@ -138,6 +138,7 @@ function rowFrom(r) {
|
|
|
138
138
|
status: r['status'],
|
|
139
139
|
cwd: r['cwd'],
|
|
140
140
|
host_kind: r['host_kind'] ?? null,
|
|
141
|
+
profile_id: r['profile_id'] ?? null,
|
|
141
142
|
parent: r['parent'] ?? null,
|
|
142
143
|
created: r['created'],
|
|
143
144
|
intent: r['intent'] ?? null,
|
package/dist/core/canvas/db.js
CHANGED
|
@@ -349,6 +349,16 @@ CREATE TABLE IF NOT EXISTS canvas_meta (
|
|
|
349
349
|
);
|
|
350
350
|
`);
|
|
351
351
|
}
|
|
352
|
+
/** v11 — additive identity column `profile_id`: the profile-directory id
|
|
353
|
+
* (`<slug>-<id>`) a node runs under, chosen once at spawn (inherited from the
|
|
354
|
+
* spawner, or `--profile`). Durable IDENTITY mirrored from meta.json like
|
|
355
|
+
* `cwd`/`host_kind` (NOT runtime like `pane`), so it survives an index rebuild
|
|
356
|
+
* via the `IDENTITY_KEYS`/`upsertRow` path. Defaults NULL (root/no-profile);
|
|
357
|
+
* no data backfill. Additive, forward-only. */
|
|
358
|
+
function addProfileIdColumn(db) {
|
|
359
|
+
const columns = nodeColumns(db);
|
|
360
|
+
addNodeColumnIfMissing(db, columns, 'profile_id', `ALTER TABLE nodes ADD COLUMN profile_id TEXT;`);
|
|
361
|
+
}
|
|
352
362
|
/** The ordered migration list. Index `i` is migration version `i + 1`; the db's
|
|
353
363
|
* `user_version` tracks how many have been applied. Append only. */
|
|
354
364
|
export const MIGRATIONS = [
|
|
@@ -362,6 +372,7 @@ export const MIGRATIONS = [
|
|
|
362
372
|
/* v8 */ addHostKindColumn,
|
|
363
373
|
/* v9 */ migrateWakeupsToTriggers,
|
|
364
374
|
/* v10 */ addCanvasMetaTable,
|
|
375
|
+
/* v11 */ addProfileIdColumn,
|
|
365
376
|
];
|
|
366
377
|
/** Bring `db` up to the latest schema version. Reads `user_version`, runs each
|
|
367
378
|
* pending migration in order, and bumps `user_version` after each so the work
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { NodeStatus } from './types.js';
|
|
2
|
-
export type HistorySource = 'report' | 'doc' | 'roadmap' | 'meta';
|
|
2
|
+
export type HistorySource = 'report' | 'doc' | 'roadmap' | 'meta' | 'inbox';
|
|
3
3
|
/** One searchable unit of the episodic record: a report, a context doc, a
|
|
4
4
|
* node's roadmap, or a node's meta (name/description/kind). */
|
|
5
5
|
export interface HistoryArtifact {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { statSync, readdirSync } from 'node:fs';
|
|
13
13
|
import { join, relative, sep } from 'node:path';
|
|
14
14
|
import { getNode, listNodes, view } from './canvas.js';
|
|
15
|
-
import { contextDir, reportsDir, nodeDir } from './paths.js';
|
|
15
|
+
import { contextDir, reportsDir, nodeDir, inboxPath } from './paths.js';
|
|
16
16
|
import { readTextIfExists, walkFiles, pathExists } from '../fs-utils.js';
|
|
17
17
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
18
18
|
/** The cwd the caller means by default: the calling node's cwd (resolved from
|
|
@@ -126,6 +126,68 @@ function docArtifact(row, desc, abs) {
|
|
|
126
126
|
loadBody: load,
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
/** Every entry ever appended to a node's inbox.jsonl, oldest first, cursor-
|
|
130
|
+
* independent (the cursor only gates the live wake path, not this read).
|
|
131
|
+
* Empty when the node has no inbox file yet. */
|
|
132
|
+
function allInboxEntries(nodeId) {
|
|
133
|
+
const raw = readTextIfExists(inboxPath(nodeId));
|
|
134
|
+
if (raw === null)
|
|
135
|
+
return [];
|
|
136
|
+
return raw
|
|
137
|
+
.split('\n')
|
|
138
|
+
.filter((l) => l.trim() !== '')
|
|
139
|
+
.map((l) => JSON.parse(l));
|
|
140
|
+
}
|
|
141
|
+
/** One inbox entry's full content: a direct message or system alert's
|
|
142
|
+
* un-clipped body — dereferenced from its spilled ref file when the inline
|
|
143
|
+
* preview clipped, else the inline body itself — or, for a push pointer
|
|
144
|
+
* (which carries no body of its own), its kind/label plus the sender-side
|
|
145
|
+
* report path it points to.
|
|
146
|
+
*
|
|
147
|
+
* A spilled ref (message/system notice too long to inline) is arbitrary
|
|
148
|
+
* text with no crouter frontmatter contract — read it RAW, unlike a report
|
|
149
|
+
* ref, which always carries the `---\n...\n---` header stripped by
|
|
150
|
+
* fileBodyLoader. The presence of an inline `data.body` preview is what
|
|
151
|
+
* distinguishes the two: only a spilled message/notice ever carries one
|
|
152
|
+
* (a push pointer's entry has no `data` field at all). */
|
|
153
|
+
function inboxEntryBody(e) {
|
|
154
|
+
const meta = [`from: ${e.from ?? 'system'}`, `tier: ${e.tier}`].join('\n');
|
|
155
|
+
const header = `[${e.kind}] ${e.label}`;
|
|
156
|
+
const inlinePreview = typeof e.data?.['body'] === 'string' ? e.data['body'].trim() : '';
|
|
157
|
+
const isSpilledMessage = typeof e.data?.['body'] === 'string';
|
|
158
|
+
const full = e.ref !== undefined
|
|
159
|
+
? isSpilledMessage
|
|
160
|
+
? (readTextIfExists(e.ref) ?? '')
|
|
161
|
+
: fileBodyLoader(e.ref).load()
|
|
162
|
+
: inlinePreview;
|
|
163
|
+
const hasBody = full !== '' && full !== e.label;
|
|
164
|
+
const parts = [meta, header];
|
|
165
|
+
if (e.ref !== undefined)
|
|
166
|
+
parts.push(`ref: ${e.ref}`);
|
|
167
|
+
if (hasBody)
|
|
168
|
+
parts.push(full);
|
|
169
|
+
return parts.join('\n\n');
|
|
170
|
+
}
|
|
171
|
+
function inboxArtifact(row, desc, entry, index) {
|
|
172
|
+
const relpath = `inbox/${String(index).padStart(6, '0')}`;
|
|
173
|
+
const body = inboxEntryBody(entry);
|
|
174
|
+
return {
|
|
175
|
+
ref: `${row.node_id}:${relpath}`,
|
|
176
|
+
nodeId: row.node_id,
|
|
177
|
+
relpath,
|
|
178
|
+
source: 'inbox',
|
|
179
|
+
ts: entry.ts,
|
|
180
|
+
tsMs: toMs(entry.ts),
|
|
181
|
+
title: `[${entry.kind}] from ${entry.from ?? 'system'}: ${entry.label}`,
|
|
182
|
+
nodeName: row.name,
|
|
183
|
+
nodeKind: row.kind,
|
|
184
|
+
nodeStatus: row.status,
|
|
185
|
+
nodeCwd: row.cwd,
|
|
186
|
+
nodeDesc: desc,
|
|
187
|
+
path: null,
|
|
188
|
+
loadBody: () => body,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
129
191
|
function metaArtifact(row, desc) {
|
|
130
192
|
const body = `${row.name}\n${desc}\n${row.kind}`;
|
|
131
193
|
return {
|
|
@@ -189,7 +251,9 @@ export function buildCorpus(scope, corpus) {
|
|
|
189
251
|
return false;
|
|
190
252
|
return true;
|
|
191
253
|
});
|
|
192
|
-
// --report-kind implies type=report.
|
|
254
|
+
// --report-kind implies type=report. `inbox` is opt-in only — never part of
|
|
255
|
+
// the default set — since a node's raw inbox history is unbounded diagnostic
|
|
256
|
+
// detail, not curated past work.
|
|
193
257
|
const wantReportKind = corpus.reportKind;
|
|
194
258
|
const types = corpus.types !== undefined && corpus.types.length > 0
|
|
195
259
|
? corpus.types
|
|
@@ -199,7 +263,7 @@ export function buildCorpus(scope, corpus) {
|
|
|
199
263
|
const want = new Set(types);
|
|
200
264
|
const out = [];
|
|
201
265
|
for (const row of survivors) {
|
|
202
|
-
const desc = want.has('meta') || want.has('report') || want.has('doc') || want.has('roadmap')
|
|
266
|
+
const desc = want.has('meta') || want.has('report') || want.has('doc') || want.has('roadmap') || want.has('inbox')
|
|
203
267
|
? nodeDescOf(row.node_id)
|
|
204
268
|
: '';
|
|
205
269
|
if (want.has('meta'))
|
|
@@ -224,6 +288,9 @@ export function buildCorpus(scope, corpus) {
|
|
|
224
288
|
out.push(a);
|
|
225
289
|
}
|
|
226
290
|
}
|
|
291
|
+
if (want.has('inbox')) {
|
|
292
|
+
allInboxEntries(row.node_id).forEach((entry, i) => out.push(inboxArtifact(row, desc, entry, i)));
|
|
293
|
+
}
|
|
227
294
|
}
|
|
228
295
|
// Time window on artifact ts.
|
|
229
296
|
return out.filter((a) => {
|
|
@@ -274,6 +341,22 @@ export function resolveRef(ref) {
|
|
|
274
341
|
path: null,
|
|
275
342
|
};
|
|
276
343
|
}
|
|
344
|
+
if (relpath.startsWith('inbox/')) {
|
|
345
|
+
const index = Number(relpath.slice('inbox/'.length));
|
|
346
|
+
const entry = allInboxEntries(nodeId)[index];
|
|
347
|
+
if (entry === undefined)
|
|
348
|
+
return null;
|
|
349
|
+
return {
|
|
350
|
+
nodeId,
|
|
351
|
+
relpath,
|
|
352
|
+
source: 'inbox',
|
|
353
|
+
ts: entry.ts,
|
|
354
|
+
nodeName: node.name,
|
|
355
|
+
body: inboxEntryBody(entry),
|
|
356
|
+
raw: null,
|
|
357
|
+
path: null,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
277
360
|
if (relpath.includes('..') || relpath.startsWith('/')) {
|
|
278
361
|
throw new Error(`illegal relpath in ref: ${relpath}`);
|
|
279
362
|
}
|
|
@@ -321,6 +404,7 @@ export function nodeArtifacts(nodeId, types) {
|
|
|
321
404
|
status: node.status,
|
|
322
405
|
cwd: node.cwd,
|
|
323
406
|
host_kind: node.host_kind ?? null,
|
|
407
|
+
profile_id: node.profile_id ?? null,
|
|
324
408
|
parent: node.parent ?? null,
|
|
325
409
|
created: node.created,
|
|
326
410
|
intent: node.intent ?? null,
|
|
@@ -329,6 +413,7 @@ export function nodeArtifacts(nodeId, types) {
|
|
|
329
413
|
tmux_session: node.tmux_session ?? null,
|
|
330
414
|
pane: node.pane ?? null,
|
|
331
415
|
};
|
|
416
|
+
// `inbox` is opt-in only, same as buildCorpus — must be requested explicitly.
|
|
332
417
|
const want = new Set(types !== undefined && types.length > 0 ? types : ['report', 'doc', 'roadmap']);
|
|
333
418
|
const desc = node.description ?? '';
|
|
334
419
|
const out = [];
|
|
@@ -349,6 +434,9 @@ export function nodeArtifacts(nodeId, types) {
|
|
|
349
434
|
out.push(a);
|
|
350
435
|
}
|
|
351
436
|
}
|
|
437
|
+
if (want.has('inbox')) {
|
|
438
|
+
allInboxEntries(nodeId).forEach((entry, i) => out.push(inboxArtifact(row, desc, entry, i)));
|
|
439
|
+
}
|
|
352
440
|
return out;
|
|
353
441
|
}
|
|
354
442
|
/** Bounded per-cwd aggregate for the branch `-h` `<corpus>` block. Counts files
|
|
@@ -26,6 +26,12 @@ export declare function nodeDir(nodeId: string): string;
|
|
|
26
26
|
export declare function contextDir(nodeId: string): string;
|
|
27
27
|
export declare function jobDir(nodeId: string): string;
|
|
28
28
|
export declare function reportsDir(nodeId: string): string;
|
|
29
|
+
/** Overflow store for inbox entries whose inline body exceeds the digest's
|
|
30
|
+
* bounded preview (a direct `node msg`, a daemon trigger/deadline notice, a
|
|
31
|
+
* close/cancel doctrine wake) — the full body spills here, mirroring how a
|
|
32
|
+
* push's body always lives in a file, never inline; the inbox entry then
|
|
33
|
+
* carries `ref` at this path, same as a push pointer. */
|
|
34
|
+
export declare function messagesDir(nodeId: string): string;
|
|
29
35
|
export declare function nodeMetaPath(nodeId: string): string;
|
|
30
36
|
export declare function inboxPath(nodeId: string): string;
|
|
31
37
|
/** Passive-subscription accumulator. Pushes from publishers this node subscribes
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// context/ roadmap.md, initial-prompt.md, explore-*.md, artifacts
|
|
8
8
|
// job/ log.jsonl, telemetry.json
|
|
9
9
|
// reports/ append-only push history (<ts>-<kind>.md)
|
|
10
|
+
// messages/ overflow bodies for inbox entries too long to inline
|
|
10
11
|
// inbox.jsonl messages + coalesced subscription feed
|
|
11
12
|
// transcript.jsonl mirror/pointer of the pi session
|
|
12
13
|
// session.ptr pi session id/path
|
|
@@ -73,6 +74,14 @@ export function jobDir(nodeId) {
|
|
|
73
74
|
export function reportsDir(nodeId) {
|
|
74
75
|
return join(nodeDir(nodeId), 'reports');
|
|
75
76
|
}
|
|
77
|
+
/** Overflow store for inbox entries whose inline body exceeds the digest's
|
|
78
|
+
* bounded preview (a direct `node msg`, a daemon trigger/deadline notice, a
|
|
79
|
+
* close/cancel doctrine wake) — the full body spills here, mirroring how a
|
|
80
|
+
* push's body always lives in a file, never inline; the inbox entry then
|
|
81
|
+
* carries `ref` at this path, same as a push pointer. */
|
|
82
|
+
export function messagesDir(nodeId) {
|
|
83
|
+
return join(nodeDir(nodeId), 'messages');
|
|
84
|
+
}
|
|
76
85
|
export function nodeMetaPath(nodeId) {
|
|
77
86
|
return join(nodeDir(nodeId), 'meta.json');
|
|
78
87
|
}
|