@north-light/crouter 0.3.276 → 0.3.278

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.
@@ -103,6 +103,15 @@ export interface DashboardRow {
103
103
  /** Lazy-enrichment guard (internal): true once loadPreview has read goal + the
104
104
  * session (prompts + lastAssistant). Idempotency marker for the selected row. */
105
105
  previewLoaded?: boolean;
106
+ /** The node's conversation as ORDERED user/assistant turns — what the preview panel
107
+ * paints as a chat transcript. Populated LAZILY by loadTranscript for the SELECTED
108
+ * row only (never on the background warm pass): one transcript per row is a few KB,
109
+ * and holding one for every node on a multi-thousand-node canvas is what `prompts`
110
+ * and `lastAssistant` exist to avoid. Undefined until asked for; an empty array
111
+ * means "read, and the node has no session turns". */
112
+ transcript?: TranscriptTurn[];
113
+ /** Lazy guard (internal): true once loadTranscript has read the session for this row. */
114
+ transcriptLoaded?: boolean;
106
115
  /** The source `NodeRow` this row was built from (dashboardRowFromNodeRow only).
107
116
  * Carries every column listNodes already fetched — the runtime fields
108
117
  * (host_kind/parent/waiting_for/final_report/finalized_at/pi_pid) plus the
@@ -132,6 +141,17 @@ interface SessionParts {
132
141
  lastAssistant?: string;
133
142
  }
134
143
  export declare function readSessionParts(sessionFile: string | null | undefined): SessionParts;
144
+ /** One conversation turn as the preview panel paints it. */
145
+ export interface TranscriptTurn {
146
+ role: 'user' | 'assistant';
147
+ text: string;
148
+ }
149
+ /** The node's conversation as ordered user/assistant turns, newest-last, capped to
150
+ * the last TRANSCRIPT_TURNS. Generated context cards are dropped (they are runtime
151
+ * scaffolding, not something the user or the agent said) and so are assistant turns
152
+ * with no text (tool-only turns), so the result reads as a chat rather than a log.
153
+ * One file read; never throws; empty for a node that was never revived. */
154
+ export declare function readTranscript(sessionFile: string | null | undefined): TranscriptTurn[];
135
155
  /** Did this node's engine ever PRODUCE something — an assistant message with real
136
156
  * text, a tool call, or non-empty reasoning? The signal that a node did work and
137
157
  * is not an empty shell. A never-revived node (no session file) is trivially
@@ -174,4 +194,10 @@ export declare function enrichRowsFromSource(_source: CanvasSource, rows: Dashbo
174
194
  /** goal (initial-prompt.md) and session parts are local disk reads — suppressed
175
195
  * to undefined for a remote source. */
176
196
  export declare function loadPreviewFromSource(source: CanvasSource, row: DashboardRow): Promise<DashboardRow>;
197
+ /** Fold the ordered conversation onto `row` — the preview panel's chat body. Kept
198
+ * OUT of loadPreviewFromSource because that runs over every row on the background
199
+ * warm pass, and a per-row transcript across a whole canvas is exactly the memory
200
+ * cost the capped `prompts` string avoids. Call it for the selected row only.
201
+ * Remote sources have no local session file, so the transcript stays empty. */
202
+ export declare function loadTranscriptFromSource(source: CanvasSource, row: DashboardRow): Promise<DashboardRow>;
177
203
  export {};
@@ -307,6 +307,61 @@ export function readSessionParts(sessionFile) {
307
307
  }
308
308
  return { prompts, lastAssistant };
309
309
  }
