@north-light/crouter 0.3.169 → 0.3.171

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.
Files changed (122) hide show
  1. package/dist/build-root.d.ts +1 -16
  2. package/dist/build-root.js +1 -25
  3. package/dist/builtin-memory/insights/capture.md +66 -0
  4. package/dist/builtin-memory/insights/init.md +45 -0
  5. package/dist/builtin-memory/internal/plugins.md +12 -11
  6. package/dist/cli.js +2 -6
  7. package/dist/clients/attach/__tests__/attach-keybindings.test.js +7 -1
  8. package/dist/clients/attach/__tests__/context-message.test.js +50 -3
  9. package/dist/clients/attach/__tests__/diagram.test.js +1 -1
  10. package/dist/clients/attach/chrome/header.d.ts +10 -0
  11. package/dist/clients/attach/chrome/header.js +35 -0
  12. package/dist/clients/attach/chrome/status-line.d.ts +3 -0
  13. package/dist/clients/attach/chrome/status-line.js +5 -0
  14. package/dist/clients/attach/chrome/widgets.js +7 -18
  15. package/dist/clients/attach/input/controller.d.ts +10 -0
  16. package/dist/clients/attach/input/controller.js +3 -0
  17. package/dist/clients/attach/overlays/help.d.ts +33 -0
  18. package/dist/clients/attach/overlays/help.js +204 -0
  19. package/dist/clients/attach/render/chat-view.d.ts +81 -9
  20. package/dist/clients/attach/render/chat-view.js +434 -90
  21. package/dist/clients/attach/render/context-message.js +3 -3
  22. package/dist/clients/attach/render/diagram.js +2 -2
  23. package/dist/clients/attach/render/measured-container.d.ts +94 -0
  24. package/dist/clients/attach/render/measured-container.js +307 -0
  25. package/dist/clients/attach/render/transcript-copy.d.ts +9 -0
  26. package/dist/clients/attach/render/transcript-copy.js +81 -0
  27. package/dist/clients/attach/render/viewport.d.ts +29 -0
  28. package/dist/clients/attach/render/viewport.js +160 -0
  29. package/dist/clients/attach/session/bindings.d.ts +5 -1
  30. package/dist/clients/attach/session/bindings.js +16 -0
  31. package/dist/clients/attach/session/context.d.ts +5 -3
  32. package/dist/clients/attach/session/frame.d.ts +34 -0
  33. package/dist/clients/attach/session/frame.js +173 -0
  34. package/dist/clients/attach/session/frames.d.ts +2 -0
  35. package/dist/clients/attach/session/frames.js +3 -0
  36. package/dist/clients/attach/session/input-wiring.d.ts +10 -4
  37. package/dist/clients/attach/session/input-wiring.js +13 -13
  38. package/dist/clients/attach/session/keys.d.ts +4 -1
  39. package/dist/clients/attach/session/keys.js +9 -4
  40. package/dist/clients/attach/session/layout.d.ts +10 -8
  41. package/dist/clients/attach/session/layout.js +32 -31
  42. package/dist/clients/attach/session/mouse.d.ts +15 -0
  43. package/dist/clients/attach/session/mouse.js +61 -0
  44. package/dist/clients/attach/slash/dispatch.d.ts +10 -0
  45. package/dist/clients/attach/slash/dispatch.js +35 -2
  46. package/dist/clients/attach/viewer.js +605 -581
  47. package/dist/clients/web/dev-server.d.ts +4 -1
  48. package/dist/clients/web/dev-server.js +8 -19
  49. package/dist/clients/web/web-cmd.js +1 -1
  50. package/dist/commands/human/prompts.js +16 -6
  51. package/dist/commands/pkg/plugin-manage.js +143 -68
  52. package/dist/commands/sys/__tests__/setup-core.test.js +4 -2
  53. package/dist/commands/sys/setup-core.js +21 -16
  54. package/dist/core/__tests__/human-deliver.test.js +62 -2
  55. package/dist/core/clipboard-text.d.ts +8 -0
  56. package/dist/core/clipboard-text.js +18 -0
  57. package/dist/core/command-manifests/registry.d.ts +1 -3
  58. package/dist/core/command-plugins/bundle.d.ts +29 -0
  59. package/dist/core/command-plugins/bundle.js +198 -0
  60. package/dist/core/command-plugins/discovery.d.ts +0 -8
  61. package/dist/core/command-plugins/discovery.js +2 -39
  62. package/dist/core/command-plugins/transport/http-fetch.d.ts +4 -30
  63. package/dist/core/command-plugins/transport/http-fetch.js +17 -95
  64. package/dist/core/command.d.ts +9 -13
  65. package/dist/core/command.js +12 -17
  66. package/dist/core/config.js +2 -1
  67. package/dist/core/keybindings/__tests__/resolve.test.js +4 -0
  68. package/dist/core/keybindings/attach-control.d.ts +3 -0
  69. package/dist/core/keybindings/attach-control.js +1 -0
  70. package/dist/core/keybindings/catalog.d.ts +2 -2
  71. package/dist/core/keybindings/catalog.js +8 -0
  72. package/dist/core/runtime/broker-protocol.d.ts +13 -1
  73. package/dist/core/runtime/broker.js +213 -5
  74. package/dist/core/runtime/canvas-extensions.d.ts +3 -0
  75. package/dist/core/runtime/canvas-extensions.js +3 -0
  76. package/dist/core/runtime/front-door.js +4 -4
  77. package/dist/core/runtime/kickoff.d.ts +1 -5
  78. package/dist/core/runtime/kickoff.js +1 -5
  79. package/dist/core/runtime/naming.d.ts +7 -0
  80. package/dist/core/runtime/naming.js +5 -2
  81. package/dist/core/runtime/node-read.js +15 -0
  82. package/dist/core/runtime/package-health.d.ts +6 -21
  83. package/dist/core/runtime/package-health.js +12 -123
  84. package/dist/core/runtime/stop-guard.d.ts +1 -1
  85. package/dist/core/runtime/stop-guard.js +3 -3
  86. package/dist/core/runtime/tool-group-summary.d.ts +19 -0
  87. package/dist/core/runtime/tool-group-summary.js +117 -0
  88. package/dist/core/termrender/code-doc.d.ts +12 -0
  89. package/dist/core/termrender/code-doc.js +91 -0
  90. package/dist/core/termrender/display.d.ts +12 -0
  91. package/dist/core/termrender/display.js +19 -0
  92. package/dist/core/termrender/termrender.d.ts +73 -0
  93. package/dist/core/termrender/termrender.js +795 -0
  94. package/dist/core/termrender/version.d.ts +1 -0
  95. package/dist/core/termrender/version.js +1 -0
  96. package/dist/core/user-settings.d.ts +14 -0
  97. package/dist/core/user-settings.js +20 -0
  98. package/dist/daemon/crtrd.js +5 -6
  99. package/dist/index.d.ts +2 -0
  100. package/dist/index.js +1 -0
  101. package/dist/pi-extensions/__tests__/canvas-structured-output.test.d.ts +1 -0
  102. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +63 -0
  103. package/dist/pi-extensions/canvas-stophook.js +2 -2
  104. package/dist/pi-extensions/canvas-structured-output.js +52 -8
  105. package/dist/pi-extensions/summary-tool.d.ts +33 -0
  106. package/dist/pi-extensions/summary-tool.js +62 -0
  107. package/dist/shared/generated-context.d.ts +19 -4
  108. package/dist/shared/generated-context.js +98 -7
  109. package/dist/shared/tool-groups.d.ts +23 -0
  110. package/dist/shared/tool-groups.js +60 -0
  111. package/dist/types.d.ts +11 -0
  112. package/dist/types.js +1 -0
  113. package/dist/web-client/assets/{index-U_NZ66VE.js → index-D7I3gHCL.js} +26 -19
  114. package/dist/web-client/index.html +1 -1
  115. package/dist/web-client/sw.js +1 -1
  116. package/package.json +3 -1
  117. package/runtime.lock.json +62 -3
  118. package/scripts/postinstall.mjs +14 -1
  119. package/dist/core/command-plugins/store.d.ts +0 -16
  120. package/dist/core/command-plugins/store.js +0 -64
  121. package/dist/core/runtime/fault-recovery-nudge.d.ts +0 -3
  122. package/dist/core/runtime/fault-recovery-nudge.js +0 -3
@@ -14,6 +14,7 @@ import { readFileSync } from 'node:fs';
14
14
  import { spawnSync } from 'node:child_process';
15
15
  import { constants as osConstants } from 'node:os';
16
16
  import { isatty } from 'node:tty';
17
+ import { resolve } from 'node:path';
17
18
  // ---------------------------------------------------------------------------
18
19
  // Factory functions
19
20
  // ---------------------------------------------------------------------------
@@ -268,6 +269,7 @@ function flagNameToKey(name) {
268
269
  export async function parseArgv(params, tokens, options) {
269
270
  const result = {};
270
271
  const provided = new Set(); // Track explicitly-provided parameter names
272
+ const contextFilePaths = new Map();
271
273
  // Index params by kind for quick lookup
272
274
  const positionalParam = params.find((p) => p.kind === 'positional');
273
275
  const stdinParam = params.find((p) => p.kind === 'stdin');
@@ -332,6 +334,7 @@ export async function parseArgv(params, tokens, options) {
332
334
  }
333
335
  const contextFileKey = flagNameToKey(contextFileParam.name);
334
336
  result[contextFileKey] = parsed;
337
+ contextFilePaths.set(contextFileKey, resolve(pathVal));
335
338
  provided.add(contextFileKey);
336
339
  i++;
337
340
  continue;
@@ -539,9 +542,12 @@ export async function parseArgv(params, tokens, options) {
539
542
  if (options?.onProvidedParams !== undefined) {
540
543
  options.onProvidedParams(provided);
541
544
  }
545
+ if (options?.onContextFilePaths !== undefined) {
546
+ options.onContextFilePaths(contextFilePaths);
547
+ }
542
548
  return result;
543
549
  }
544
- export async function runCli(root, argv, options) {
550
+ export async function runCli(root, argv) {
545
551
  // argv is process.argv — strip node binary + script path. `--json` is a
546
552
  // global: pull it out anywhere it appears so the rest of argv parses against
547
553
  // the leaf schema unchanged, and switch stdout from rendered prose to raw
@@ -564,21 +570,6 @@ export async function runCli(root, argv, options) {
564
570
  }
565
571
  let { node, path, remaining } = walk(root, tokens);
566
572
  try {
567
- // An unknown first token that misses at root is the only site absent
568
- // HTTP-plugin stores may hydrate. A deeper unknown token under a known
569
- // command never triggers it. Invoke the seam once; if it hydrated commands,
570
- // re-walk the original argv against the rebuilt root. Keeping it inside the
571
- // try sends any seam failure through the ordinary error handler.
572
- if (options?.onUnknownFirstTokenMiss !== undefined &&
573
- node.kind === 'root' &&
574
- path.length === 0 &&
575
- remaining.length > 0 &&
576
- remaining[0] !== '-h' &&
577
- remaining[0] !== '--help') {
578
- const rebuilt = await options.onUnknownFirstTokenMiss();
579
- if (rebuilt !== undefined)
580
- ({ node, path, remaining } = walk(rebuilt, tokens));
581
- }
582
573
  // Passthrough branches (e.g. `crtr capture`) opt out of the tree model
583
574
  // entirely: forward every token after the branch verbatim to an external
584
575
  // binary before any of the normal unknown-path / help / dispatch logic
@@ -618,13 +609,17 @@ export async function runCli(root, argv, options) {
618
609
  const leaf = node;
619
610
  const params = leaf.help.params !== undefined ? leaf.help.params : [];
620
611
  let providedParams = new Set();
612
+ let contextFilePaths = new Map();
621
613
  const input = await parseArgv(params, remaining, {
622
614
  leafPath: path,
623
615
  onProvidedParams: (provided) => {
624
616
  providedParams = provided;
625
617
  },
618
+ onContextFilePaths: (paths) => {
619
+ contextFilePaths = paths;
620
+ },
626
621
  });
627
- const context = { providedParams };
622
+ const context = { providedParams, contextFilePaths };
628
623
  const result = await operationIdContext.fresh(() => leaf.run(input, context));
629
624
  if (result !== undefined && result !== null) {
630
625
  if (isJsonOutput()) {
@@ -441,6 +441,7 @@ function mergeConfig(partial) {
441
441
  ? partial.condensed_history
442
442
  : defaults.condensed_history;
443
443
  const fold_finished_tools = typeof partial.fold_finished_tools === 'boolean' ? partial.fold_finished_tools : defaults.fold_finished_tools;
444
+ const summarize_tool_calls = typeof partial.summarize_tool_calls === 'boolean' ? partial.summarize_tool_calls : defaults.summarize_tool_calls;
444
445
  const keybindings = normalizeKeybindings(partial.keybindings);
445
446
  const modelLadders = mergeModelLadders(partial.modelLadders);
446
447
  const modelRoutes = mergeModelRoutes(partial.modelRoutes);
@@ -448,7 +449,7 @@ function mergeConfig(partial) {
448
449
  const kinds = mergeKinds(partial.kinds);
449
450
  const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
450
451
  const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
451
- return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, completion_bell, live_cycles, condensed_history, fold_finished_tools, keybindings, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
452
+ return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, completion_bell, live_cycles, condensed_history, fold_finished_tools, summarize_tool_calls, keybindings, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
452
453
  }
453
454
  /** Raw (un-defaulted) partial config for one scope, or null if the scope has
454
455
  * no root or no config.json. Used by `readMergedLaunchConfig` to layer
@@ -34,12 +34,16 @@ const REQUIRED_IDS = [
34
34
  ['clear-or-detach', 'crtr.attach.clear-or-detach'],
35
35
  ['mode-cycle', 'crtr.mode.cycle'],
36
36
  ['graph.toggle', 'crtr.attach.graph.toggle'],
37
+ ['help.toggle', 'crtr.attach.help.toggle'],
37
38
  ['model-ladder.next', 'crtr.attach.model-ladder.next'],
38
39
  ['model-ladder.previous', 'crtr.attach.model-ladder.previous'],
39
40
  ['command.inspect', 'crtr.attach.command.inspect'],
40
41
  ['file-review', 'crtr.attach.file-review'],
41
42
  ['profile-files', 'crtr.attach.profile-files'],
42
43
  ]),
44
+ ...ids('crtr.attach.scroll', ['up', 'down', 'page-up', 'page-down', 'live']),
45
+ 'crtr.attach.mouse.toggle',
46
+ 'crtr.attach.copy.message',
43
47
  ...ids('crtr.graph', ['down', 'up', 'first', 'last', 'collapse-or-manager', 'expand-or-child', 'focus', 'revive', 'focus-manager', 'close-subtree', 'dismiss']),
44
48
  ...ids('crtr.graph.confirm', ['accept', 'cancel']),
45
49
  ...ids('crtr.view.host', ['force-quit', 'editor.backspace']),
@@ -10,6 +10,9 @@ export declare const ATTACH_CONTROL_BINDINGS: readonly [{
10
10
  }, {
11
11
  readonly menuId: "crtr.tmux.menu.attach.graph.toggle";
12
12
  readonly actionId: "crtr.attach.graph.toggle";
13
+ }, {
14
+ readonly menuId: "crtr.tmux.menu.attach.help.toggle";
15
+ readonly actionId: "crtr.attach.help.toggle";
13
16
  }, {
14
17
  readonly menuId: "crtr.tmux.menu.attach.model-ladder.next";
15
18
  readonly actionId: "crtr.attach.model-ladder.next";
@@ -3,6 +3,7 @@ export const ATTACH_CONTROL_BINDINGS = [
3
3
  { menuId: 'crtr.tmux.menu.attach.clear-or-detach', actionId: 'crtr.attach.clear-or-detach' },
4
4
  { menuId: 'crtr.tmux.menu.attach.mode-cycle', actionId: 'crtr.mode.cycle' },
5
5
  { menuId: 'crtr.tmux.menu.attach.graph.toggle', actionId: 'crtr.attach.graph.toggle' },
6
+ { menuId: 'crtr.tmux.menu.attach.help.toggle', actionId: 'crtr.attach.help.toggle' },
6
7
  { menuId: 'crtr.tmux.menu.attach.model-ladder.next', actionId: 'crtr.attach.model-ladder.next' },
7
8
  { menuId: 'crtr.tmux.menu.attach.model-ladder.previous', actionId: 'crtr.attach.model-ladder.previous' },
8
9
  { menuId: 'crtr.tmux.menu.attach.command.inspect', actionId: 'crtr.attach.command.inspect' },
@@ -10,8 +10,8 @@ type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' |
10
10
  type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
11
11
  type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
12
12
  type SetupModelsAction = 'open' | 'commit' | 'cancel' | 'scope-next';
13
- 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.view.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view.inspector.${'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | '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}`;
14
- 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.tmux.view.next"> | BindingDefinition<"crtr.tmux.view.previous"> | 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.view.host.force-quit"> | BindingDefinition<"crtr.view.host.editor.backspace"> | BindingDefinition<"crtr.view.chat.submit"> | BindingDefinition<"crtr.view.chat.contextual-cancel"> | BindingDefinition<"crtr.view.chat.reconnect"> | BindingDefinition<"crtr.view.chat.quit"> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view.inspector.down"> | BindingDefinition<"crtr.view.inspector.up"> | BindingDefinition<"crtr.view.inspector.section-next"> | BindingDefinition<"crtr.view.inspector.section-prev"> | BindingDefinition<"crtr.view.inspector.page-down"> | BindingDefinition<"crtr.view.inspector.page-up"> | BindingDefinition<"crtr.view.inspector.toggle-detail"> | BindingDefinition<"crtr.view.inspector.cancel-item"> | BindingDefinition<"crtr.view.inspector.open-log"> | BindingDefinition<"crtr.view.inspector.confirm"> | BindingDefinition<"crtr.view.inspector.dismiss"> | BindingDefinition<"crtr.view.inspector.retarget"> | BindingDefinition<"crtr.view.inspector.refresh"> | BindingDefinition<"crtr.view.inspector.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.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.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.9" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7">)[], 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.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
+ 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.view.${'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' | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view.inspector.${'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | '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}`;
14
+ 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.tmux.view.next"> | BindingDefinition<"crtr.tmux.view.previous"> | 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.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.view.host.force-quit"> | BindingDefinition<"crtr.view.host.editor.backspace"> | BindingDefinition<"crtr.view.chat.submit"> | BindingDefinition<"crtr.view.chat.contextual-cancel"> | BindingDefinition<"crtr.view.chat.reconnect"> | BindingDefinition<"crtr.view.chat.quit"> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view.inspector.down"> | BindingDefinition<"crtr.view.inspector.up"> | BindingDefinition<"crtr.view.inspector.section-next"> | BindingDefinition<"crtr.view.inspector.section-prev"> | BindingDefinition<"crtr.view.inspector.page-down"> | BindingDefinition<"crtr.view.inspector.page-up"> | BindingDefinition<"crtr.view.inspector.toggle-detail"> | BindingDefinition<"crtr.view.inspector.cancel-item"> | BindingDefinition<"crtr.view.inspector.open-log"> | BindingDefinition<"crtr.view.inspector.confirm"> | BindingDefinition<"crtr.view.inspector.dismiss"> | BindingDefinition<"crtr.view.inspector.retarget"> | BindingDefinition<"crtr.view.inspector.refresh"> | BindingDefinition<"crtr.view.inspector.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.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.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.9" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7">)[], 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.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">];
15
15
  export declare const BINDING_IDS: ReadonlySet<string>;
16
16
  export declare const SAFETY_REQUIREMENTS: readonly SafetyRequirement[];
17
17
  export {};
@@ -74,11 +74,19 @@ export const BINDING_CATALOG = Object.freeze([
74
74
  ...attachEntry('crtr.tmux.menu.attach.clear-or-detach', 'crtr.attach.clear-or-detach', 'Clear or detach', 'Clear input or detach when input is empty.', 'c', ['ctrl+c']),
75
75
  ...attachEntry('crtr.tmux.menu.attach.mode-cycle', 'crtr.mode.cycle', 'Cycle mode', 'Cycle the active agent mode.', 'p', ['alt+shift+p']),
76
76
  ...attachEntry('crtr.tmux.menu.attach.graph.toggle', 'crtr.attach.graph.toggle', 'Toggle graph', 'Open or close the graph overlay.', 'g', ['alt+g']),
77
+ ...attachEntry('crtr.tmux.menu.attach.help.toggle', 'crtr.attach.help.toggle', 'Help', 'Open the attach help overlay.', 'k', ['alt+shift+k']),
77
78
  ...attachEntry('crtr.tmux.menu.attach.model-ladder.next', 'crtr.attach.model-ladder.next', 'Next model ladder', 'Move to the next model ladder rung.', ']', ['alt+m']),
78
79
  ...attachEntry('crtr.tmux.menu.attach.model-ladder.previous', 'crtr.attach.model-ladder.previous', 'Previous model ladder', 'Move to the previous model ladder rung.', '[', ['alt+shift+m']),
79
80
  ...attachEntry('crtr.tmux.menu.attach.command.inspect', 'crtr.attach.command.inspect', 'Inspect loaded command', 'Open the loaded slash command’s expanded prompt.', 'h', ['alt+shift+h']),
80
81
  ...attachEntry('crtr.tmux.menu.attach.file-review', 'crtr.attach.file-review', 'Review a file', 'Open a line-anchored review of a file named in the transcript.', 'r', ['alt+shift+r']),
81
82
  ...attachEntry('crtr.tmux.menu.attach.profile-files', 'crtr.attach.profile-files', 'Search profile files', 'Search files across the node’s profile projects, cwd, and ancestor crouter memory stores.', 'f', []),
83
+ b('crtr.attach.scroll.up', 'Attach', 'Scroll up', 'Scroll the transcript one line up.', ['shift+up'], 'terminal', ATTACH_CONTEXTS),
84
+ b('crtr.attach.scroll.down', 'Attach', 'Scroll down', 'Scroll the transcript one line down.', ['shift+down'], 'terminal', ATTACH_CONTEXTS),
85
+ b('crtr.attach.scroll.page-up', 'Attach', 'Scroll page up', 'Scroll the transcript one page up.', ['pageup'], 'terminal', ATTACH_CONTEXTS),
86
+ b('crtr.attach.scroll.page-down', 'Attach', 'Scroll page down', 'Scroll the transcript one page down.', ['pagedown'], 'terminal', ATTACH_CONTEXTS),
87
+ b('crtr.attach.scroll.live', 'Attach', 'Return to live', 'Return the transcript to the live bottom.', ['shift+end'], 'terminal', ATTACH_CONTEXTS),
88
+ b('crtr.attach.mouse.toggle', 'Attach', 'Toggle mouse wheel scrolling', 'Toggle mouse wheel scrolling on or off.', ['alt+shift+o'], 'terminal', ATTACH_CONTEXTS),
89
+ b('crtr.attach.copy.message', 'Attach', 'Copy visible message', 'Copy the newest message visible in the transcript viewport.', ['alt+shift+y'], 'terminal', ATTACH_CONTEXTS),
82
90
  b('crtr.graph.down', 'GRAPH', 'Move down', 'Move to the next graph row.', ['j'], 'terminal', ['graph']),
83
91
  b('crtr.graph.up', 'GRAPH', 'Move up', 'Move to the previous graph row.', ['k'], 'terminal', ['graph']),
84
92
  b('crtr.graph.first', 'GRAPH', 'First row', 'Move to the first graph row.', ['g'], 'terminal', ['graph']),
@@ -1,6 +1,7 @@
1
1
  import type { AgentSession, AgentSessionEvent, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcSessionState, SessionEntry, SessionInfo, SettingsConfig, SessionStats } from '@earendil-works/pi-coding-agent';
2
2
  import type { Api, ImageContent, Model } from '@earendil-works/pi-ai';
3
3
  import type { BrokerErrorFrame } from '../../api/dto/broker.js';
4
+ import type { ToolGroupSummary } from '../../shared/tool-groups.js';
4
5
  /** pi's own RPC types, re-exported unchanged. `broker-protocol` stays the one
5
6
  * import site for the wire union; the shapes themselves are pi's, versioned by
6
7
  * pi, so a pi change is a compile error here instead of silent drift. */
@@ -45,6 +46,10 @@ export interface BrokerSnapshot {
45
46
  }>;
46
47
  title: string | undefined;
47
48
  };
49
+ /** Broker-owned summaries keyed by the first tool-call id in each group.
50
+ * Structured objects with bullets, nodesSpawned, and filesEdited.
51
+ * Optional so snapshots from an older live runtime remain readable. */
52
+ toolGroupSummaries?: Record<string, ToolGroupSummary>;
48
53
  }
49
54
  /** Opens the connection and FIXES this client's role for its lifetime. A repeated
50
55
  * `hello` on an already-helloed socket never changes the established role. */
@@ -563,10 +568,17 @@ export interface ExtensionUIDismissFrame {
563
568
  type: 'extension_ui_dismiss';
564
569
  id: string;
565
570
  }
571
+ /** A completed broker-side tool-group summary. The viewer owns presentation;
572
+ * this frame carries the durable group key and structured summary object. */
573
+ export interface ToolGroupSummaryFrame {
574
+ type: 'tool_group_summary';
575
+ key: string;
576
+ summary: ToolGroupSummary;
577
+ }
566
578
  /** Everything the broker can send. Live `AgentSessionEvent`s are relayed
567
579
  * verbatim (the broker adds nothing); the broker's own control frames carry
568
580
  * non-colliding `type` discriminants. */
569
- export type BrokerToClient = WelcomeFrame | ModelChangedFrame | ErrorFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | AgentSessionEvent;
581
+ export type BrokerToClient = WelcomeFrame | ModelChangedFrame | ErrorFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | ToolGroupSummaryFrame | AgentSessionEvent;
570
582
  /** Encode one frame as a single newline-terminated JSON line. */
571
583
  export declare function encodeFrame(frame: ClientToBroker | BrokerToClient): string;
572
584
  /** Byte bounds for a {@link FrameDecoder} (C5). */
@@ -32,6 +32,8 @@ import { operationIdContext } from '../events/operation-id.js';
32
32
  import { findMissingPackagePaths } from './package-health.js';
33
33
  import { SessionListCache } from './session-list-cache.js';
34
34
  import { getNode, updateNode } from '../canvas/index.js';
35
+ import { readConfig } from '../config.js';
36
+ import { atomicWriteJson, readJsonIfExists } from '../fs-utils.js';
35
37
  import { buildRefInventory } from '../memory/inline-ref-inventory.js';
36
38
  import { buildGuidance } from '../memory/inline-ref-guidance.js';
37
39
  import { clearSessionCache } from '../substrate/session-cache.js';
@@ -44,7 +46,9 @@ import { PoolCredentialStore } from '../pool-credential-store.js';
44
46
  import { clearFault, isModelNotFoundError, isProviderUnconfiguredError, readFault, recordFault, recordFaultForCurrentNode } from './fault.js';
45
47
  import { clearCleanAbort, markBusy, markCleanAbort } from './busy.js';
46
48
  import { extractCoolingDeadline } from './managed-provider-cooling.js';
47
- import { AUTH_FAULT_RECOVERY_BODY, CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY } from './fault-recovery-nudge.js';
49
+ import { AUTH_FAULT_RECOVERY_BODY, CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY, formatModelFallbackRecovery, generatedContextText, } from '../../shared/generated-context.js';
50
+ import { assistantVisibleText, isTrueUserMessage, normalizeToolGroupSummaryRecord, } from '../../shared/tool-groups.js';
51
+ import { headlessToolGroupSummary, serializeToolGroup } from './tool-group-summary.js';
48
52
  import { markFatalOnExhaust, nextFaultRetry, policyFor } from './fault-recovery.js';
49
53
  import { probeOnline as probeOnlineReal } from './connectivity.js';
50
54
  import { CRTR_CYCLE_CUSTOM_TYPE, cycleAwareMessages } from './session-cycles.js';
@@ -588,7 +592,26 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
588
592
  const displayStatuses = new Map();
589
593
  const displayWidgets = new Map();
590
594
  let displayTitle;
595
+ const toolSummariesPath = join(jobDir(nodeId), 'tool-summaries.json');
596
+ const toolGroupSummaries = new Map();
597
+ try {
598
+ const persisted = readJsonIfExists(toolSummariesPath);
599
+ for (const [key, summary] of Object.entries(normalizeToolGroupSummaryRecord(persisted))) {
600
+ toolGroupSummaries.set(key, summary);
601
+ }
602
+ }
603
+ catch {
604
+ // A malformed sidecar merely withholds summaries; transcript tools remain visible.
605
+ }
606
+ const summaryAttempted = new Set(toolGroupSummaries.keys());
607
+ const generatingToolGroups = new Set();
608
+ const toolGroupsByCallId = new Map();
609
+ const summaryTasks = new Set();
610
+ let openToolGroup;
611
+ let lastToolGroupBoundaryProse = '';
612
+ let sawAssistantProse = false;
591
613
  let disposed = false;
614
+ let disposePending = false;
592
615
  let agentRunActive = false;
593
616
  let shutdownRequested = false;
594
617
  let server;
@@ -729,6 +752,168 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
729
752
  }
730
753
  broadcast(frame);
731
754
  };
755
+ /** Record and asynchronously summarize the maximal non-boundary region between
756
+ * visible prose messages. A boundary can land while a tool is still running,
757
+ * so closed groups retain their call ids until every result has joined the
758
+ * same serialized segment; only then may they become collapsible. */
759
+ const ensureOpenToolGroup = () => {
760
+ if (openToolGroup === undefined) {
761
+ openToolGroup = {
762
+ toolCount: 0,
763
+ pieces: [],
764
+ boundaryProse: lastToolGroupBoundaryProse,
765
+ pendingToolIds: new Set(),
766
+ closed: false,
767
+ summarizeWhenSettled: false,
768
+ };
769
+ }
770
+ return openToolGroup;
771
+ };
772
+ const generateSettledToolGroup = (group) => {
773
+ if (!group.closed || group.pendingToolIds.size > 0 || group.key === undefined || !group.summarizeWhenSettled)
774
+ return;
775
+ const key = group.key;
776
+ if (summaryAttempted.has(key) || generatingToolGroups.has(key))
777
+ return;
778
+ summaryAttempted.add(key);
779
+ generatingToolGroups.add(key);
780
+ let task;
781
+ task = headlessToolGroupSummary(serializeToolGroup(group.boundaryProse, group.pieces))
782
+ .then((summary) => {
783
+ if (summary === null)
784
+ return;
785
+ toolGroupSummaries.set(key, summary);
786
+ try {
787
+ atomicWriteJson(toolSummariesPath, Object.fromEntries(toolGroupSummaries));
788
+ }
789
+ catch {
790
+ // A delivery without durable state would disappear on reattach, so do
791
+ // not advertise it. The calls remain folded normally instead.
792
+ toolGroupSummaries.delete(key);
793
+ return;
794
+ }
795
+ broadcast({ type: 'tool_group_summary', key, summary });
796
+ })
797
+ .catch(() => undefined)
798
+ .finally(() => {
799
+ generatingToolGroups.delete(key);
800
+ summaryTasks.delete(task);
801
+ });
802
+ summaryTasks.add(task);
803
+ };
804
+ const finishOpenToolGroup = () => {
805
+ const group = openToolGroup;
806
+ openToolGroup = undefined;
807
+ if (group === undefined)
808
+ return;
809
+ group.closed = true;
810
+ // Configuration is read at the boundary, not when a slow tool later settles.
811
+ group.summarizeWhenSettled = readConfig('user').summarize_tool_calls;
812
+ generateSettledToolGroup(group);
813
+ };
814
+ const visibleBoundary = (prose) => {
815
+ finishOpenToolGroup();
816
+ lastToolGroupBoundaryProse = prose;
817
+ };
818
+ const observeToolGroupEvent = (event) => {
819
+ // The viewer turns every compaction_end into a summary, cancellation, or
820
+ // error line, each of which is a hard transcript seam.
821
+ if (event.type === 'compaction_end') {
822
+ finishOpenToolGroup();
823
+ return;
824
+ }
825
+ // A failed automatic retry is rendered as ChatView's visible error line.
826
+ // Successful retries only clear activity and remain within the same group.
827
+ if (event.type === 'auto_retry_end' && event.success !== true) {
828
+ finishOpenToolGroup();
829
+ return;
830
+ }
831
+ // The attach viewer renders aborted and errored assistant endings as visible
832
+ // transcript seams. Close their completed tool run here too; any tool that
833
+ // never emitted its execution_end keeps the group unsettled and visible.
834
+ if (event.type === 'message_end') {
835
+ const message = event.message;
836
+ if (message !== null && typeof message === 'object') {
837
+ const item = message;
838
+ if (item.role === 'assistant' && (item.stopReason === 'aborted' || item.stopReason === 'error')) {
839
+ finishOpenToolGroup();
840
+ }
841
+ }
842
+ return;
843
+ }
844
+ if (event.type === 'tool_execution_start') {
845
+ const group = ensureOpenToolGroup();
846
+ const id = typeof event.toolCallId === 'string' ? event.toolCallId : '';
847
+ if (group.key === undefined && id !== '')
848
+ group.key = id;
849
+ if (id !== '') {
850
+ group.pendingToolIds.add(id);
851
+ toolGroupsByCallId.set(id, group);
852
+ }
853
+ group.toolCount += 1;
854
+ group.pieces.push({ type: 'tool', name: typeof event.toolName === 'string' ? event.toolName : 'tool', args: event.args });
855
+ return;
856
+ }
857
+ if (event.type === 'tool_execution_end') {
858
+ const id = typeof event.toolCallId === 'string' ? event.toolCallId : '';
859
+ const group = id === '' ? openToolGroup : toolGroupsByCallId.get(id);
860
+ if (group !== undefined) {
861
+ group.pieces.push({
862
+ type: 'result',
863
+ name: typeof event.toolName === 'string' ? event.toolName : 'tool',
864
+ result: event.result,
865
+ isError: event.isError === true,
866
+ });
867
+ if (id !== '') {
868
+ group.pendingToolIds.delete(id);
869
+ toolGroupsByCallId.delete(id);
870
+ }
871
+ generateSettledToolGroup(group);
872
+ }
873
+ return;
874
+ }
875
+ if (event.type !== 'message_start' && event.type !== 'message_update')
876
+ return;
877
+ const message = event.message;
878
+ if (message === null || typeof message !== 'object')
879
+ return;
880
+ const item = message;
881
+ const role = item.role;
882
+ if (role === 'assistant') {
883
+ if (event.type === 'message_start')
884
+ sawAssistantProse = false;
885
+ const prose = assistantVisibleText(item);
886
+ if (!sawAssistantProse && prose.trim() !== '') {
887
+ sawAssistantProse = true;
888
+ visibleBoundary(prose);
889
+ }
890
+ return;
891
+ }
892
+ if (event.type !== 'message_start')
893
+ return;
894
+ if (role === 'compactionSummary' || role === 'branchSummary' || role === 'bashExecution' || role === 'status' || role === 'error' || item.customType === 'crtr-cycle-divider') {
895
+ finishOpenToolGroup();
896
+ return;
897
+ }
898
+ if (isTrueUserMessage(item)) {
899
+ visibleBoundary(generatedContextText(item));
900
+ return;
901
+ }
902
+ if (item.display === false)
903
+ return;
904
+ const text = generatedContextText(item);
905
+ const isBearingsOrRevive = item.customType === 'crtr-context' || (role === 'user' && text.startsWith('You have been revived fresh after a context refresh'));
906
+ if (isBearingsOrRevive) {
907
+ if (openToolGroup !== undefined && text !== '')
908
+ openToolGroup.pieces.push({ type: 'text', text });
909
+ return;
910
+ }
911
+ // Generated inbox/situational/nudge and visible custom output are part of
912
+ // the between-message activity and may begin a group before its first tool.
913
+ const group = ensureOpenToolGroup();
914
+ if (text !== '')
915
+ group.pieces.push({ type: 'text', text });
916
+ };
732
917
  // ---------------------------------------------------------------------------
733
918
  // F2: message_update coalescing. `relayEvent` is the single entry point for
734
919
  // engine events headed to viewers: a message_update is held (latest wins) and
@@ -951,9 +1136,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
951
1136
  // node from reviving straight back into fault-recovery chrome.
952
1137
  if (unconfigured)
953
1138
  clearFault(nodeId, { link: 'pi→provider' });
954
- await operationIdContext.fresh(() => session.prompt(unconfigured
955
- ? `The previous model (${currentSpec}) had no usable provider credential, so you were automatically switched to ${resolved}. Continue the task from where the failed turn left off.`
956
- : `The previous model (${currentSpec}) was unavailable (provider 404 not_found), so you were automatically switched to ${resolved}. Continue the task from where the failed turn left off.`));
1139
+ await operationIdContext.fresh(() => session.prompt(formatModelFallbackRecovery(currentSpec, resolved, unconfigured ? 'credential' : 'not-found')));
957
1140
  })()
958
1141
  .catch((err) => {
959
1142
  emitEvent({
@@ -1295,6 +1478,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
1295
1478
  widgets: Object.fromEntries([...displayWidgets].map(([k, w]) => [k, { lines: [...w.lines], placement: w.placement }])),
1296
1479
  title: displayTitle,
1297
1480
  },
1481
+ toolGroupSummaries: Object.fromEntries(toolGroupSummaries),
1298
1482
  });
1299
1483
  // Send a client its catch-up snapshot. welcome.pending_dialog carries a single
1300
1484
  // still-in-flight dialog to a WRITABLE client attaching mid-dialog; an observer
@@ -1337,7 +1521,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
1337
1521
  // The single exit helper (plan §0): dispose the engine, close + unlink the
1338
1522
  // socket, exit 0. Idempotent — every trigger converges here.
1339
1523
  // -------------------------------------------------------------------------
1340
- const disposeAndExit = (_reason, status = 0) => {
1524
+ const completeExit = (status) => {
1341
1525
  if (disposed)
1342
1526
  return;
1343
1527
  disposed = true;
@@ -1381,6 +1565,23 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
1381
1565
  }
1382
1566
  process.exit(status);
1383
1567
  };
1568
+ const disposeAndExit = (reason, status = 0) => {
1569
+ if (disposed)
1570
+ return;
1571
+ // A normal terminal shutdown follows a visible boundary that may have just
1572
+ // launched a capped (45s) headless summary. Keep the broker and its child
1573
+ // alive until those already-started tasks persist/broadcast, rather than
1574
+ // process.exit() orphaning pi and losing the sidecar. Abrupt failures and
1575
+ // signals retain their existing immediate teardown behavior.
1576
+ if ((reason === 'shutdown-hook' || reason === 'shutdown-after-settle') && summaryTasks.size > 0) {
1577
+ if (disposePending)
1578
+ return;
1579
+ disposePending = true;
1580
+ void Promise.allSettled([...summaryTasks]).finally(() => completeExit(status));
1581
+ return;
1582
+ }
1583
+ completeExit(status);
1584
+ };
1384
1585
  // A stophook can request shutdown while pi is still unwinding a completed
1385
1586
  // turn. Let that turn reach agent_settled first: inbox delivery commits its
1386
1587
  // durable cursor there, and only there. Explicit operator shutdown and OS
@@ -1491,6 +1692,7 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
1491
1692
  return;
1492
1693
  }
1493
1694
  }
1695
+ observeToolGroupEvent(event);
1494
1696
  relayEvent(event, generation);
1495
1697
  if (event.type === 'agent_settled')
1496
1698
  scheduleFaultRetry();
@@ -1526,6 +1728,9 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
1526
1728
  providerFallbackInFlight = null;
1527
1729
  notFoundFallbackInFlight = null;
1528
1730
  reloadAuthInFlight = null;
1731
+ openToolGroup = undefined;
1732
+ toolGroupsByCallId.clear();
1733
+ sawAssistantProse = false;
1529
1734
  await candidateSession.bindExtensions({
1530
1735
  uiContext,
1531
1736
  // 'rpc' — NOT 'print'. Per pi docs/rpc.md:1078 this is exactly our shape:
@@ -1810,6 +2015,9 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
1810
2015
  // Order before any held assistant message_update, so a `!` run mid-stream
1811
2016
  // never renders ahead of the update it follows.
1812
2017
  flushPendingUpdate();
2018
+ // The live bash component is a visible transcript boundary, even though
2019
+ // its persisted bashExecution message arrives later via the session.
2020
+ finishOpenToolGroup();
1813
2021
  broadcast({ type: 'bash_start', command, excludeFromContext: exclude });
1814
2022
  void session
1815
2023
  .executeBash(command, (chunk) => broadcast({ type: 'bash_output', chunk }), {
@@ -12,6 +12,9 @@ export declare const CANVAS_BASH_VALVE_PATH: string;
12
12
  * (`runtime/naming.ts`) to give it a constrained-schema `name_session` tool.
13
13
  * Deliberately absent from CANVAS_EXTENSIONS: a node must never carry it. */
14
14
  export declare const NAMING_TOOL_PATH: string;
15
+ /** NOT a canvas extension — loaded only by the broker's headless tool-group
16
+ * summarizer, which is the sole owner of summary generation and persistence. */
17
+ export declare const SUMMARY_TOOL_PATH: string;
15
18
  /** The canvas extensions every node loads, in order. The inbox watcher commits
16
19
  * its cursor at agent_settled before the stophook checks it for idle release. */
17
20
  export declare const CANVAS_EXTENSIONS: string[];
@@ -24,6 +24,9 @@ export const CANVAS_BASH_VALVE_PATH = resolveExtension('canvas-bash-valve');
24
24
  * (`runtime/naming.ts`) to give it a constrained-schema `name_session` tool.
25
25
  * Deliberately absent from CANVAS_EXTENSIONS: a node must never carry it. */
26
26
  export const NAMING_TOOL_PATH = resolveExtension('naming-tool');
27
+ /** NOT a canvas extension — loaded only by the broker's headless tool-group
28
+ * summarizer, which is the sole owner of summary generation and persistence. */
29
+ export const SUMMARY_TOOL_PATH = resolveExtension('summary-tool');
27
30
  /** The canvas extensions every node loads, in order. The inbox watcher commits
28
31
  * its cursor at agent_settled before the stophook checks it for idle release. */
29
32
  export const CANVAS_EXTENSIONS = [
@@ -151,7 +151,7 @@ export async function maybeBootRoot(argv) {
151
151
  // Unambiguous front-door launch → boot a resident root inline (exec pi in
152
152
  // this terminal). Does not return. Load the heavy boot-path deps lazily now
153
153
  // that we've committed to booting (see the import note at the top of file).
154
- const [{ bootRoot }, { resumeRoot }, { globalHumanloopWarning, missingPackageWarning, piAgentDir }] = await Promise.all([
154
+ const [{ bootRoot }, { resumeRoot }, { rendererWarning, missingPackageWarning, piAgentDir }] = await Promise.all([
155
155
  import('./boot-root.js'),
156
156
  import('./resume-root.js'),
157
157
  import('./package-health.js'),
@@ -164,9 +164,9 @@ export async function maybeBootRoot(argv) {
164
164
  const warn = missingPackageWarning(piAgentDir());
165
165
  if (warn !== null)
166
166
  process.stderr.write(`\n${warn}\n\n`);
167
- const humanloopWarn = globalHumanloopWarning();
168
- if (humanloopWarn !== null)
169
- process.stderr.write(`\n${humanloopWarn}\n\n`);
167
+ const rendererWarn = rendererWarning();
168
+ if (rendererWarn !== null)
169
+ process.stderr.write(`\n${rendererWarn}\n\n`);
170
170
  }
171
171
  catch {
172
172
  /* best-effort diagnostic — never block a boot */
@@ -1,6 +1,6 @@
1
1
  import { type NodeMeta } from '../canvas/index.js';
2
2
  import { type WakeOrigin } from './bearings.js';
3
- export { REVIVE_KICKOFF_SENTINEL } from '../../shared/generated-context.js';
3
+ export { REVIVE_KICKOFF_SENTINEL, RUNTIME_RESTART_CONTINUATION, } from '../../shared/generated-context.js';
4
4
  /** The goal file — the prompt/task a node was spawned with, persisted at birth
5
5
  * so a fresh revive can re-read its mandate. */
6
6
  export declare function goalPath(nodeId: string): string;
@@ -16,10 +16,6 @@ export declare function captureGoalIfAbsent(nodeId: string, text: string): boole
16
16
  /** Sentinel opening the fresh-revive kickoff message (see buildReviveKickoff).
17
17
  * The goal-capture extension and attached renderers share its pure definition,
18
18
  * so the same exact contract controls both prompt exclusion and display. */
19
- /** Injected only when a broker cleanly persisted an aborted in-flight turn
20
- * before a daemon restart. The transcript already carries the full picture, so
21
- * this is a bare nudge — a longer explanation only destabilizes the resume. */
22
- export declare const RUNTIME_RESTART_CONTINUATION = "continue";
23
19
  /** The yield-message file — a short note `crtr node yield` records for the next
24
20
  * fresh/cycling revive ("on wake, do X"). It remains durable through failed
25
21
  * launch attempts and is cleared only when session_start confirms a boot. */
@@ -25,7 +25,7 @@ import { buildWakeBearings } from './bearings.js';
25
25
  import { personaDrift, commitPersonaAck } from './persona.js';
26
26
  import { readInboxSince, readCursor, writeCursor, coalesce, } from '../feed/inbox.js';
27
27
  import { REVIVE_KICKOFF_SENTINEL } from '../../shared/generated-context.js';
28
- export { REVIVE_KICKOFF_SENTINEL } from '../../shared/generated-context.js';
28
+ export { REVIVE_KICKOFF_SENTINEL, RUNTIME_RESTART_CONTINUATION, } from '../../shared/generated-context.js';
29
29
  // ---------------------------------------------------------------------------
30
30
  // Companion context files: the goal (the spawning mandate) and the one-shot
31
31
  // yield message (a note from the prior self to the revived self).
@@ -65,10 +65,6 @@ export function captureGoalIfAbsent(nodeId, text) {
65
65
  /** Sentinel opening the fresh-revive kickoff message (see buildReviveKickoff).
66
66
  * The goal-capture extension and attached renderers share its pure definition,
67
67
  * so the same exact contract controls both prompt exclusion and display. */
68
- /** Injected only when a broker cleanly persisted an aborted in-flight turn
69
- * before a daemon restart. The transcript already carries the full picture, so
70
- * this is a bare nudge — a longer explanation only destabilizes the resume. */
71
- export const RUNTIME_RESTART_CONTINUATION = 'continue';
72
68
  /** The yield-message file — a short note `crtr node yield` records for the next
73
69
  * fresh/cycling revive ("on wake, do X"). It remains durable through failed
74
70
  * launch attempts and is cleared only when session_start confirms a boot. */