@north-light/crouter 0.3.263 → 0.3.269
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 +5 -1
- package/dist/api/client.js +6 -0
- package/dist/api/dto/canvas.d.ts +53 -2
- package/dist/api/dto/nodes.d.ts +6 -2
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +7 -2
- package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
- package/dist/clients/attach/session/whip-achievements.js +145 -0
- package/dist/clients/attach/slash/dispatch.js +5 -3
- package/dist/clients/attach/viewer.js +607 -606
- package/dist/commands/canvas-history/grep.js +3 -22
- package/dist/commands/canvas-history/read.js +5 -3
- package/dist/commands/canvas-history/search.js +3 -22
- package/dist/commands/canvas-history/shared.d.ts +8 -0
- package/dist/commands/canvas-history/shared.js +64 -1
- package/dist/commands/canvas-history/stats.d.ts +1 -0
- package/dist/commands/canvas-history/stats.js +68 -0
- package/dist/commands/canvas-history.js +4 -3
- package/dist/commands/node/create.js +2 -2
- package/dist/commands/sys/config.js +19 -2
- package/dist/commands/sys/panels/models-panel.js +3 -1
- package/dist/core/__tests__/integration/revive.test.js +4 -2
- package/dist/core/__tests__/model-pin-durability.test.js +25 -1
- package/dist/core/__tests__/relaunch-root.test.js +17 -0
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
- package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
- package/dist/core/broker-client/transport-relay.js +5 -1
- package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
- package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +13 -10
- package/dist/core/canvas/browse/model.js +16 -1
- package/dist/core/canvas/browse/render.d.ts +23 -3
- package/dist/core/canvas/browse/render.js +429 -64
- package/dist/core/canvas/canvas.js +12 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.d.ts +36 -1
- package/dist/core/canvas/history.js +328 -7
- package/dist/core/canvas/labels.d.ts +2 -2
- package/dist/core/canvas/migrations.js +32 -0
- package/dist/core/canvas/node-recap.d.ts +23 -0
- package/dist/core/canvas/node-recap.js +67 -0
- package/dist/core/canvas/render-source.d.ts +33 -1
- package/dist/core/canvas/render-source.js +80 -22
- package/dist/core/canvas/types.d.ts +9 -0
- package/dist/core/config.js +13 -5
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/model-routes.js +1 -1
- package/dist/core/runtime/broker/auth-reload.js +4 -1
- package/dist/core/runtime/broker/fault-retry.js +7 -2
- package/dist/core/runtime/broker/frame-client.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.js +7 -0
- package/dist/core/runtime/broker/inbox.d.ts +12 -2
- package/dist/core/runtime/broker/inbox.js +68 -38
- package/dist/core/runtime/broker-protocol.d.ts +6 -0
- package/dist/core/runtime/broker.js +14 -0
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/interactive-deliver.d.ts +8 -2
- package/dist/core/runtime/interactive-deliver.js +12 -3
- package/dist/core/runtime/launch-target.js +1 -1
- package/dist/core/runtime/launch.d.ts +12 -6
- package/dist/core/runtime/launch.js +19 -17
- package/dist/core/runtime/model-swap.d.ts +5 -4
- package/dist/core/runtime/model-swap.js +7 -5
- package/dist/core/runtime/promote.js +2 -1
- package/dist/core/runtime/reset.js +4 -2
- package/dist/core/runtime/revive.js +22 -0
- package/dist/core/runtime/spawn.d.ts +3 -3
- package/dist/core/runtime/spawn.js +20 -3
- package/dist/core/runtime/stamp/channel.d.ts +18 -0
- package/dist/core/runtime/stamp/channel.js +74 -0
- package/dist/core/runtime/stamp/protocol.d.ts +86 -0
- package/dist/core/runtime/stamp/protocol.js +113 -0
- package/dist/core/runtime/tmux-driver.d.ts +11 -1
- package/dist/core/runtime/tmux-driver.js +27 -11
- package/dist/core/self-update.js +2 -5
- package/dist/core/tui/draw.d.ts +1 -0
- package/dist/core/tui/draw.js +5 -1
- package/dist/core/tui/markdown.d.ts +17 -0
- package/dist/core/tui/markdown.js +407 -0
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
- package/dist/daemon/api/bridge.d.ts +3 -2
- package/dist/daemon/api/bridge.js +34 -9
- package/dist/daemon/api/handlers/canvas.js +62 -16
- package/dist/daemon/api/handlers/messages.js +12 -1
- package/dist/daemon/api/handlers/nodes.js +10 -3
- package/dist/daemon/api/map.js +2 -0
- package/dist/daemon/messaging/node-message.js +15 -1
- package/dist/daemon/reconcilers/broker-supervision.js +26 -9
- package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
- package/dist/daemon/reconcilers/live-obligation.js +17 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
- package/dist/pi-extensions/canvas-recap.js +4 -0
- package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
- package/dist/pi-extensions/canvas-stamp.js +77 -0
- package/dist/pi-extensions/canvas-stophook.js +3 -0
- package/dist/shared/__tests__/env-boundary.test.js +4 -2
- package/dist/shared/crtr-version.d.ts +2 -0
- package/dist/shared/crtr-version.js +18 -0
- package/dist/shared/env.d.ts +10 -0
- package/dist/shared/env.js +19 -4
- package/dist/types.d.ts +10 -0
- package/dist/types.js +4 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -83,14 +83,15 @@ function writeMeta(meta) {
|
|
|
83
83
|
* state. A fresh insert takes the schema defaults for runtime. */
|
|
84
84
|
function upsertRow(meta) {
|
|
85
85
|
openDb()
|
|
86
|
-
.prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file)
|
|
87
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
86
|
+
.prepare(`INSERT INTO nodes (node_id, name, description, cycles, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file)
|
|
87
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
88
88
|
ON CONFLICT(node_id) DO UPDATE SET
|
|
89
|
-
name=excluded.name,
|
|
89
|
+
name=excluded.name, description=excluded.description, cycles=excluded.cycles,
|
|
90
|
+
kind=excluded.kind, mode=excluded.mode,
|
|
90
91
|
lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
|
|
91
92
|
profile_id=excluded.profile_id, parent=excluded.parent,
|
|
92
93
|
pi_session_file=excluded.pi_session_file`)
|
|
93
|
-
.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, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null);
|
|
94
|
+
.run(meta.node_id, meta.name, meta.description ?? null, meta.cycles ?? null, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null);
|
|
94
95
|
}
|
|
95
96
|
/** Seed a node's row at BIRTH: identity columns + runtime columns taken from the
|
|
96
97
|
* incoming meta (defaults: status='active', the rest null). The only writer
|
|
@@ -99,11 +100,12 @@ function upsertRow(meta) {
|
|
|
99
100
|
function seedRow(meta) {
|
|
100
101
|
openDb()
|
|
101
102
|
.prepare(`INSERT INTO nodes
|
|
102
|
-
(node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file,
|
|
103
|
+
(node_id, name, description, cycles, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file,
|
|
103
104
|
status, intent, waiting_for, final_report, finalized_at, pi_pid, "window", tmux_session, pane)
|
|
104
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
105
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
105
106
|
ON CONFLICT(node_id) DO UPDATE SET
|
|
106
|
-
name=excluded.name,
|
|
107
|
+
name=excluded.name, description=excluded.description, cycles=excluded.cycles,
|
|
108
|
+
kind=excluded.kind, mode=excluded.mode,
|
|
107
109
|
lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
|
|
108
110
|
profile_id=excluded.profile_id, parent=excluded.parent,
|
|
109
111
|
pi_session_file=excluded.pi_session_file,
|
|
@@ -111,12 +113,14 @@ function seedRow(meta) {
|
|
|
111
113
|
final_report=excluded.final_report, finalized_at=excluded.finalized_at,
|
|
112
114
|
pi_pid=excluded.pi_pid, "window"=excluded."window", tmux_session=excluded.tmux_session,
|
|
113
115
|
pane=excluded.pane`)
|
|
114
|
-
.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, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null, meta.status ?? 'active', meta.intent ?? null, meta.waiting_for ?? null, meta.final_report ?? null, meta.finalized_at ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
|
|
116
|
+
.run(meta.node_id, meta.name, meta.description ?? null, meta.cycles ?? null, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null, meta.status ?? 'active', meta.intent ?? null, meta.waiting_for ?? null, meta.final_report ?? null, meta.finalized_at ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
|
|
115
117
|
}
|
|
116
118
|
function rowFrom(r) {
|
|
117
119
|
return {
|
|
118
120
|
node_id: r['node_id'],
|
|
119
121
|
name: r['name'],
|
|
122
|
+
description: r['description'] ?? undefined,
|
|
123
|
+
cycles: r['cycles'] ?? undefined,
|
|
120
124
|
kind: r['kind'],
|
|
121
125
|
mode: r['mode'],
|
|
122
126
|
lifecycle: r['lifecycle'],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-review-boundary", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-tool-guide", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve", "canvas-preview-result", "canvas-prompt-scrub"];
|
|
1
|
+
export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-review-boundary", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-tool-guide", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve", "canvas-preview-result", "canvas-prompt-scrub", "canvas-stamp"];
|
|
2
2
|
/** Structural shipped-hook identity: a canvas-hook stem directly under a
|
|
3
3
|
* `pi-extensions` directory. A user's own extension elsewhere may share the
|
|
4
4
|
* stem and is not shipped. */
|
|
@@ -7,9 +7,15 @@ export const CANVAS_EXTENSION_NAMES = [
|
|
|
7
7
|
'canvas-inbox-watcher', 'canvas-review-boundary', 'canvas-stophook', 'canvas-recap',
|
|
8
8
|
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
9
9
|
'canvas-tool-guide', 'canvas-doc-substrate', 'canvas-structured-output',
|
|
10
|
-
'canvas-bash-valve', 'canvas-preview-result', 'canvas-prompt-scrub',
|
|
10
|
+
'canvas-bash-valve', 'canvas-preview-result', 'canvas-prompt-scrub', 'canvas-stamp',
|
|
11
11
|
];
|
|
12
12
|
const HISTORICAL_CANVAS_EXTENSION_PREFIXES = [
|
|
13
|
+
[
|
|
14
|
+
'canvas-inbox-watcher', 'canvas-review-boundary', 'canvas-stophook', 'canvas-recap',
|
|
15
|
+
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
16
|
+
'canvas-tool-guide', 'canvas-doc-substrate', 'canvas-structured-output',
|
|
17
|
+
'canvas-bash-valve', 'canvas-preview-result', 'canvas-prompt-scrub',
|
|
18
|
+
],
|
|
13
19
|
[
|
|
14
20
|
'canvas-inbox-watcher', 'canvas-review-boundary', 'canvas-stophook', 'canvas-recap',
|
|
15
21
|
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { NodeStatus } from './types.js';
|
|
2
|
-
export type HistorySource = 'report' | 'doc' | 'roadmap' | 'meta' | 'inbox';
|
|
2
|
+
export type HistorySource = 'report' | 'doc' | 'roadmap' | 'meta' | 'inbox' | 'transcript';
|
|
3
|
+
/** Every origin token `--origin` accepts. `human` is a person at a front door;
|
|
4
|
+
* the rest mirror the runtime origin kinds; `unknown` is a message that
|
|
5
|
+
* carries no stamp at all — its own bucket, never folded into another, since
|
|
6
|
+
* a rising unknown rate is how a delivery seam that stopped announcing shows
|
|
7
|
+
* itself. */
|
|
8
|
+
export declare const ORIGIN_TOKENS: readonly ["human", "inbox", "wake", "revive", "restart-continuation", "stop-guard", "park", "recovery", "context-nudge", "kickoff", "unknown"];
|
|
9
|
+
/** Message roles carried by a transcript artifact. */
|
|
10
|
+
export declare const MESSAGE_ROLES: readonly ["user", "assistant", "toolResult"];
|
|
3
11
|
/** One searchable unit of the episodic record: a report, a context doc, a
|
|
4
12
|
* node's roadmap, or a node's meta (name/description/kind). */
|
|
5
13
|
export interface HistoryArtifact {
|
|
@@ -26,6 +34,16 @@ export interface HistoryArtifact {
|
|
|
26
34
|
path: string | null;
|
|
27
35
|
/** Frontmatter-stripped body, loaded once and cached. Empty on read error. */
|
|
28
36
|
loadBody: () => string;
|
|
37
|
+
/** What drove this message to arrive. Set ONLY on inbound artifacts — a
|
|
38
|
+
* user-role transcript message or an inbox entry — because origin is a
|
|
39
|
+
* property of arrival. An assistant turn leaves it undefined, so `--origin`
|
|
40
|
+
* excludes generated turns without needing a role filter beside it. */
|
|
41
|
+
origin?: string;
|
|
42
|
+
/** Transcript only: user | assistant | toolResult. */
|
|
43
|
+
role?: string;
|
|
44
|
+
/** Sender attribution: an inbox stamp's delivered entries, or an inbox
|
|
45
|
+
* entry's own sender. Comma-joined when a delivery carried several. */
|
|
46
|
+
from?: string;
|
|
29
47
|
}
|
|
30
48
|
export interface ScopeFilter {
|
|
31
49
|
/** A specific cwd. Default (all flags absent) = the caller's cwd. */
|
|
@@ -48,6 +66,12 @@ export interface CorpusFilter {
|
|
|
48
66
|
/** Inclusive lower / upper bound on artifact ts, epoch ms. */
|
|
49
67
|
sinceMs?: number;
|
|
50
68
|
untilMs?: number;
|
|
69
|
+
/** Message-attribute narrowing — meaningful for transcript + inbox only.
|
|
70
|
+
* An artifact without the field never survives the corresponding filter. */
|
|
71
|
+
origins?: string[];
|
|
72
|
+
roles?: string[];
|
|
73
|
+
/** Case-insensitive substring against the artifact's sender attribution. */
|
|
74
|
+
from?: string;
|
|
51
75
|
}
|
|
52
76
|
/** The cwd the caller means by default: the calling node's cwd (resolved from
|
|
53
77
|
* CRTR_NODE_ID via the canvas data layer), falling back to the process cwd
|
|
@@ -62,6 +86,17 @@ export declare function newestReportRef(nodeId: string): string | null;
|
|
|
62
86
|
* are applied against the db rows first (cheap); only surviving nodes' files
|
|
63
87
|
* are read. */
|
|
64
88
|
export declare function buildCorpus(scope: ScopeFilter, corpus: CorpusFilter): HistoryArtifact[];
|
|
89
|
+
export type GroupKey = 'origin' | 'node' | 'kind' | 'day' | 'type' | 'role';
|
|
90
|
+
export interface CorpusGroup {
|
|
91
|
+
key: string;
|
|
92
|
+
count: number;
|
|
93
|
+
}
|
|
94
|
+
/** Collapse a built corpus into counts by one dimension, descending by count.
|
|
95
|
+
* Artifacts that do not carry the grouped dimension are omitted rather than
|
|
96
|
+
* bucketed as empty — grouping by origin answers "what drove arrivals", and a
|
|
97
|
+
* generated turn is not an arrival. `unknown` is a real origin value here, not
|
|
98
|
+
* a missing one. */
|
|
99
|
+
export declare function groupCorpus(arts: HistoryArtifact[], by: GroupKey): CorpusGroup[];
|
|
65
100
|
export interface ResolvedRef {
|
|
66
101
|
nodeId: string;
|
|
67
102
|
relpath: string;
|
|
@@ -13,10 +13,31 @@ import { envNodeId } from '../../shared/env.js';
|
|
|
13
13
|
import { statSync, readdirSync } from 'node:fs';
|
|
14
14
|
import { join, relative, sep } from 'node:path';
|
|
15
15
|
import { getNode, listNodes, view } from './canvas.js';
|
|
16
|
-
import { contextDir, reportsDir, nodeDir, inboxPath } from './paths.js';
|
|
16
|
+
import { contextDir, reportsDir, nodeDir, inboxPath, sessionPtrPath } from './paths.js';
|
|
17
17
|
import { readTextIfExists, walkFiles, pathExists } from '../fs-utils.js';
|
|
18
18
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
19
19
|
import { warn } from '../output.js';
|
|
20
|
+
import { readStamps } from '../runtime/stamp/protocol.js';
|
|
21
|
+
/** Every origin token `--origin` accepts. `human` is a person at a front door;
|
|
22
|
+
* the rest mirror the runtime origin kinds; `unknown` is a message that
|
|
23
|
+
* carries no stamp at all — its own bucket, never folded into another, since
|
|
24
|
+
* a rising unknown rate is how a delivery seam that stopped announcing shows
|
|
25
|
+
* itself. */
|
|
26
|
+
export const ORIGIN_TOKENS = [
|
|
27
|
+
'human',
|
|
28
|
+
'inbox',
|
|
29
|
+
'wake',
|
|
30
|
+
'revive',
|
|
31
|
+
'restart-continuation',
|
|
32
|
+
'stop-guard',
|
|
33
|
+
'park',
|
|
34
|
+
'recovery',
|
|
35
|
+
'context-nudge',
|
|
36
|
+
'kickoff',
|
|
37
|
+
'unknown',
|
|
38
|
+
];
|
|
39
|
+
/** Message roles carried by a transcript artifact. */
|
|
40
|
+
export const MESSAGE_ROLES = ['user', 'assistant', 'toolResult'];
|
|
20
41
|
/** The cwd the caller means by default: the calling node's cwd (resolved from
|
|
21
42
|
* CRTR_NODE_ID via the canvas data layer), falling back to the process cwd
|
|
22
43
|
* when not run as a node. */
|
|
@@ -184,7 +205,10 @@ function inboxEntryBody(e) {
|
|
|
184
205
|
function inboxArtifact(row, desc, entry, index) {
|
|
185
206
|
const relpath = `inbox/${String(index).padStart(6, '0')}`;
|
|
186
207
|
const body = inboxEntryBody(entry);
|
|
187
|
-
|
|
208
|
+
// An inbox entry IS an arrival, so it carries the same two inbound
|
|
209
|
+
// dimensions a stamped user message does — which is what lets `--origin` and
|
|
210
|
+
// `--from` span the transcript and inbox corpora with one vocabulary.
|
|
211
|
+
const a = {
|
|
188
212
|
ref: `${row.node_id}:${relpath}`,
|
|
189
213
|
nodeId: row.node_id,
|
|
190
214
|
relpath,
|
|
@@ -199,7 +223,201 @@ function inboxArtifact(row, desc, entry, index) {
|
|
|
199
223
|
nodeDesc: desc,
|
|
200
224
|
path: null,
|
|
201
225
|
loadBody: () => body,
|
|
226
|
+
origin: 'inbox',
|
|
227
|
+
};
|
|
228
|
+
if (typeof entry.from === 'string' && entry.from !== '')
|
|
229
|
+
a.from = entry.from;
|
|
230
|
+
return a;
|
|
231
|
+
}
|
|
232
|
+
/** The node's session `.jsonl`, or null when nothing has been written yet. pi
|
|
233
|
+
* binds the path at session creation but writes the file on the FIRST appended
|
|
234
|
+
* entry, so a fresh node has a bound path that is not yet on disk. Mirrors
|
|
235
|
+
* `runtime/node-read.ts`'s resolution: the recorded path first, then the
|
|
236
|
+
* `session.ptr` the node dir carries. */
|
|
237
|
+
function resolveSessionFile(nodeId) {
|
|
238
|
+
const node = getNode(nodeId);
|
|
239
|
+
const recorded = node?.pi_session_file;
|
|
240
|
+
if (typeof recorded === 'string' && recorded !== '' && pathExists(recorded))
|
|
241
|
+
return recorded;
|
|
242
|
+
const raw = readTextIfExists(sessionPtrPath(nodeId));
|
|
243
|
+
if (raw === null)
|
|
244
|
+
return null;
|
|
245
|
+
const target = raw.trim();
|
|
246
|
+
return target !== '' && pathExists(target) ? target : null;
|
|
247
|
+
}
|
|
248
|
+
/** The searchable text of a message's content blocks. `thinking` blocks are
|
|
249
|
+
* deliberately dropped: private model reasoning is excluded from every human
|
|
250
|
+
* transcript surface, and putting it in the search corpus would leak it back
|
|
251
|
+
* in through grep. */
|
|
252
|
+
function textFromBlocks(content) {
|
|
253
|
+
if (typeof content === 'string')
|
|
254
|
+
return content;
|
|
255
|
+
if (!Array.isArray(content))
|
|
256
|
+
return '';
|
|
257
|
+
const parts = [];
|
|
258
|
+
for (const block of content) {
|
|
259
|
+
if (block === null || typeof block !== 'object')
|
|
260
|
+
continue;
|
|
261
|
+
const b = block;
|
|
262
|
+
if (b['type'] === 'text' && typeof b['text'] === 'string')
|
|
263
|
+
parts.push(b['text']);
|
|
264
|
+
else if (b['type'] === 'toolCall')
|
|
265
|
+
parts.push(`Tool: ${typeof b['name'] === 'string' ? b['name'] : 'call'}`);
|
|
266
|
+
}
|
|
267
|
+
return parts.join('\n');
|
|
268
|
+
}
|
|
269
|
+
/** The single token `--origin` matches against. */
|
|
270
|
+
function originToken(origin) {
|
|
271
|
+
if (origin === null || origin === undefined)
|
|
272
|
+
return 'unknown';
|
|
273
|
+
return origin.type === 'human' ? 'human' : origin.kind;
|
|
274
|
+
}
|
|
275
|
+
/** Who a delivery came from, comma-joined when it carried several entries. */
|
|
276
|
+
function sendersOf(origin) {
|
|
277
|
+
if (origin === null || origin === undefined || origin.type !== 'runtime')
|
|
278
|
+
return undefined;
|
|
279
|
+
const entries = origin.entries;
|
|
280
|
+
if (entries === undefined || entries.length === 0)
|
|
281
|
+
return undefined;
|
|
282
|
+
const names = new Set();
|
|
283
|
+
for (const e of entries) {
|
|
284
|
+
const who = e.fromName ?? e.from;
|
|
285
|
+
if (typeof who === 'string' && who !== '')
|
|
286
|
+
names.add(who);
|
|
287
|
+
}
|
|
288
|
+
return names.size === 0 ? undefined : [...names].join(', ');
|
|
289
|
+
}
|
|
290
|
+
/** Parse a node's session file into its messages, each carrying the origin its
|
|
291
|
+
* stamp recorded. Deliberately a direct line parse rather than pi's
|
|
292
|
+
* SessionManager: reconstruction copies the file to a temp dir and rebuilds
|
|
293
|
+
* cycles, which is far too heavy to run across every node in a scan.
|
|
294
|
+
*
|
|
295
|
+
* Stamps are correlated by the target message's own `timestamp` (epoch ms) —
|
|
296
|
+
* never by tree position, because a stamp is physically written BEFORE the
|
|
297
|
+
* message it describes. */
|
|
298
|
+
function sessionMessages(nodeId) {
|
|
299
|
+
const file = resolveSessionFile(nodeId);
|
|
300
|
+
if (file === null)
|
|
301
|
+
return [];
|
|
302
|
+
const raw = readTextIfExists(file);
|
|
303
|
+
if (raw === null)
|
|
304
|
+
return [];
|
|
305
|
+
const entries = [];
|
|
306
|
+
for (const line of raw.split('\n')) {
|
|
307
|
+
if (line.trim() === '')
|
|
308
|
+
continue;
|
|
309
|
+
try {
|
|
310
|
+
entries.push(JSON.parse(line));
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// A truncated final line is normal while a broker is mid-append.
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const origins = new Map();
|
|
317
|
+
for (const stamp of readStamps(entries)) {
|
|
318
|
+
if (stamp.kind !== 'message')
|
|
319
|
+
continue;
|
|
320
|
+
const payload = stamp.payload;
|
|
321
|
+
if (typeof payload.msgTs !== 'number')
|
|
322
|
+
continue;
|
|
323
|
+
origins.set(payload.msgTs, payload.origin);
|
|
324
|
+
}
|
|
325
|
+
const out = [];
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (entry === null || typeof entry !== 'object')
|
|
328
|
+
continue;
|
|
329
|
+
const e = entry;
|
|
330
|
+
if (e['type'] !== 'message')
|
|
331
|
+
continue;
|
|
332
|
+
const message = e['message'];
|
|
333
|
+
if (message === null || typeof message !== 'object')
|
|
334
|
+
continue;
|
|
335
|
+
const m = message;
|
|
336
|
+
const role = typeof m['role'] === 'string' ? m['role'] : '';
|
|
337
|
+
if (role === '')
|
|
338
|
+
continue;
|
|
339
|
+
const msgTs = typeof m['timestamp'] === 'number' ? m['timestamp'] : undefined;
|
|
340
|
+
const iso = typeof e['timestamp'] === 'string'
|
|
341
|
+
? e['timestamp']
|
|
342
|
+
: msgTs !== undefined ? new Date(msgTs).toISOString() : '';
|
|
343
|
+
const text = textFromBlocks(m['content']);
|
|
344
|
+
const msg = {
|
|
345
|
+
index: out.length,
|
|
346
|
+
role,
|
|
347
|
+
text,
|
|
348
|
+
ts: iso,
|
|
349
|
+
tsMs: msgTs ?? toMs(iso),
|
|
350
|
+
};
|
|
351
|
+
// Origin is a property of ARRIVAL, so only an inbound turn carries one.
|
|
352
|
+
if (role === 'user') {
|
|
353
|
+
const origin = msgTs !== undefined && origins.has(msgTs) ? origins.get(msgTs) : null;
|
|
354
|
+
msg.origin = originToken(origin);
|
|
355
|
+
const from = sendersOf(origin);
|
|
356
|
+
if (from !== undefined)
|
|
357
|
+
msg.from = from;
|
|
358
|
+
}
|
|
359
|
+
out.push(msg);
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
/** The verbatim jsonl line that persisted the Nth MESSAGE. Message ordinals
|
|
364
|
+
* and line numbers differ — stamps, model changes, and session headers occupy
|
|
365
|
+
* lines of their own — so this re-walks the file counting message entries
|
|
366
|
+
* rather than indexing lines directly. */
|
|
367
|
+
function rawSessionLine(file, index) {
|
|
368
|
+
const raw = readTextIfExists(file);
|
|
369
|
+
if (raw === null)
|
|
370
|
+
return null;
|
|
371
|
+
let seen = 0;
|
|
372
|
+
for (const line of raw.split('\n')) {
|
|
373
|
+
if (line.trim() === '')
|
|
374
|
+
continue;
|
|
375
|
+
let parsed;
|
|
376
|
+
try {
|
|
377
|
+
parsed = JSON.parse(line);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
383
|
+
continue;
|
|
384
|
+
const e = parsed;
|
|
385
|
+
if (e['type'] !== 'message')
|
|
386
|
+
continue;
|
|
387
|
+
const m = e['message'];
|
|
388
|
+
if (m === null || typeof m !== 'object' || typeof m['role'] !== 'string')
|
|
389
|
+
continue;
|
|
390
|
+
if (seen === index)
|
|
391
|
+
return line;
|
|
392
|
+
seen++;
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
function transcriptArtifact(row, desc, msg) {
|
|
397
|
+
const relpath = `session/${String(msg.index).padStart(6, '0')}`;
|
|
398
|
+
const lead = msg.text.split('\n').map((l) => l.trim()).find((l) => l !== '') ?? '';
|
|
399
|
+
const a = {
|
|
400
|
+
ref: `${row.node_id}:${relpath}`,
|
|
401
|
+
nodeId: row.node_id,
|
|
402
|
+
relpath,
|
|
403
|
+
source: 'transcript',
|
|
404
|
+
ts: msg.ts,
|
|
405
|
+
tsMs: msg.tsMs,
|
|
406
|
+
title: `[${msg.role}] ${lead.length > 80 ? `${lead.slice(0, 80)}…` : lead}`,
|
|
407
|
+
nodeName: row.name,
|
|
408
|
+
nodeKind: row.kind,
|
|
409
|
+
nodeStatus: row.status,
|
|
410
|
+
nodeCwd: row.cwd,
|
|
411
|
+
nodeDesc: desc,
|
|
412
|
+
path: null,
|
|
413
|
+
loadBody: () => msg.text,
|
|
414
|
+
role: msg.role,
|
|
202
415
|
};
|
|
416
|
+
if (msg.origin !== undefined)
|
|
417
|
+
a.origin = msg.origin;
|
|
418
|
+
if (msg.from !== undefined)
|
|
419
|
+
a.from = msg.from;
|
|
420
|
+
return a;
|
|
203
421
|
}
|
|
204
422
|
function metaArtifact(row, desc) {
|
|
205
423
|
const body = `${row.name}\n${desc}\n${row.kind}`;
|
|
@@ -280,9 +498,10 @@ export function buildCorpus(scope, corpus) {
|
|
|
280
498
|
return false;
|
|
281
499
|
return true;
|
|
282
500
|
});
|
|
283
|
-
// --report-kind implies type=report. `inbox`
|
|
284
|
-
// the default set — since a node's raw inbox history
|
|
285
|
-
// detail, not curated past
|
|
501
|
+
// --report-kind implies type=report. `inbox` and `transcript` are opt-in
|
|
502
|
+
// only — never part of the default set — since a node's raw inbox history and
|
|
503
|
+
// its full conversation are unbounded diagnostic detail, not curated past
|
|
504
|
+
// work.
|
|
286
505
|
const wantReportKind = corpus.reportKind;
|
|
287
506
|
const types = corpus.types !== undefined && corpus.types.length > 0
|
|
288
507
|
? corpus.types
|
|
@@ -335,16 +554,76 @@ export function buildCorpus(scope, corpus) {
|
|
|
335
554
|
if (want.has('inbox')) {
|
|
336
555
|
allInboxEntries(row.node_id).forEach((entry, i) => out.push(inboxArtifact(row, desc, entry, i)));
|
|
337
556
|
}
|
|
557
|
+
if (want.has('transcript')) {
|
|
558
|
+
for (const msg of sessionMessages(row.node_id))
|
|
559
|
+
out.push(transcriptArtifact(row, desc, msg));
|
|
560
|
+
}
|
|
338
561
|
}
|
|
339
|
-
// Time window on artifact ts.
|
|
562
|
+
// Time window on artifact ts, then the message-attribute narrowing. An
|
|
563
|
+
// artifact that does not carry a dimension never survives that dimension's
|
|
564
|
+
// filter — asking for an origin excludes every generated turn by itself.
|
|
340
565
|
return out.filter((a) => {
|
|
341
566
|
if (corpus.sinceMs !== undefined && a.tsMs < corpus.sinceMs)
|
|
342
567
|
return false;
|
|
343
568
|
if (corpus.untilMs !== undefined && a.tsMs > corpus.untilMs)
|
|
344
569
|
return false;
|
|
345
|
-
return
|
|
570
|
+
return matchesMessageFilters(a, corpus);
|
|
346
571
|
});
|
|
347
572
|
}
|
|
573
|
+
/** The transcript/inbox dimensions: origin, role, sender. */
|
|
574
|
+
function matchesMessageFilters(a, corpus) {
|
|
575
|
+
const { origins, roles, from } = corpus;
|
|
576
|
+
if (origins !== undefined && origins.length > 0) {
|
|
577
|
+
if (a.origin === undefined || !origins.includes(a.origin))
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
if (roles !== undefined && roles.length > 0) {
|
|
581
|
+
if (a.role === undefined || !roles.includes(a.role))
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
if (from !== undefined && from !== '') {
|
|
585
|
+
if (a.from === undefined || !a.from.toLowerCase().includes(from.toLowerCase()))
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
/** Collapse a built corpus into counts by one dimension, descending by count.
|
|
591
|
+
* Artifacts that do not carry the grouped dimension are omitted rather than
|
|
592
|
+
* bucketed as empty — grouping by origin answers "what drove arrivals", and a
|
|
593
|
+
* generated turn is not an arrival. `unknown` is a real origin value here, not
|
|
594
|
+
* a missing one. */
|
|
595
|
+
export function groupCorpus(arts, by) {
|
|
596
|
+
const counts = new Map();
|
|
597
|
+
for (const a of arts) {
|
|
598
|
+
let key;
|
|
599
|
+
switch (by) {
|
|
600
|
+
case 'origin':
|
|
601
|
+
key = a.origin;
|
|
602
|
+
break;
|
|
603
|
+
case 'role':
|
|
604
|
+
key = a.role;
|
|
605
|
+
break;
|
|
606
|
+
case 'node':
|
|
607
|
+
key = `${a.nodeName} (${a.nodeId})`;
|
|
608
|
+
break;
|
|
609
|
+
case 'kind':
|
|
610
|
+
key = a.nodeKind;
|
|
611
|
+
break;
|
|
612
|
+
case 'type':
|
|
613
|
+
key = a.source;
|
|
614
|
+
break;
|
|
615
|
+
case 'day':
|
|
616
|
+
key = a.ts.slice(0, 10) || undefined;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
if (key === undefined || key === '')
|
|
620
|
+
continue;
|
|
621
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
622
|
+
}
|
|
623
|
+
return [...counts.entries()]
|
|
624
|
+
.map(([key, count]) => ({ key, count }))
|
|
625
|
+
.sort((x, y) => y.count - x.count || x.key.localeCompare(y.key));
|
|
626
|
+
}
|
|
348
627
|
/** Resolve the scope to the node rows in play. */
|
|
349
628
|
function scopedRows(scope) {
|
|
350
629
|
const all = listNodes();
|
|
@@ -401,6 +680,44 @@ export function resolveRef(ref) {
|
|
|
401
680
|
path: null,
|
|
402
681
|
};
|
|
403
682
|
}
|
|
683
|
+
// `session` addresses the whole conversation; `session/<index>` one message.
|
|
684
|
+
// `raw` is the verbatim record in both cases — the session file's bytes, or
|
|
685
|
+
// the one jsonl line that persisted that message.
|
|
686
|
+
if (relpath === 'session' || relpath.startsWith('session/')) {
|
|
687
|
+
const file = resolveSessionFile(nodeId);
|
|
688
|
+
if (file === null)
|
|
689
|
+
return null;
|
|
690
|
+
const messages = sessionMessages(nodeId);
|
|
691
|
+
if (relpath === 'session') {
|
|
692
|
+
const rendered = messages
|
|
693
|
+
.map((m) => `## ${m.role}${m.origin !== undefined ? ` (origin: ${m.origin})` : ''}\n\n${m.text}`)
|
|
694
|
+
.join('\n\n');
|
|
695
|
+
return {
|
|
696
|
+
nodeId,
|
|
697
|
+
relpath,
|
|
698
|
+
source: 'transcript',
|
|
699
|
+
ts: messages[messages.length - 1]?.ts ?? node.created,
|
|
700
|
+
nodeName: node.name,
|
|
701
|
+
body: rendered,
|
|
702
|
+
raw: readTextIfExists(file),
|
|
703
|
+
path: file,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const index = Number(relpath.slice('session/'.length));
|
|
707
|
+
const msg = messages[index];
|
|
708
|
+
if (msg === undefined)
|
|
709
|
+
return null;
|
|
710
|
+
return {
|
|
711
|
+
nodeId,
|
|
712
|
+
relpath,
|
|
713
|
+
source: 'transcript',
|
|
714
|
+
ts: msg.ts,
|
|
715
|
+
nodeName: node.name,
|
|
716
|
+
body: msg.text,
|
|
717
|
+
raw: rawSessionLine(file, index),
|
|
718
|
+
path: file,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
404
721
|
if (relpath.includes('..') || relpath.startsWith('/')) {
|
|
405
722
|
throw new Error(`illegal relpath in ref: ${relpath}`);
|
|
406
723
|
}
|
|
@@ -501,6 +818,10 @@ export function nodeArtifacts(nodeId, types) {
|
|
|
501
818
|
if (want.has('inbox')) {
|
|
502
819
|
allInboxEntries(nodeId).forEach((entry, i) => out.push(inboxArtifact(row, desc, entry, i)));
|
|
503
820
|
}
|
|
821
|
+
if (want.has('transcript')) {
|
|
822
|
+
for (const msg of sessionMessages(nodeId))
|
|
823
|
+
out.push(transcriptArtifact(row, desc, msg));
|
|
824
|
+
}
|
|
504
825
|
return out;
|
|
505
826
|
}
|
|
506
827
|
/** Bounded per-cwd aggregate for the branch `-h` `<corpus>` block. Counts files
|
|
@@ -3,8 +3,8 @@ import type { Mode, NodeMeta } from './types.js';
|
|
|
3
3
|
* as a single figure (nf-fa-user, U+F007); an orchestrator reads as a fan-out
|
|
4
4
|
* hierarchy (nf-fa-sitemap, U+F0E8). */
|
|
5
5
|
export declare function modeIcon(mode: Mode): string;
|
|
6
|
-
/** The minimal shape `fullName` needs — satisfied by NodeMeta
|
|
7
|
-
* else carrying name + kind (description optional). */
|
|
6
|
+
/** The minimal shape `fullName` needs — satisfied by NodeMeta, by NodeRow, and
|
|
7
|
+
* by anything else carrying name + kind (description optional). */
|
|
8
8
|
export interface NamedNode {
|
|
9
9
|
name: string;
|
|
10
10
|
kind: string;
|
|
@@ -1329,6 +1329,37 @@ END
|
|
|
1329
1329
|
WHERE status IN ('done','canceled','dead') AND terminal_reason IS NULL;
|
|
1330
1330
|
`);
|
|
1331
1331
|
}
|
|
1332
|
+
/** v38 — mirror the two display-identity fields (`description`, `cycles`) that
|
|
1333
|
+
* lived ONLY in meta.json onto the row, completing the identity projection the
|
|
1334
|
+
* row index already is. Before this, every roster read that wanted a node's full
|
|
1335
|
+
* label had to open and parse each node's meta.json (a per-node disk read on a
|
|
1336
|
+
* path that already had all the other columns in one query). The backfill reads
|
|
1337
|
+
* each meta once; from here `upsertRow`/`seedRow` maintain them like any other
|
|
1338
|
+
* identity column, and `rebuildIndex` re-derives them. */
|
|
1339
|
+
function addNodeLabelColumns(db) {
|
|
1340
|
+
const columns = nodeColumns(db);
|
|
1341
|
+
addNodeColumnIfMissing(db, columns, 'description', 'ALTER TABLE nodes ADD COLUMN description TEXT;');
|
|
1342
|
+
addNodeColumnIfMissing(db, columns, 'cycles', 'ALTER TABLE nodes ADD COLUMN cycles INTEGER;');
|
|
1343
|
+
const root = nodesRoot();
|
|
1344
|
+
if (!existsSync(root))
|
|
1345
|
+
return;
|
|
1346
|
+
const upd = db.prepare('UPDATE nodes SET description = ?, cycles = ? WHERE node_id = ?');
|
|
1347
|
+
for (const id of readdirSync(root)) {
|
|
1348
|
+
const p = nodeMetaPath(id);
|
|
1349
|
+
if (!existsSync(p))
|
|
1350
|
+
continue;
|
|
1351
|
+
let meta;
|
|
1352
|
+
try {
|
|
1353
|
+
meta = JSON.parse(readFileSync(p, 'utf8'));
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
continue; // a single unreadable meta never aborts the migration
|
|
1357
|
+
}
|
|
1358
|
+
const description = typeof meta['description'] === 'string' ? meta['description'] : null;
|
|
1359
|
+
const cycles = typeof meta['cycles'] === 'number' ? meta['cycles'] : null;
|
|
1360
|
+
upd.run(description, cycles, id);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1332
1363
|
/** The journaled lane of the state-migration registry, adapted to this chain's
|
|
1333
1364
|
* step shape. Registry order is preserved, so these versions stay stable as
|
|
1334
1365
|
* long as the registry is append-only (same contract as the local list). */
|
|
@@ -1379,6 +1410,7 @@ export const MIGRATIONS = [
|
|
|
1379
1410
|
/* v35 */ addNodeLifecycleColumns,
|
|
1380
1411
|
/* v36 */ makeNodeSessionFileUnique,
|
|
1381
1412
|
/* v37 */ repairNodeSessionFileUniqueIndex,
|
|
1413
|
+
/* v38 */ addNodeLabelColumns,
|
|
1382
1414
|
...JOURNALED_STATE_MIGRATIONS,
|
|
1383
1415
|
];
|
|
1384
1416
|
/** Migration indexes that manage their OWN transaction and therefore must not
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface NodeRecap {
|
|
2
|
+
/** What this conversation is for — the recap's `Goal:` line. */
|
|
3
|
+
goal: string;
|
|
4
|
+
/** Where it actually stands — the recap's `State:` line. */
|
|
5
|
+
state: string;
|
|
6
|
+
/** ISO 8601 stamp of when the briefing was generated. */
|
|
7
|
+
at: string;
|
|
8
|
+
}
|
|
9
|
+
/** Split the model's two-line briefing into its labelled halves. Returns null for
|
|
10
|
+
* anything that doesn't yield at least one non-empty line — a garbled recap is
|
|
11
|
+
* worse than none, so it is never stored. Missing labels fall back to line order
|
|
12
|
+
* (the prompt asks for Goal first, State second). */
|
|
13
|
+
export declare function parseRecapBriefing(briefing: string): {
|
|
14
|
+
goal: string;
|
|
15
|
+
state: string;
|
|
16
|
+
} | null;
|
|
17
|
+
/** Persist a freshly generated briefing. Best-effort — a write failure only costs
|
|
18
|
+
* the browse preview one stale panel, so it must never disturb the broker. */
|
|
19
|
+
export declare function writeNodeRecap(nodeId: string, briefing: string): void;
|
|
20
|
+
/** The node's stored briefing, or undefined when it never generated one (a node
|
|
21
|
+
* that finished before its first idle window, or one last run before this file
|
|
22
|
+
* existed). Never throws. */
|
|
23
|
+
export declare function readNodeRecap(nodeId: string): NodeRecap | undefined;
|