310
+ /** How many trailing turns the transcript keeps. A preview panel can paint a few
311
+ * dozen lines, and search only ever windows to a match, so the whole history is
312
+ * never needed — but the window has to be wide enough that a match found in
313
+ * `prompts` (which spans the whole session) is usually still present here. */
314
+ const TRANSCRIPT_TURNS = 120;
315
+ const TRANSCRIPT_MSG_CAP = 4096;
316
+ /** The node's conversation as ordered user/assistant turns, newest-last, capped to
317
+ * the last TRANSCRIPT_TURNS. Generated context cards are dropped (they are runtime
318
+ * scaffolding, not something the user or the agent said) and so are assistant turns
319
+ * with no text (tool-only turns), so the result reads as a chat rather than a log.
320
+ * One file read; never throws; empty for a node that was never revived. */
321
+ export function readTranscript(sessionFile) {
322
+ if (sessionFile === undefined || sessionFile === null || sessionFile === '')
323
+ return [];
324
+ let lines;
325
+ try {
326
+ if (!existsSync(sessionFile))
327
+ return [];
328
+ lines = readFileSync(sessionFile, 'utf8').split('\n');
329
+ }
330
+ catch {
331
+ return [];
332
+ }
333
+ const turns = [];
334
+ for (const line of lines) {
335
+ // Same cheap prefilter as readSessionParts: skip every line that cannot be a
336
+ // user or assistant message before paying for JSON.parse.
337
+ if (line === '')
338
+ continue;
339
+ const isUser = line.indexOf('"role":"user"') !== -1 || line.indexOf('"role": "user"') !== -1;
340
+ const isAssistant = !isUser
341
+ && (line.indexOf('"role":"assistant"') !== -1 || line.indexOf('"role": "assistant"') !== -1);
342
+ if (!isUser && !isAssistant)
343
+ continue;
344
+ let rec;
345
+ try {
346
+ rec = JSON.parse(line);
347
+ }
348
+ catch {
349
+ continue;
350
+ }
351
+ if (rec.type !== 'message')
352
+ continue;
353
+ const role = rec.message?.role;
354
+ if (role !== 'user' && role !== 'assistant')
355
+ continue;
356
+ const text = extractUserText(rec.message?.content);
357
+ if (text === '')
358
+ continue;
359
+ if (parseCard({ role, content: text }) !== null)
360
+ continue;
361
+ turns.push({ role, text: text.length > TRANSCRIPT_MSG_CAP ? text.slice(0, TRANSCRIPT_MSG_CAP) : text });
362
+ }
363
+ return turns.length > TRANSCRIPT_TURNS ? turns.slice(turns.length - TRANSCRIPT_TURNS) : turns;
364
+ }
310
365
  /** Did this node's engine ever PRODUCE something — an assistant message with real
311
366
  * text, a tool call, or non-empty reasoning? The signal that a node did work and
312
367
  * is not an empty shell. A never-revived node (no session file) is trivially
@@ -550,3 +605,21 @@ export async function loadPreviewFromSource(source, row) {
550
605
  row.previewLoaded = true;
551
606
  return row;
552
607
  }
608
+ /** Fold the ordered conversation onto `row` — the preview panel's chat body. Kept
609
+ * OUT of loadPreviewFromSource because that runs over every row on the background
610
+ * warm pass, and a per-row transcript across a whole canvas is exactly the memory
611
+ * cost the capped `prompts` string avoids. Call it for the selected row only.
612
+ * Remote sources have no local session file, so the transcript stays empty. */
613
+ export async function loadTranscriptFromSource(source, row) {
614
+ if (row.transcriptLoaded === true)
615
+ return row;
616
+ if (source instanceof RemoteCanvasSource) {
617
+ row.transcript = [];
618
+ row.transcriptLoaded = true;
619
+ return row;
620
+ }
621
+ const meta = await source.getNode(row.node_id);
622
+ row.transcript = readTranscript(meta?.pi_session_file);
623
+ row.transcriptLoaded = true;
624
+ return row;
625
+ }
@@ -29,8 +29,10 @@ export declare function validateCommandManifest(raw: unknown, options: {
29
29
  * (install, bundle parse, doctor/inspect reports) so a helpAddenda key
30
30
  * naming no core command fails there — and by the help-render lookup
31
31
  * (`collectHelpAddenda`), which reproduces the gate's verdict so render
32
- * and ingress report can never disagree. Omitted only on the tolerant
33
- * compose path, where mounting ignores addenda entirely. */
32
+ * and ingress report can never disagree. Omitted on the tolerant compose
33
+ * path, where mounting ignores addenda entirely, and on that lookup's
34
+ * claimant pass, which only needs to know whether any addendum targets the
35
+ * rendered path at all. */
34
36
  coreCommandPaths?: ReadonlySet<string>;
35
37
  /** A repository fragment attaches roots below an existing extensible branch. */
36
38
  extensionFragment?: boolean;
@@ -5,8 +5,17 @@ export interface HelpAddendum {
5
5
  /** Collect every enabled plugin's addendum targeting `commandPath` (the
6
6
  * space-joined walked path, e.g. "cron add"), in plugin-name order. `gate`
7
7
  * carries the strict-validation inputs the install/bundle gates use; the
8
- * help renderer already holds both (build-root exports them). */
8
+ * help renderer already holds both (build-root exports them).
9
+ *
10
+ * `coreCommandPaths` arrives as a thunk because producing that set walks EVERY
11
+ * core subtree — the whole command tree loaded on a path that renders one
12
+ * leaf's help. So the claimant pass below runs the SAME validator without it:
13
+ * the core path set can only reject an addendum, never introduce one, so a
14
+ * commandPath no manifest claims tolerantly claims none under the gate either,
15
+ * and the expensive input is never forced. Claimants then re-validate strictly,
16
+ * reproducing the gate's exact verdict — a manifest the gate rejects renders
17
+ * nothing, including its valid keys. */
9
18
  export declare function collectHelpAddenda(commandPath: string, gate: {
10
19
  reservedCoreNames: ReadonlySet<string>;
11
- coreCommandPaths: ReadonlySet<string>;
12
- }, startDir?: string, profileId?: string | null): HelpAddendum[];
20
+ coreCommandPaths: () => Promise<ReadonlySet<string>>;
21
+ }, startDir?: string, profileId?: string | null): Promise<HelpAddendum[]>;
@@ -15,13 +15,26 @@ import { effectiveCommandPlugins, validatePluginCommands } from './discovery.js'
15
15
  /** Collect every enabled plugin's addendum targeting `commandPath` (the
16
16
  * space-joined walked path, e.g. "cron add"), in plugin-name order. `gate`
17
17
  * carries the strict-validation inputs the install/bundle gates use; the
18
- * help renderer already holds both (build-root exports them). */
19
- export function collectHelpAddenda(commandPath, gate, startDir, profileId) {
18
+ * help renderer already holds both (build-root exports them).
19
+ *
20
+ * `coreCommandPaths` arrives as a thunk because producing that set walks EVERY
21
+ * core subtree — the whole command tree loaded on a path that renders one
22
+ * leaf's help. So the claimant pass below runs the SAME validator without it:
23
+ * the core path set can only reject an addendum, never introduce one, so a
24
+ * commandPath no manifest claims tolerantly claims none under the gate either,
25
+ * and the expensive input is never forced. Claimants then re-validate strictly,
26
+ * reproducing the gate's exact verdict — a manifest the gate rejects renders
27
+ * nothing, including its valid keys. */
28
+ export async function collectHelpAddenda(commandPath, gate, startDir, profileId) {
20
29
  if (commandPath.length === 0)
21
30
  return [];
31
+ const claimants = effectiveCommandPlugins(startDir, profileId).filter((plugin) => validatePluginCommands(plugin, gate.reservedCoreNames).manifest?.helpAddenda?.[commandPath] !== undefined);
32
+ if (claimants.length === 0)
33
+ return [];
34
+ const coreCommandPaths = await gate.coreCommandPaths();
22
35
  const out = [];
23
- for (const plugin of effectiveCommandPlugins(startDir, profileId)) {
24
- const text = validatePluginCommands(plugin, gate.reservedCoreNames, gate.coreCommandPaths).manifest?.helpAddenda?.[commandPath];
36
+ for (const plugin of claimants) {
37
+ const text = validatePluginCommands(plugin, gate.reservedCoreNames, coreCommandPaths).manifest?.helpAddenda?.[commandPath];
25
38
  if (text !== undefined)
26
39
  out.push({ plugin: plugin.name, text });
27
40
  }
@@ -209,9 +209,9 @@ async function renderNodeWithAddenda(node, path, focusedFlag) {
209
209
  import('./command-plugins/help-addenda.js'),
210
210
  import('../build-root.js'),
211
211
  ]);
212
- const addenda = collectHelpAddenda(path.join(' '), {
212
+ const addenda = await collectHelpAddenda(path.join(' '), {
213
213
  reservedCoreNames: new Set(SUBTREE_NAMES),
214
- coreCommandPaths: await coreCommandPaths(),
214
+ coreCommandPaths,
215
215
  });
216
216
  return body + addenda.map((a) => `\n\n<plugin-help plugin="${a.plugin}">\n${a.text}\n</plugin-help>`).join('');
217
217
  }
@@ -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' | 'pin' | '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' | 'report' | '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.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">];
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.report"> | 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. */
@@ -148,6 +148,7 @@ function browseBindings() {
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
150
  b('crtr.browse.preview', 'Browse and pickers', 'Toggle preview', 'Toggle the preview panel.', ['v'], 'terminal', [nav]),
151
+ b('crtr.browse.report', 'Browse and pickers', 'Toggle report', 'Swap the preview panel between the conversation and the node\'s newest report.', ['shift+v'], 'terminal', [nav]),
151
152
  b('crtr.browse.pin', 'Browse and pickers', 'Pin node', 'Star or unstar the selected node.', ['p'], 'terminal', [nav]),
152
153
  b('crtr.browse.search', 'Browse and pickers', 'Search', 'Open browse search.', ['/'], 'terminal', [nav]),
153
154
  b('crtr.browse.revive-now', 'Browse and pickers', 'Revive now', 'Revive the selected node now.', ['shift+k'], 'terminal', [nav]),
@@ -15,7 +15,7 @@ const TMUX_BINDING_MANIFEST = '@crtr_keybindings_manifest';
15
15
  const ROOT_TABLE = 'root';
16
16
  const LEAF_MENU_ACTIONS = {
17
17
  'crtr.tmux.menu.promote': { description: 'promote to orchestrator', action: { kind: 'keys', keys: '/promote' } },
18
- 'crtr.tmux.menu.resume': { description: 'find node', action: { kind: 'keys', keys: '/resume-node' } },
18
+ 'crtr.tmux.menu.resume': { description: 'find node', action: { kind: 'keys', keys: '/resume' } },
19
19
  'crtr.tmux.menu.fork': { description: 'fork into idle root', action: { kind: 'direct', run: 'node fork --pane {pane}' } },
20
20
  // Mode demote is the mirror of `/promote`: `node config` persists the change;
21
21
  // the broker appends its pending context before the next provider response.
@@ -20,8 +20,9 @@ export declare function hasHumanViewer(nodeId: string): boolean;
20
20
  /** What parking an unattended node actually amounts to — the one rule both the
21
21
  * live-broker park clock and the lifecycle tick apply.
22
22
  *
23
- * `release` — something can still wake it, or a person is watching it, so
24
- * parking is a pause.
23
+ * `release` — something can still wake it, a person is watching it, or a
24
+ * fatal provider fault prevents its engine from completing the
25
+ * summary turn; parking is a pause.
25
26
  * `park` — a resident with nothing left to wake it has completed its
26
27
  * conversation, and the row becomes prunable history. The live
27
28
  * clock asks it for a summary turn first; this verdict is what
@@ -6,6 +6,7 @@ import { readOutputRequest } from '../../core/runtime/structured-output.js';
6
6
  import { focusOf } from '../../core/runtime/placement.js';
7
7
  import { hasAttachedBrokerViewers } from '../../core/runtime/broker/client-registry.js';
8
8
  import { fanDoctrineWake } from '../../core/runtime/close.js';
9
+ import { readFault } from '../../core/runtime/fault.js';
9
10
  import { transition } from '../../core/runtime/lifecycle.js';
10
11
  /** Durable work that requires a node to remain automatically wakeable. */
11
12
  export function hasLiveObligation(nodeId) {
@@ -50,6 +51,9 @@ export function parkEvent(nodeId, lifecycle) {
50
51
  return 'release';
51
52
  if (hasLiveObligation(nodeId))
52
53
  return 'release';
54
+ const fault = readFault(nodeId);
55
+ if (fault?.link === 'pi→provider' && fault.retry.disposition === 'fatal')
56
+ return 'release';
53
57
  if (lifecycle === 'resident')
54
58
  return hasUnseenWakeMail(nodeId) ? 'release' : 'park';
55
59
  if (readInboxSince(nodeId, readCursor(nodeId)).length > 0)
package/dist/types.d.ts CHANGED
@@ -311,11 +311,11 @@ export interface ScopeConfig {
311
311
  /** Fold every SETTLED tool call in the attach viewer down to its single call
312
312
  * line (`/fold-tools`, Alt+C → z → f). A tool still running is never folded —
313
313
  * it folds itself once it settles. Toggling it in a viewer writes back here,
314
- * so the choice carries into later chats and later viewers. Default false. */
314
+ * so the choice carries into later chats and later viewers. Default true. */
315
315
  fold_finished_tools: boolean;
316
316
  /** Replace completed folded tool-call groups with a broker-generated one-line
317
317
  * summary when one is available. Pending and failed groups stay as calls.
318
- * Default false. */
318
+ * Default true. */
319
319
  summarize_tool_calls: boolean;
320
320
  /** Show individual memory reads and edited files in a folded tool recap.
321
321
  * Off shows the compact activity-count line instead. Default true. */
package/dist/types.js CHANGED
@@ -97,8 +97,8 @@ export function defaultScopeConfig() {
97
97
  live_cycles: DEFAULT_LIVE_CYCLES,
98
98
  condensed_history: DEFAULT_CONDENSED_HISTORY,
99
99
  bash_tool_purpose: true,
100
- fold_finished_tools: false,
101
- summarize_tool_calls: false,
100
+ fold_finished_tools: true,
101
+ summarize_tool_calls: true,
102
102
  detailed_tool_recaps: true,
103
103
  keybindings: {},
104
104
  tmux_passthrough: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.276",
3
+ "version": "0.3.278",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -70,6 +70,8 @@
70
70
  ],
71
71
  "scripts": {
72
72
  "build": "node scripts/build.mjs",
73
+ "activate": "npm run build && npm run install-runtime && crtr sys daemon restart",
74
+ "activate:pull": "git pull && npm install && npm run activate",
73
75
  "build:api": "tsc -p packages/crouter-api/tsconfig.json",
74
76
  "typecheck:pi-packages": "npx tsc -p src/builtin-pi-packages/pi-crtr-extensions/tsconfig.json",
75
77
  "test:build-output": "node scripts/build-concurrency-test.mjs",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.276",
3
+ "version": "0.3.278",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.276",
9
+ "version": "0.3.278",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {