@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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// node-recap.ts — the node's last Goal/State briefing, persisted.
|
|
2
|
+
//
|
|
3
|
+
// The recap itself is generated in the broker (`pi-extensions/canvas-recap.ts`)
|
|
4
|
+
// and painted above the editor as throwaway chrome. That widget lives in broker
|
|
5
|
+
// memory, so the moment a node goes dormant its briefing is gone — which is
|
|
6
|
+
// exactly when a developer scanning `crtr canvas browse` needs it most ("what was
|
|
7
|
+
// this one for, and where did it stop?"). Persisting the two lines the viewer
|
|
8
|
+
// already produced makes them readable without a broker and without a model call.
|
|
9
|
+
//
|
|
10
|
+
// Stored beside telemetry in `job/` because it is machine-generated runtime
|
|
11
|
+
// state, not a document the node authored: safe to delete, rebuilt on the node's
|
|
12
|
+
// next idle recap. `context/` stays the agent's own document space.
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { mkdirSync } from 'node:fs';
|
|
15
|
+
import { jobDir } from './paths.js';
|
|
16
|
+
import { readJsonIfExists, atomicWriteJson } from '../fs-utils.js';
|
|
17
|
+
function recapPath(nodeId) {
|
|
18
|
+
return join(jobDir(nodeId), 'recap.json');
|
|
19
|
+
}
|
|
20
|
+
/** Strip one leading `Goal:` / `State:` label from a briefing line. */
|
|
21
|
+
function stripLabel(line) {
|
|
22
|
+
return line.replace(/^\s*(goal|state)\s*:\s*/i, '').trim();
|
|
23
|
+
}
|
|
24
|
+
/** Split the model's two-line briefing into its labelled halves. Returns null for
|
|
25
|
+
* anything that doesn't yield at least one non-empty line — a garbled recap is
|
|
26
|
+
* worse than none, so it is never stored. Missing labels fall back to line order
|
|
27
|
+
* (the prompt asks for Goal first, State second). */
|
|
28
|
+
export function parseRecapBriefing(briefing) {
|
|
29
|
+
const lines = briefing.split('\n').map((l) => l.trim()).filter((l) => l !== '');
|
|
30
|
+
if (lines.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
const labelled = (prefix) => lines.find((l) => new RegExp(`^${prefix}\\s*:`, 'i').test(l));
|
|
33
|
+
const goalLine = labelled('goal') ?? lines[0];
|
|
34
|
+
const stateLine = labelled('state') ?? (lines[1] !== goalLine ? lines[1] : undefined);
|
|
35
|
+
const goal = goalLine === undefined ? '' : stripLabel(goalLine);
|
|
36
|
+
const state = stateLine === undefined ? '' : stripLabel(stateLine);
|
|
37
|
+
if (goal === '' && state === '')
|
|
38
|
+
return null;
|
|
39
|
+
return { goal, state };
|
|
40
|
+
}
|
|
41
|
+
/** Persist a freshly generated briefing. Best-effort — a write failure only costs
|
|
42
|
+
* the browse preview one stale panel, so it must never disturb the broker. */
|
|
43
|
+
export function writeNodeRecap(nodeId, briefing) {
|
|
44
|
+
const parsed = parseRecapBriefing(briefing);
|
|
45
|
+
if (parsed === null)
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(jobDir(nodeId), { recursive: true });
|
|
49
|
+
atomicWriteJson(recapPath(nodeId), { ...parsed, at: new Date().toISOString() });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* best-effort: the recap is regenerated on the node's next idle window */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The node's stored briefing, or undefined when it never generated one (a node
|
|
56
|
+
* that finished before its first idle window, or one last run before this file
|
|
57
|
+
* existed). Never throws. */
|
|
58
|
+
export function readNodeRecap(nodeId) {
|
|
59
|
+
const raw = readJsonIfExists(recapPath(nodeId));
|
|
60
|
+
if (raw === null || typeof raw !== 'object')
|
|
61
|
+
return undefined;
|
|
62
|
+
const goal = typeof raw.goal === 'string' ? raw.goal : '';
|
|
63
|
+
const state = typeof raw.state === 'string' ? raw.state : '';
|
|
64
|
+
if (goal === '' && state === '')
|
|
65
|
+
return undefined;
|
|
66
|
+
return { goal, state, at: typeof raw.at === 'string' ? raw.at : '' };
|
|
67
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CanvasSource } from './source.js';
|
|
2
|
+
import { type NodeRecap } from './node-recap.js';
|
|
2
3
|
import { type Fault } from '../runtime/fault.js';
|
|
3
4
|
import type { NodeStatus, Lifecycle, NodeMeta, NodeRow, TerminalReason } from './types.js';
|
|
4
5
|
export declare function activeFaultForDisplay<T extends {
|
|
@@ -69,6 +70,15 @@ export interface DashboardRow {
|
|
|
69
70
|
* by loadPreview (same single session read as `prompts`); undefined for a node whose
|
|
70
71
|
* session has no assistant reply yet. Shown in the preview's reply block. */
|
|
71
72
|
lastAssistant?: string;
|
|
73
|
+
/** The node's last persisted Goal/State briefing (`job/recap.json`, written by the
|
|
74
|
+
* broker's recap extension). Populated LAZILY by loadPreview; undefined for a node
|
|
75
|
+
* that never generated one. The preview's fastest "what is this / where is it" line. */
|
|
76
|
+
recap?: NodeRecap;
|
|
77
|
+
/** The newest report the node pushed (`reports/<ts>-<id>-<kind>.md`). Populated
|
|
78
|
+
* LAZILY by loadPreview; undefined when the node has pushed nothing. Residents push
|
|
79
|
+
* updates as they work, so this is the last thing the node deliberately DELIVERED —
|
|
80
|
+
* usually crisper than its last assistant message. */
|
|
81
|
+
report?: NodeReportPreview;
|
|
72
82
|
/** True when the node is GENUINELY mid-turn right now — its `busy` marker exists
|
|
73
83
|
* AND its broker pid is alive (not merely an `active`, between-turns node). The
|
|
74
84
|
* live "is it generating?" cue. Computed on the cheap boot path for LIVE nodes
|
|
@@ -93,8 +103,30 @@ export interface DashboardRow {
|
|
|
93
103
|
/** Lazy-enrichment guard (internal): true once loadPreview has read goal + the
|
|
94
104
|
* session (prompts + lastAssistant). Idempotency marker for the selected row. */
|
|
95
105
|
previewLoaded?: boolean;
|
|
106
|
+
/** The source `NodeRow` this row was built from (dashboardRowFromNodeRow only).
|
|
107
|
+
* Carries every column listNodes already fetched — the runtime fields
|
|
108
|
+
* (host_kind/parent/waiting_for/final_report/finalized_at/pi_pid) plus the
|
|
109
|
+
* mirrored identity fields (description/cycles) — so the snapshot handler
|
|
110
|
+
* reads them here instead of opening meta.json.
|
|
111
|
+
* Undefined for rows built by other paths (dashboardRowsFromSource). */
|
|
112
|
+
srcRow?: NodeRow;
|
|
96
113
|
}
|
|
97
114
|
export declare function readGoalText(nodeId: string): string | undefined;
|
|
115
|
+
/** The newest pushed report, reduced to what a preview panel can use. */
|
|
116
|
+
export interface NodeReportPreview {
|
|
117
|
+
/** final | update | urgent (frontmatter, else the filename suffix). */
|
|
118
|
+
kind: string;
|
|
119
|
+
/** ISO 8601 push time (frontmatter, else the filename stamp). */
|
|
120
|
+
ts: string;
|
|
121
|
+
/** The report's first markdown heading, or its first non-empty line. */
|
|
122
|
+
heading: string;
|
|
123
|
+
/** The body after that heading, whitespace-normalized + capped. */
|
|
124
|
+
body: string;
|
|
125
|
+
}
|
|
126
|
+
/** The node's newest report, read straight off disk. Report filenames lead with a
|
|
127
|
+
* compact timestamp, so the lexicographic maximum is the latest one — no stat of
|
|
128
|
+
* the whole directory. Never throws; undefined when the node pushed nothing. */
|
|
129
|
+
export declare function readNewestReport(nodeId: string): NodeReportPreview | undefined;
|
|
98
130
|
interface SessionParts {
|
|
99
131
|
prompts?: string;
|
|
100
132
|
lastAssistant?: string;
|
|
@@ -138,7 +170,7 @@ export declare function dashboardRowsFromSource(source: CanvasSource, rootId: st
|
|
|
138
170
|
* coupling the canvas source to local viewer state. Remote sources ignore that
|
|
139
171
|
* set. Local disk telemetry/fault/streaming are likewise suppressed remotely. */
|
|
140
172
|
export declare function dashboardRowsAllFromSource(source: CanvasSource, localFocusedNodeIds?: ReadonlySet<string>, liveOnly?: boolean): Promise<DashboardRow[]>;
|
|
141
|
-
export declare function enrichRowsFromSource(
|
|
173
|
+
export declare function enrichRowsFromSource(_source: CanvasSource, rows: DashboardRow[], asks?: Record<string, number>): Promise<void>;
|
|
142
174
|
/** goal (initial-prompt.md) and session parts are local disk reads — suppressed
|
|
143
175
|
* to undefined for a remote source. */
|
|
144
176
|
export declare function loadPreviewFromSource(source: CanvasSource, row: DashboardRow): Promise<DashboardRow>;
|
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
// migrated onto these `*FromSource` variants directly.
|
|
10
10
|
//
|
|
11
11
|
// ALLOWED imports (verified openDb-free): node:fs/path, ./pid.js, ./labels.js,
|
|
12
|
-
// ./status-glyph.js, ../runtime/fault.js,
|
|
13
|
-
// and TYPE-ONLY ./source.js + ./types.js. Do NOT import ./canvas.js, ./attention.js,
|
|
12
|
+
// ./status-glyph.js, ./node-recap.js, ../runtime/fault.js, ../frontmatter.js,
|
|
13
|
+
// ./paths.js, ./remote-canvas-source.js, and TYPE-ONLY ./source.js + ./types.js. Do NOT import ./canvas.js, ./attention.js,
|
|
14
14
|
// ./focuses.js, or ./db.js here — that is the whole point of the split.
|
|
15
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { RemoteCanvasSource } from './remote-canvas-source.js';
|
|
18
18
|
import { fullName } from './labels.js';
|
|
19
|
-
import { jobDir, contextDir } from './paths.js';
|
|
19
|
+
import { jobDir, contextDir, reportsDir } from './paths.js';
|
|
20
|
+
import { readNodeRecap } from './node-recap.js';
|
|
21
|
+
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
20
22
|
import { isPidAlive } from './pid.js';
|
|
21
23
|
import { isFrozenRow, resolveNodeVisual, faultSummary } from './status-glyph.js';
|
|
22
24
|
import { readFault } from '../runtime/fault.js';
|
|
@@ -168,6 +170,63 @@ export function readGoalText(nodeId) {
|
|
|
168
170
|
return undefined;
|
|
169
171
|
}
|
|
170
172
|
}
|
|
173
|
+
const REPORT_BODY_CAP = 1024;
|
|
174
|
+
/** Compact report stamp `20260810T190533286` → an ISO 8601 instant. */
|
|
175
|
+
function reportStampToIso(stamp) {
|
|
176
|
+
const m = stamp.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})?$/);
|
|
177
|
+
if (m === null)
|
|
178
|
+
return undefined;
|
|
179
|
+
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}.${m[7] ?? '000'}Z`;
|
|
180
|
+
}
|
|
181
|
+
/** The node's newest report, read straight off disk. Report filenames lead with a
|
|
182
|
+
* compact timestamp, so the lexicographic maximum is the latest one — no stat of
|
|
183
|
+
* the whole directory. Never throws; undefined when the node pushed nothing. */
|
|
184
|
+
export function readNewestReport(nodeId) {
|
|
185
|
+
let newest;
|
|
186
|
+
try {
|
|
187
|
+
for (const f of readdirSync(reportsDir(nodeId))) {
|
|
188
|
+
if (!f.endsWith('.md'))
|
|
189
|
+
continue;
|
|
190
|
+
if (newest === undefined || f > newest)
|
|
191
|
+
newest = f;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return undefined; // no reports dir
|
|
196
|
+
}
|
|
197
|
+
if (newest === undefined)
|
|
198
|
+
return undefined;
|
|
199
|
+
let raw;
|
|
200
|
+
try {
|
|
201
|
+
raw = readFileSync(join(reportsDir(nodeId), newest), 'utf8');
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
const { data, body } = parseFrontmatterGeneric(raw);
|
|
207
|
+
const name = newest.replace(/\.md$/, '');
|
|
208
|
+
const kind = typeof data?.['kind'] === 'string'
|
|
209
|
+
? data['kind']
|
|
210
|
+
: (name.split('-').pop() ?? 'report');
|
|
211
|
+
const ts = typeof data?.['ts'] === 'string'
|
|
212
|
+
? data['ts']
|
|
213
|
+
: (reportStampToIso(name.split('-')[0] ?? '') ?? '');
|
|
214
|
+
// The heading is the report's own headline; everything after it is the detail the
|
|
215
|
+
// preview shows next, so a panel never repeats the heading twice.
|
|
216
|
+
const lines = body.split('\n');
|
|
217
|
+
let heading = '';
|
|
218
|
+
let rest = body;
|
|
219
|
+
for (let i = 0; i < lines.length; i++) {
|
|
220
|
+
const t = (lines[i] ?? '').trim();
|
|
221
|
+
if (t === '')
|
|
222
|
+
continue;
|
|
223
|
+
heading = t.startsWith('#') ? t.replace(/^#+\s*/, '').trim() : t;
|
|
224
|
+
rest = lines.slice(i + 1).join('\n');
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
const text = rest.replace(/\s+/g, ' ').trim();
|
|
228
|
+
return { kind, ts, heading, body: text.length > REPORT_BODY_CAP ? text.slice(0, REPORT_BODY_CAP) : text };
|
|
229
|
+
}
|
|
171
230
|
/** The node's session-derived preview text in ONE file read (today's two reads,
|
|
172
231
|
* folded): EVERY user prompt across the pi session (`prompts` — the whole
|
|
173
232
|
* conversation, not just the spawn prompt) AND the LAST assistant message text
|
|
@@ -385,8 +444,9 @@ export function dashboardRowFromNodeRow(row, focusedNodeIds, remote) {
|
|
|
385
444
|
const hanging = remote ? null : activeFaultForDisplay(row, readFault(row.node_id));
|
|
386
445
|
return {
|
|
387
446
|
node_id: row.node_id,
|
|
388
|
-
name: row
|
|
447
|
+
name: fullName(row), // description is a row column (v38) — no meta read needed
|
|
389
448
|
handle: row.name,
|
|
449
|
+
description: row.description,
|
|
390
450
|
status: row.status,
|
|
391
451
|
frozen_at: row.frozen_at ?? null,
|
|
392
452
|
terminal_reason: row.terminal_reason ?? null,
|
|
@@ -403,6 +463,7 @@ export function dashboardRowFromNodeRow(row, focusedNodeIds, remote) {
|
|
|
403
463
|
streaming: !remote && hanging === null && live ? isStreaming(row.node_id, row.pi_pid) : false,
|
|
404
464
|
hanging,
|
|
405
465
|
viewed: remote ? false : focusedNodeIds.has(row.node_id) || hasLiveBrokerViewer(row),
|
|
466
|
+
srcRow: row,
|
|
406
467
|
};
|
|
407
468
|
}
|
|
408
469
|
/** One row per node visible in the sub-DAG of `rootId` (including root), read
|
|
@@ -449,28 +510,23 @@ export async function dashboardRowsAllFromSource(source, localFocusedNodeIds = n
|
|
|
449
510
|
const rows = await source.listNodes(liveOnly ? { status: ['active', 'idle'] } : undefined);
|
|
450
511
|
return rows.map((row) => dashboardRowFromNodeRow(row, focusedNodeIds, remote));
|
|
451
512
|
}
|
|
452
|
-
// Lazy enrichment (source-driven) — fold
|
|
453
|
-
// demand. Ask counts are supplied by the caller (`asks` — a
|
|
454
|
-
// `ticketCountsForView` map merged across the visible roots), NOT
|
|
455
|
-
// (`ticketCountsForNodes` reaches openDb). Telemetry (ctx tokens)
|
|
456
|
-
//
|
|
457
|
-
|
|
513
|
+
// Lazy enrichment (source-driven) — fold the per-node reads the cheap boot skips
|
|
514
|
+
// into rows on demand. Ask counts are supplied by the caller (`asks` — a
|
|
515
|
+
// pre-fetched `ticketCountsForView` map merged across the visible roots), NOT
|
|
516
|
+
// scanned here (`ticketCountsForNodes` reaches openDb). Telemetry (ctx tokens) is
|
|
517
|
+
// a LOCAL disk read, suppressed for a `RemoteCanvasSource`.
|
|
518
|
+
//
|
|
519
|
+
// This does NO source read. The label fields it used to re-fetch per node
|
|
520
|
+
// (name/handle/description) are set by the row builders: `description` became a
|
|
521
|
+
// nodes-table column in v38, so both builders already hold the full label and a
|
|
522
|
+
// per-node getNode here would re-read what the caller's single listNodes returned.
|
|
523
|
+
export async function enrichRowsFromSource(_source, rows, asks) {
|
|
458
524
|
const todo = rows.filter((r) => r.enriched !== true);
|
|
459
525
|
if (todo.length === 0)
|
|
460
526
|
return;
|
|
461
|
-
const remote =
|
|
527
|
+
const remote = _source instanceof RemoteCanvasSource;
|
|
462
528
|
const askMap = asks ?? {};
|
|
463
|
-
// A full dashboard may contain thousands of rows. Keep source reads serial:
|
|
464
|
-
// ApiCanvasSource turns each into a unix-socket request, and an unbounded
|
|
465
|
-
// Promise.all can overflow crtrd's accept backlog then spuriously trigger
|
|
466
|
-
// the client's cold-daemon path even though the daemon is still running.
|
|
467
529
|
for (const row of todo) {
|
|
468
|
-
const meta = await source.getNode(row.node_id);
|
|
469
|
-
if (meta !== null) {
|
|
470
|
-
row.name = fullName(meta);
|
|
471
|
-
row.handle = meta.name;
|
|
472
|
-
row.description = meta.description;
|
|
473
|
-
}
|
|
474
530
|
row.ctx_tokens = remote ? 0 : (readNodeTelemetry(row.node_id).tokens_in ?? 0);
|
|
475
531
|
row.asks = askMap[row.node_id] ?? 0;
|
|
476
532
|
row.enriched = true;
|
|
@@ -488,6 +544,8 @@ export async function loadPreviewFromSource(source, row) {
|
|
|
488
544
|
const { prompts, lastAssistant } = readSessionParts(meta?.pi_session_file);
|
|
489
545
|
row.prompts = prompts;
|
|
490
546
|
row.lastAssistant = lastAssistant;
|
|
547
|
+
row.recap = readNodeRecap(row.node_id);
|
|
548
|
+
row.report = readNewestReport(row.node_id);
|
|
491
549
|
}
|
|
492
550
|
row.previewLoaded = true;
|
|
493
551
|
return row;
|
|
@@ -292,6 +292,15 @@ export type NodeMeta = NodeIdentity & NodeRuntime;
|
|
|
292
292
|
export interface NodeRow {
|
|
293
293
|
node_id: string;
|
|
294
294
|
name: string;
|
|
295
|
+
/** The pi-generated first-task description; see NodeIdentity.description.
|
|
296
|
+
* Durable IDENTITY mirrored to the row (like `cwd`/`host_kind`) so a roster
|
|
297
|
+
* read can build the full display label without opening meta.json. Absent for
|
|
298
|
+
* a node not yet named — the column's NULL is mapped to undefined at `rowFrom`
|
|
299
|
+
* so identity, row, and DTO all spell "unnamed" the same way. */
|
|
300
|
+
description?: string;
|
|
301
|
+
/** Relaunch count; see NodeIdentity.cycles. Mirrored to the row for the same
|
|
302
|
+
* reason as `description`; absent on a row written before this column. */
|
|
303
|
+
cycles?: number;
|
|
295
304
|
kind: string;
|
|
296
305
|
mode: Mode;
|
|
297
306
|
lifecycle: Lifecycle;
|
package/dist/core/config.js
CHANGED
|
@@ -114,10 +114,11 @@ export function ensureScopeInitialized(scope, root) {
|
|
|
114
114
|
// a missing `modelLadders` from whatever build is CURRENTLY running, and an explicit
|
|
115
115
|
// user override (`sys config set modelLadders...`) still persists and still wins.
|
|
116
116
|
// Keybindings are sparse user overrides, so an untouched scope omits that member too.
|
|
117
|
-
const { modelLadders: _omittedModelLadders, keybindings: _omittedKeybindings, tmux_passthrough: _omittedTmuxPassthrough, brokerThresholds, ...rest } = defaultScopeConfig();
|
|
118
|
-
// Broker thresholds are host-wide daemon policy, so
|
|
119
|
-
// persists them. Project config must never acquire a
|
|
120
|
-
|
|
117
|
+
const { modelLadders: _omittedModelLadders, keybindings: _omittedKeybindings, tmux_passthrough: _omittedTmuxPassthrough, brokerThresholds, lifecycle, ...rest } = defaultScopeConfig();
|
|
118
|
+
// Broker thresholds and lifecycle timing are host-wide daemon policy, so
|
|
119
|
+
// only user scope persists them. Project config must never acquire a
|
|
120
|
+
// machine setting.
|
|
121
|
+
atomicWriteJson(cfgPath, scope === 'user' ? { ...rest, brokerThresholds, lifecycle } : rest);
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
124
|
function normalizeKeybindings(raw) {
|
|
@@ -701,6 +702,12 @@ function mergeBrokerThresholds(raw, defaults) {
|
|
|
701
702
|
? { warning, automaticReviveCap }
|
|
702
703
|
: { ...defaults };
|
|
703
704
|
}
|
|
705
|
+
function mergeLifecycle(raw, defaults) {
|
|
706
|
+
const partial = raw !== null && typeof raw === 'object'
|
|
707
|
+
? raw
|
|
708
|
+
: {};
|
|
709
|
+
return { unattendedParkMs: normalizePositiveInteger(partial.unattendedParkMs, defaults.unattendedParkMs) };
|
|
710
|
+
}
|
|
704
711
|
function normalizeWhipMessages(raw) {
|
|
705
712
|
if (!Array.isArray(raw))
|
|
706
713
|
return [...DEFAULT_WHIP_MESSAGES];
|
|
@@ -728,6 +735,7 @@ function mergeConfig(partial) {
|
|
|
728
735
|
? partial.auto_open_child_viewers
|
|
729
736
|
: defaults.auto_open_child_viewers;
|
|
730
737
|
const brokerThresholds = mergeBrokerThresholds(partial.brokerThresholds, defaults.brokerThresholds);
|
|
738
|
+
const lifecycle = mergeLifecycle(partial.lifecycle, defaults.lifecycle);
|
|
731
739
|
const completion_bell = typeof partial.completion_bell === 'boolean' ? partial.completion_bell : defaults.completion_bell;
|
|
732
740
|
const working_gerunds = partial.working_gerunds === undefined
|
|
733
741
|
? [...defaults.working_gerunds]
|
|
@@ -761,7 +769,7 @@ function mergeConfig(partial) {
|
|
|
761
769
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
762
770
|
const bin = normalizeBin(partial.bin);
|
|
763
771
|
const humanActions = normalizeHumanActions(partial.humanActions);
|
|
764
|
-
return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
772
|
+
return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, lifecycle, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(Object.keys(routes.legacy).length > 0 ? { legacyModelRoutes: routes.legacy } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
765
773
|
}
|
|
766
774
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
767
775
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -3,13 +3,13 @@ import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
|
|
|
3
3
|
type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'context-admin' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
|
|
4
4
|
type TmuxMenuEntryAction = Exclude<TmuxMenuAction, 'open' | 'attach'> | `issue.${'idea' | 'bug'}`;
|
|
5
5
|
type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
|
|
6
|
-
type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
|
|
6
|
+
type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'pin' | 'search' | 'revive-now' | 'focus' | 'focus-background' | 'yank-id';
|
|
7
7
|
type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' | `slot.${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | 'create' | 'search-existing' | 'use-root' | 'set-default' | 'add-directory' | 'unpin' | 'manage';
|
|
8
8
|
type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
|
|
9
9
|
type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
|
|
10
10
|
type SetupModelsAction = 'open' | 'commit' | 'cancel' | 'scope-next';
|
|
11
11
|
export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | AttachMenuBindingId | `crtr.tmux.menu-global.${TmuxMenuEntryAction}` | `crtr.tmux.node.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.scroll.${'up' | 'down' | 'page-up' | 'page-down' | 'live'}` | 'crtr.attach.mouse.toggle' | 'crtr.attach.copy.message' | 'crtr.attach.help.toggle' | `crtr.attach.search.${'next' | 'previous' | 'edit' | 'close'}` | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.inspector.${'force-quit' | 'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
|
|
12
|
-
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.attach.search.next"> | BindingDefinition<"crtr.attach.search.previous"> | BindingDefinition<"crtr.attach.search.edit"> | BindingDefinition<"crtr.attach.search.close"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.menu.manage">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
|
|
12
|
+
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.attach.search.next"> | BindingDefinition<"crtr.attach.search.previous"> | BindingDefinition<"crtr.attach.search.edit"> | BindingDefinition<"crtr.attach.search.close"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.pin"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.menu.manage">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
|
|
13
13
|
export declare const BINDING_IDS: ReadonlySet<string>;
|
|
14
14
|
/** True for an action whose direct gesture is handled inside an attach viewer.
|
|
15
15
|
* Tmux menu selectors and root shortcuts are chrome, not pane input. */
|
|
@@ -147,7 +147,8 @@ function browseBindings() {
|
|
|
147
147
|
b('crtr.browse.scope', 'Browse and pickers', 'Change scope', 'Toggle the browse cwd scope.', ['c'], 'terminal', [nav]),
|
|
148
148
|
b('crtr.browse.profile', 'Browse and pickers', 'Toggle profile', 'Toggle the browse profile scope.', ['shift+p'], 'terminal', [nav]),
|
|
149
149
|
b('crtr.browse.residents', 'Browse and pickers', 'Toggle residents', 'Toggle resident display.', ['r'], 'terminal', [nav]),
|
|
150
|
-
b('crtr.browse.preview', 'Browse and pickers', 'Toggle preview', 'Toggle the preview panel.', ['
|
|
150
|
+
b('crtr.browse.preview', 'Browse and pickers', 'Toggle preview', 'Toggle the preview panel.', ['v'], 'terminal', [nav]),
|
|
151
|
+
b('crtr.browse.pin', 'Browse and pickers', 'Pin node', 'Star or unstar the selected node.', ['p'], 'terminal', [nav]),
|
|
151
152
|
b('crtr.browse.search', 'Browse and pickers', 'Search', 'Open browse search.', ['/'], 'terminal', [nav]),
|
|
152
153
|
b('crtr.browse.revive-now', 'Browse and pickers', 'Revive now', 'Revive the selected node now.', ['shift+k'], 'terminal', [nav]),
|
|
153
154
|
b('crtr.browse.focus', 'Browse and pickers', 'Focus node', 'Focus the selected node.', ['enter', 'o'], 'terminal', [nav]),
|
|
@@ -189,7 +189,7 @@ export function modelRequestFromConfig(model, modelIntent, exact) {
|
|
|
189
189
|
if (exact)
|
|
190
190
|
return { kind: 'exact', spec: model };
|
|
191
191
|
if (modelIntent) {
|
|
192
|
-
const slash = modelIntent.
|
|
192
|
+
const slash = modelIntent.lastIndexOf('/');
|
|
193
193
|
const strength = slash > 0 ? modelIntent.slice(slash + 1) : '';
|
|
194
194
|
if (STRENGTHS.includes(strength))
|
|
195
195
|
return { kind: 'logical', family: modelIntent.slice(0, slash), strength: strength, spec: model };
|
|
@@ -3,6 +3,7 @@ import { envModelIntent } from '../../../shared/env.js';
|
|
|
3
3
|
import { emitEvent } from '../../events/emit.js';
|
|
4
4
|
import { clearFault, readFault } from '../fault.js';
|
|
5
5
|
import { isUnknownModel } from '../model-selection.js';
|
|
6
|
+
import { announceMessageOrigin } from '../stamp/channel.js';
|
|
6
7
|
export function createAuthReloadGate(deps) {
|
|
7
8
|
const { nodeId, cfg, replies, rebind, session, services, broadcastModelChanged, formatModelSpec, resolveLaunchModel, driveKickoff, turnAdmission, } = deps;
|
|
8
9
|
let reloadInFlight = null;
|
|
@@ -118,7 +119,9 @@ export function createAuthReloadGate(deps) {
|
|
|
118
119
|
return Promise.resolve();
|
|
119
120
|
}
|
|
120
121
|
clearFault(nodeId, { link: 'pi→provider' });
|
|
121
|
-
|
|
122
|
+
const card = formatCard('recovery', {}, AUTH_FAULT_RECOVERY_BODY);
|
|
123
|
+
announceMessageOrigin(card, { type: 'runtime', kind: 'recovery' });
|
|
124
|
+
return faultSession.prompt(card, { preflightResult: release });
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
127
|
catch (err) {
|
|
@@ -12,6 +12,7 @@ import { markFatalOnExhaust, nextFaultRetry, policyFor } from '../fault-recovery
|
|
|
12
12
|
import { parseModelSpec } from '../model-selection.js';
|
|
13
13
|
import { endedByAbort } from '../session-cycles.js';
|
|
14
14
|
import { resolveProviderCandidates } from '../../subscription-state.js';
|
|
15
|
+
import { announceMessageOrigin } from '../stamp/channel.js';
|
|
15
16
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
16
17
|
/** Owns provider fallback and daemon-driven fault retry state for one broker session. */
|
|
17
18
|
export class FaultRetry {
|
|
@@ -266,7 +267,9 @@ export class FaultRetry {
|
|
|
266
267
|
return;
|
|
267
268
|
if (unconfigured)
|
|
268
269
|
clearFault(this.deps.nodeId, { link: 'pi→provider' });
|
|
269
|
-
|
|
270
|
+
const fallbackCard = formatCard('recovery', {}, formatModelFallbackRecovery(currentSpec, resolved, unconfigured ? 'credential' : 'not-found'));
|
|
271
|
+
announceMessageOrigin(fallbackCard, { type: 'runtime', kind: 'recovery' });
|
|
272
|
+
await this.deps.turnAdmission.admit((release) => operationIdContext.fresh(() => candidateSession.prompt(fallbackCard, { preflightResult: release })));
|
|
270
273
|
})().catch((err) => {
|
|
271
274
|
emitEvent({ level: 'debug', event: 'broker.runtime.diagnostic', fields: { message: `${unconfigured ? 'unconfigured-provider' : 'not_found'} fallback failed: ${err instanceof Error ? err.message : String(err)}` } });
|
|
272
275
|
}).finally(() => {
|
|
@@ -405,7 +408,9 @@ export class FaultRetry {
|
|
|
405
408
|
return Promise.resolve();
|
|
406
409
|
}
|
|
407
410
|
recordFault(this.deps.nodeId, plan.fault);
|
|
408
|
-
|
|
411
|
+
const retryCard = formatCard('recovery', {}, fault.kind === 'connection' ? CONNECTION_FAULT_RECOVERY_BODY : PROVIDER_FAULT_RECOVERY_BODY);
|
|
412
|
+
announceMessageOrigin(retryCard, { type: 'runtime', kind: 'recovery' });
|
|
413
|
+
return operationIdContext.fresh(() => candidateSession.prompt(retryCard, { preflightResult: release }));
|
|
409
414
|
});
|
|
410
415
|
}
|
|
411
416
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SITUATIONAL_CONTEXT_CUSTOM_TYPE } from '../../../shared/generated-context.js';
|
|
2
2
|
import { emitEvent } from '../../events/emit.js';
|
|
3
3
|
import { normalizeSnapshotTail } from '../broker-protocol.js';
|
|
4
|
+
import { announceMessageOrigin } from '../stamp/channel.js';
|
|
4
5
|
export function createClientHandlers(deps) {
|
|
5
6
|
const { registry, replies, session, driveEngine, turnAcceptance, pendingDialogs, sendWelcome, replayExtraPendingDialogsTo, disposeAndExit, } = deps;
|
|
6
7
|
const { notWritable, ackTo } = replies;
|
|
@@ -82,6 +83,9 @@ export function createClientHandlers(deps) {
|
|
|
82
83
|
};
|
|
83
84
|
if (via === 'prompt')
|
|
84
85
|
turnAcceptance.waitFor(ackAccepted);
|
|
86
|
+
// The daemon knows who authored this body; the stamp writer lives here.
|
|
87
|
+
if (frame.origin !== undefined)
|
|
88
|
+
announceMessageOrigin(frame.text, frame.origin);
|
|
85
89
|
const outcome = driveEngine(client, {
|
|
86
90
|
type: 'prompt',
|
|
87
91
|
text: frame.text,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createFrameReplies } from './frame-replies.js';
|
|
2
2
|
import { createEngineDriver } from './engine-drive.js';
|
|
3
|
+
import { announceMessageOrigin } from '../stamp/channel.js';
|
|
3
4
|
import { createClientHandlers } from './frame-client.js';
|
|
4
5
|
import { createMemoryRefGuidance } from './frame-memory-refs.js';
|
|
5
6
|
import { createAuthReloadGate } from './auth-reload.js';
|
|
@@ -51,6 +52,12 @@ export function createFrameDispatchContext(deps) {
|
|
|
51
52
|
const handleDriveFrame = (client, frame) => {
|
|
52
53
|
if (notWritable(client, 'drive the engine'))
|
|
53
54
|
return;
|
|
55
|
+
// These frames are the viewer's input vocabulary — a person typed them.
|
|
56
|
+
// (crtrd's `deliver` never lands here: frame-client synthesizes its own
|
|
57
|
+
// prompt frame and calls driveEngine directly, announcing its own origin.)
|
|
58
|
+
if (frame.type === 'prompt' || frame.type === 'steer' || frame.type === 'follow_up') {
|
|
59
|
+
announceMessageOrigin(frame.text, { type: 'human', channel: { type: 'viewer' } });
|
|
60
|
+
}
|
|
54
61
|
driveEngine(client, frame);
|
|
55
62
|
};
|
|
56
63
|
const clientHandlers = createClientHandlers({
|
|
@@ -27,7 +27,9 @@ export declare function readBrokerCanceledEntryIds(nodeId: string): Set<BrokerIn
|
|
|
27
27
|
export declare function reportRefNodeId(ref: string | undefined): string | undefined;
|
|
28
28
|
/** One-shot runtime cards an entry carries (rendered envelopes written by the
|
|
29
29
|
* message API). They ride the delivery their own entry belongs to, so a note
|
|
30
|
-
* can never land ahead of an unrelated person's message
|
|
30
|
+
* can never land ahead of an unrelated person's message — except in the one
|
|
31
|
+
* deliberate fold below, where deferred entries join a person's delivery as
|
|
32
|
+
* the context their turn reads. */
|
|
31
33
|
export declare function entryCards(entry: BrokerInboxEntry): readonly string[];
|
|
32
34
|
/** One deliverable user message: either a person's verbatim words or the whole
|
|
33
35
|
* `<runtime kind="inbox">` envelope. Never both — a reader parses an envelope
|
|
@@ -40,5 +42,13 @@ export interface InboxDelivery {
|
|
|
40
42
|
entries: readonly BrokerInboxEntry[];
|
|
41
43
|
}
|
|
42
44
|
/** Split unread entries into ordered deliveries: each human entry verbatim, and
|
|
43
|
-
*
|
|
45
|
+
* each CONTIGUOUS run of node/system entries as one card in the physical slot
|
|
46
|
+
* that run occupies. A deferred-only card is folded into the person's delivery
|
|
47
|
+
* waiting immediately behind it rather than being deliverable on its own.
|
|
48
|
+
*
|
|
49
|
+
* The runs are what keep the cursor honest. Exactly one delivery goes out per
|
|
50
|
+
* cycle and the rest requeue, while a sent delivery commits the cursor across
|
|
51
|
+
* its own entries — so a delivery that spanned a person's message would commit
|
|
52
|
+
* past words this cycle never sent. A run can never span one, and node mail
|
|
53
|
+
* that arrived after a person's message now waits behind it. */
|
|
44
54
|
export declare function coalesceBrokerInbox(entries: readonly BrokerInboxEntry[], reportNodes: ReadonlyMap<string, BrokerReportNodeDTO>): readonly InboxDelivery[];
|