@north-light/crouter 0.3.200 → 0.3.201

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 (34) hide show
  1. package/dist/api/client.d.ts +2 -1
  2. package/dist/api/client.js +3 -0
  3. package/dist/api/dto/inbox.d.ts +1 -1
  4. package/dist/api/dto/nodes.d.ts +16 -0
  5. package/dist/clients/attach/ansi-cells.d.ts +13 -4
  6. package/dist/clients/attach/ansi-cells.js +16 -10
  7. package/dist/clients/attach/overlays/help.js +6 -0
  8. package/dist/clients/attach/render/transcript-search.d.ts +28 -0
  9. package/dist/clients/attach/render/transcript-search.js +153 -0
  10. package/dist/clients/attach/render/viewport.d.ts +10 -1
  11. package/dist/clients/attach/render/viewport.js +27 -2
  12. package/dist/clients/attach/session/frame.d.ts +2 -1
  13. package/dist/clients/attach/session/frame.js +9 -0
  14. package/dist/clients/attach/session/keys.d.ts +5 -0
  15. package/dist/clients/attach/session/keys.js +3 -0
  16. package/dist/clients/attach/session/layout.d.ts +2 -2
  17. package/dist/clients/attach/session/layout.js +3 -2
  18. package/dist/clients/attach/session/transcript-search.d.ts +20 -0
  19. package/dist/clients/attach/session/transcript-search.js +151 -0
  20. package/dist/clients/attach/viewer.js +742 -740
  21. package/dist/core/__tests__/seam/dormancy-release.test.js +2 -0
  22. package/dist/core/human/scan.d.ts +1 -3
  23. package/dist/core/human/scan.js +3 -3
  24. package/dist/core/human/types.d.ts +1 -0
  25. package/dist/core/keybindings/attach-control.d.ts +3 -0
  26. package/dist/core/keybindings/attach-control.js +1 -0
  27. package/dist/core/keybindings/catalog.d.ts +2 -2
  28. package/dist/core/keybindings/catalog.js +6 -0
  29. package/dist/core/runtime/node-read.d.ts +11 -0
  30. package/dist/core/runtime/node-read.js +107 -41
  31. package/dist/daemon/api/handlers/inbox.js +1 -0
  32. package/dist/daemon/api/handlers/nodes.js +34 -1
  33. package/package.json +1 -1
  34. package/runtime.lock.json +2 -2
@@ -93,6 +93,7 @@ test('idle-release settles its real watcher before exit; daemon revives exactly
93
93
  // settles its own prompt lifecycle: watcher → engine → stophook → settle →
94
94
  // broker exit. No test-driven turn or settlement dispatch is involved.
95
95
  await awaitReleased(manager);
96
+ await h.awaitFleetExit(manager);
96
97
  assert.equal(readCursor(manager), first.entry_id, 'the exact handed-off row commits before clean shutdown');
97
98
  const cyclesAfterRelease = h.node(manager).cycles ?? 0;
98
99
  await h.tick();
@@ -112,6 +113,7 @@ test('idle-release settles its real watcher before exit; daemon revives exactly
112
113
  await h.awaitBoot(manager, { minCount: 2 });
113
114
  await h.awaitWake(manager, { sinceCount: injectedBeforeSecond, match: /second completed/ });
114
115
  await awaitReleased(manager);
116
+ await h.awaitFleetExit(manager);
115
117
  assert.equal(readCursor(manager), second.entry_id, 'the later row commits at its own natural idle release');
116
118
  await h.tick();
117
119
  assert.equal(h.node(manager).cycles ?? 0, cyclesAfterRelease + 1, 'the settled later row does not cause a duplicate revival');
@@ -1,9 +1,7 @@
1
1
  import type { PageTicketSummary, TicketSummary } from './types.js';
2
2
  /** Read all unresolved page/review tickets, newest first. Progress never affects visibility. */
3
3
  export declare function scanInbox(): TicketSummary[];
4
- export interface PageHistoryEntry extends PageTicketSummary {
5
- state: 'pending' | 'resolved' | 'canceled' | 'passive';
6
- }
4
+ export type PageHistoryEntry = PageTicketSummary;
7
5
  /**
8
6
  * Every page ticket raised by one node — pending, resolved or canceled — oldest
9
7
  * first. This is the durable discovery seam for transcript placement, so it is a
@@ -56,6 +56,7 @@ function pageSummary(dir, id) {
56
56
  awaitsResponse: manifest.delivery.reply,
57
57
  source: manifest.source ?? {},
58
58
  emittedAt,
59
+ state: pageTicketState(dir),
59
60
  claim: claimSummary(dir),
60
61
  };
61
62
  }
@@ -112,9 +113,8 @@ export function scanPageHistory(nodeId) {
112
113
  const summary = pageSummary(dir, basename(dir));
113
114
  if (summary === null || summary.source.nodeId !== nodeId)
114
115
  continue;
115
- const state = pageTicketState(dir);
116
- const result = state === 'resolved' ? readTicketResult(dir) : null;
117
- items.push({ ...summary, state, ...(result?.kind === 'page' ? { answerDigest: result.summary.replace(/\s+/g, ' ').trim() } : {}) });
116
+ const result = summary.state === 'resolved' ? readTicketResult(dir) : null;
117
+ items.push({ ...summary, ...(result?.kind === 'page' ? { answerDigest: result.summary.replace(/\s+/g, ' ').trim() } : {}) });
118
118
  }
119
119
  return items.sort((a, b) => Date.parse(a.emittedAt) - Date.parse(b.emittedAt) || a.id.localeCompare(b.id));
120
120
  }
@@ -65,6 +65,7 @@ export interface PageTicketSummary {
65
65
  steps: number;
66
66
  slotKinds: string[];
67
67
  awaitsResponse: boolean;
68
+ state: 'pending' | 'resolved' | 'canceled' | 'passive';
68
69
  }
69
70
  /** The only pending-ticket shape scanners expose. */
70
71
  export type TicketSummary = ReviewTicketSummary | PageTicketSummary;
@@ -28,6 +28,9 @@ export declare const ATTACH_CONTROL_BINDINGS: readonly [{
28
28
  }, {
29
29
  readonly menuId: "crtr.tmux.menu.attach.profile-files";
30
30
  readonly actionId: "crtr.attach.profile-files";
31
+ }, {
32
+ readonly menuId: "crtr.tmux.menu.attach.search";
33
+ readonly actionId: "crtr.attach.search";
31
34
  }, {
32
35
  readonly menuId: "crtr.tmux.menu.attach.whip";
33
36
  readonly actionId: "crtr.attach.whip";
@@ -9,6 +9,7 @@ export const ATTACH_CONTROL_BINDINGS = [
9
9
  { menuId: 'crtr.tmux.menu.attach.command.inspect', actionId: 'crtr.attach.command.inspect' },
10
10
  { menuId: 'crtr.tmux.menu.attach.file-review', actionId: 'crtr.attach.file-review' },
11
11
  { menuId: 'crtr.tmux.menu.attach.profile-files', actionId: 'crtr.attach.profile-files' },
12
+ { menuId: 'crtr.tmux.menu.attach.search', actionId: 'crtr.attach.search' },
12
13
  { menuId: 'crtr.tmux.menu.attach.whip', actionId: 'crtr.attach.whip' },
13
14
  ];
14
15
  const ATTACH_CONTROL_PREFIX = '\x1b_crtr;';
@@ -8,8 +8,8 @@ type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' |
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
- 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' | 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.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.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">];
11
+ export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | AttachMenuBindingId | `crtr.tmux.menu-global.${TmuxMenuEntryAction}` | `crtr.tmux.node.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.scroll.${'up' | 'down' | 'page-up' | 'page-down' | 'live'}` | 'crtr.attach.mouse.toggle' | 'crtr.attach.copy.message' | 'crtr.attach.help.toggle' | `crtr.attach.search.${'next' | 'previous' | 'edit' | 'close'}` | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.inspector.${'force-quit' | 'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
12
+ export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.attach.search.next"> | BindingDefinition<"crtr.attach.search.previous"> | BindingDefinition<"crtr.attach.search.edit"> | BindingDefinition<"crtr.attach.search.close"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.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. */
@@ -77,6 +77,7 @@ export const BINDING_CATALOG = Object.freeze([
77
77
  ...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']),
78
78
  ...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.', [], ['alt+shift+r']),
79
79
  ...attachEntry('crtr.tmux.menu.attach.profile-files', 'crtr.attach.profile-files', 'Search profile files', 'Search files across the node’s profile projects, profile memory, cwd, and ancestor crouter memory stores.', ['f'], ['ctrl+f']),
80
+ ...attachEntry('crtr.tmux.menu.attach.search', 'crtr.attach.search', 'Search transcript', 'Search the transcript as it is currently displayed — folded tool output is not searched until it is unfolded.', ['/'], ['alt+/']),
80
81
  ...attachEntry('crtr.tmux.menu.attach.whip', 'crtr.attach.whip', 'Whip agent', 'Interrupt the agent and send a playful command.', ['w'], []),
81
82
  b('crtr.attach.scroll.up', 'Attach', 'Scroll up', 'Scroll the transcript one line up.', ['shift+up'], 'terminal', ATTACH_CONTEXTS),
82
83
  b('crtr.attach.scroll.down', 'Attach', 'Scroll down', 'Scroll the transcript one line down.', ['shift+down'], 'terminal', ATTACH_CONTEXTS),
@@ -85,6 +86,10 @@ export const BINDING_CATALOG = Object.freeze([
85
86
  b('crtr.attach.scroll.live', 'Attach', 'Return to live', 'Return the transcript to the live bottom.', ['shift+end'], 'terminal', ATTACH_CONTEXTS),
86
87
  b('crtr.attach.mouse.toggle', 'Attach', 'Toggle mouse wheel scrolling', 'Toggle mouse wheel scrolling on or off.', ['alt+shift+o'], 'terminal', ATTACH_CONTEXTS),
87
88
  b('crtr.attach.copy.message', 'Attach', 'Copy visible message', 'Copy the newest message visible in the transcript viewport.', ['alt+shift+y'], 'terminal', ATTACH_CONTEXTS),
89
+ b('crtr.attach.search.next', 'Attach', 'Next match', 'Move to the next transcript search match.', ['n'], 'terminal', ['attach.search']),
90
+ b('crtr.attach.search.previous', 'Attach', 'Previous match', 'Move to the previous transcript search match.', ['shift+n'], 'terminal', ['attach.search']),
91
+ b('crtr.attach.search.edit', 'Attach', 'Edit query', 'Return to editing the transcript search query.', ['/'], 'terminal', ['attach.search']),
92
+ b('crtr.attach.search.close', 'Attach', 'Close search', 'Leave transcript search and clear its highlights.', ['escape'], 'terminal', ['attach.search'], 'dismiss-modal'),
88
93
  b('crtr.graph.down', 'GRAPH', 'Move down', 'Move to the next graph row.', ['j'], 'terminal', ['graph']),
89
94
  b('crtr.graph.up', 'GRAPH', 'Move up', 'Move to the previous graph row.', ['k'], 'terminal', ['graph']),
90
95
  b('crtr.graph.first', 'GRAPH', 'First row', 'Move to the first graph row.', ['g'], 'terminal', ['graph']),
@@ -219,6 +224,7 @@ export const SAFETY_REQUIREMENTS = Object.freeze([
219
224
  { context: 'graph.confirm', capability: 'dismiss-modal' },
220
225
  { context: 'inspector', capability: 'leave-screen' },
221
226
  { context: 'inspector', capability: 'dismiss-modal' },
227
+ { context: 'attach.search', capability: 'dismiss-modal' },
222
228
  { context: 'browse', capability: 'leave-screen' },
223
229
  { context: 'browse.search', capability: 'dismiss-modal' },
224
230
  { context: 'profile.menu', capability: 'leave-screen' },
@@ -28,9 +28,20 @@ export interface NodeSnapshotRead {
28
28
  source: 'builtin';
29
29
  }>;
30
30
  }
31
+ export interface NodeMessagesPageRead {
32
+ messages: unknown[];
33
+ nextCursor: string | null;
34
+ }
35
+ export interface NodeMessagesPageOptions {
36
+ cursor?: string;
37
+ limit?: number;
38
+ }
31
39
  /** Reconstruct a persisted session without launching its broker. Backs the
32
40
  * machine-readable snapshot endpoint and the human transcript render. */
33
41
  export declare function readNodeSnapshot(nodeId: string): Promise<NodeSnapshotRead>;
42
+ /** A backward-paged, chronological window over the same visible history as a
43
+ * snapshot. `nextCursor` walks toward older messages. */
44
+ export declare function readNodeMessagesPage(nodeId: string, { cursor, limit }: NodeMessagesPageOptions): Promise<NodeMessagesPageRead>;
34
45
  /** Human-readable history only: text messages plus terse tool records, never
35
46
  * private model reasoning. */
36
47
  export declare function transcriptMarkdown(nodeId: string): Promise<string>;
@@ -135,9 +135,10 @@ function readToolGroupSummaries(nodeId) {
135
135
  return {};
136
136
  }
137
137
  }
138
- /** Reconstruct a persisted session without launching its broker. Backs the
139
- * machine-readable snapshot endpoint and the human transcript render. */
140
- export async function readNodeSnapshot(nodeId) {
138
+ /** Reconstruct the visible message history from a stable at-rest session copy.
139
+ * Both snapshot and paged reads use this one path so their histories cannot
140
+ * diverge. */
141
+ async function reconstructVisibleMessages(nodeId) {
141
142
  const node = getNode(nodeId);
142
143
  if (node === null) {
143
144
  throw new InputError({ error: 'not_found', message: `no node: ${nodeId}`, field: 'id', next: 'List nodes with `crtr node inspect list`.' });
@@ -146,57 +147,122 @@ export async function readNodeSnapshot(nodeId) {
146
147
  if (sessionFile === null) {
147
148
  throw new InputError({ error: 'no_session', message: `node has no readable session file: ${nodeId}`, field: 'id', next: 'Choose a node with captured conversation history.' });
148
149
  }
149
- const tempDir = mkdtempSync(join(tmpdir(), 'crtr-node-snapshot-'));
150
+ const tempDir = mkdtempSync(join(tmpdir(), 'crtr-node-session-'));
150
151
  try {
151
152
  const copy = join(tempDir, basename(sessionFile));
152
153
  copyFileSync(sessionFile, copy);
153
154
  const manager = SessionManager.open(copy);
154
155
  const context = manager.buildSessionContext();
155
156
  const messages = visibleMessages(cycleAwareMessages(manager), { boundaryReviewId: node.review_binding?.review_id });
156
- const sessionId = manager.getSessionId();
157
- const sessionName = manager.getSessionName();
158
- const snapshot = {
159
- messages,
160
- stats: statsFromMessages(messages, sessionId, sessionFile),
161
- // pi's `get_state` shape (RpcSessionState), reconstructed offline: the
162
- // live-only fields report their at-rest values (nothing is streaming or
163
- // queued for a node with no broker).
164
- state: {
165
- model: await resolveRecordedModel(context.model),
166
- thinkingLevel: context.thinkingLevel,
167
- isStreaming: false,
168
- isCompacting: false,
169
- steeringMode: 'one-at-a-time',
170
- followUpMode: 'one-at-a-time',
171
- sessionFile,
172
- sessionId,
173
- sessionName,
174
- autoCompactionEnabled: true,
175
- messageCount: messages.length,
176
- pendingMessageCount: 0,
177
- },
178
- // A snapshot reconstructed from the .jsonl alone: display state is broker
179
- // process state (last setStatus/setWidget/setTitle) and is not persisted,
180
- // so an offline read has none.
181
- display: { statuses: {}, widgets: {}, title: undefined },
182
- // The unrun queue lives in the engine's memory; a dormant node has no
183
- // engine, so nothing is waiting to run.
184
- queued: { steering: [], followUp: [] },
185
- // Sidecar summaries survive broker exit so a dormant transcript receives
186
- // the same completed-group data as a live welcome snapshot. Malformed or
187
- // absent sidecars are deliberately ignored: tool calls remain visible.
188
- toolGroupSummaries: readToolGroupSummaries(nodeId),
189
- };
190
157
  return {
191
- nodeId,
192
- snapshot,
193
- commands: BUILTIN_SLASH_COMMANDS.map((c) => ({ ...c, source: 'builtin' })),
158
+ sessionFile,
159
+ messages,
160
+ model: context.model,
161
+ thinkingLevel: context.thinkingLevel,
162
+ sessionId: manager.getSessionId(),
163
+ sessionName: manager.getSessionName(),
194
164
  };
195
165
  }
196
166
  finally {
197
167
  rmSync(tempDir, { recursive: true, force: true });
198
168
  }
199
169
  }
170
+ function invalidCursor() {
171
+ throw new InputError({
172
+ error: 'invalid_cursor',
173
+ message: 'cursor must be a base64url-encoded messages cursor.',
174
+ field: 'cursor',
175
+ next: 'Omit cursor to start from the newest messages.',
176
+ });
177
+ }
178
+ function decodeMessagesCursor(cursor, messagesLength) {
179
+ if (!/^[A-Za-z0-9_-]+$/u.test(cursor))
180
+ invalidCursor();
181
+ let parsed;
182
+ try {
183
+ const json = Buffer.from(cursor, 'base64url').toString('utf8');
184
+ if (Buffer.from(json, 'utf8').toString('base64url') !== cursor)
185
+ invalidCursor();
186
+ parsed = JSON.parse(json);
187
+ }
188
+ catch {
189
+ invalidCursor();
190
+ }
191
+ if (typeof parsed !== 'object'
192
+ || parsed === null
193
+ || parsed.v !== 1
194
+ || !Number.isSafeInteger(parsed.before)) {
195
+ invalidCursor();
196
+ }
197
+ const before = parsed.before;
198
+ if (before < 0 || before > messagesLength)
199
+ invalidCursor();
200
+ return before;
201
+ }
202
+ function encodeMessagesCursor(before) {
203
+ return Buffer.from(JSON.stringify({ v: 1, before }), 'utf8').toString('base64url');
204
+ }
205
+ /** Reconstruct a persisted session without launching its broker. Backs the
206
+ * machine-readable snapshot endpoint and the human transcript render. */
207
+ export async function readNodeSnapshot(nodeId) {
208
+ const read = await reconstructVisibleMessages(nodeId);
209
+ const snapshot = {
210
+ messages: read.messages,
211
+ stats: statsFromMessages(read.messages, read.sessionId, read.sessionFile),
212
+ // pi's `get_state` shape (RpcSessionState), reconstructed offline: the
213
+ // live-only fields report their at-rest values (nothing is streaming or
214
+ // queued for a node with no broker).
215
+ state: {
216
+ model: await resolveRecordedModel(read.model),
217
+ thinkingLevel: read.thinkingLevel,
218
+ isStreaming: false,
219
+ isCompacting: false,
220
+ steeringMode: 'one-at-a-time',
221
+ followUpMode: 'one-at-a-time',
222
+ sessionFile: read.sessionFile,
223
+ sessionId: read.sessionId,
224
+ sessionName: read.sessionName,
225
+ autoCompactionEnabled: true,
226
+ messageCount: read.messages.length,
227
+ pendingMessageCount: 0,
228
+ },
229
+ // A snapshot reconstructed from the .jsonl alone: display state is broker
230
+ // process state (last setStatus/setWidget/setTitle) and is not persisted,
231
+ // so an offline read has none.
232
+ display: { statuses: {}, widgets: {}, title: undefined },
233
+ // The unrun queue lives in the engine's memory; a dormant node has no
234
+ // engine, so nothing is waiting to run.
235
+ queued: { steering: [], followUp: [] },
236
+ // Sidecar summaries survive broker exit so a dormant transcript receives
237
+ // the same completed-group data as a live welcome snapshot. Malformed or
238
+ // absent sidecars are deliberately ignored: tool calls remain visible.
239
+ toolGroupSummaries: readToolGroupSummaries(nodeId),
240
+ };
241
+ return {
242
+ nodeId,
243
+ snapshot,
244
+ commands: BUILTIN_SLASH_COMMANDS.map((c) => ({ ...c, source: 'builtin' })),
245
+ };
246
+ }
247
+ /** A backward-paged, chronological window over the same visible history as a
248
+ * snapshot. `nextCursor` walks toward older messages. */
249
+ export async function readNodeMessagesPage(nodeId, { cursor, limit = 200 }) {
250
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
251
+ throw new InputError({
252
+ error: 'invalid_limit',
253
+ message: 'limit must be an integer from 1 through 500.',
254
+ field: 'limit',
255
+ next: 'Use a limit from 1 through 500.',
256
+ });
257
+ }
258
+ const { messages } = await reconstructVisibleMessages(nodeId);
259
+ const before = cursor === undefined ? messages.length : decodeMessagesCursor(cursor, messages.length);
260
+ const start = Math.max(0, before - limit);
261
+ return {
262
+ messages: messages.slice(start, before),
263
+ nextCursor: start === 0 ? null : encodeMessagesCursor(start),
264
+ };
265
+ }
200
266
  function textFromContent(content) {
201
267
  if (typeof content === 'string')
202
268
  return content;
@@ -187,6 +187,7 @@ function handleList() {
187
187
  slot_kinds: item.slotKinds,
188
188
  inbox: item.inbox,
189
189
  awaits_response: item.awaitsResponse,
190
+ state: item.state,
190
191
  };
191
192
  if (item.subtitle !== '')
192
193
  dto.subtitle = item.subtitle;
@@ -14,7 +14,7 @@ import { childrenOf, subscribersOf, subscriptionsOf, setMessageWait, updateNode
14
14
  import { subtreeIds } from '../../../core/canvas/nav-model.js';
15
15
  import { contextDir, reportsDir } from '../../../core/canvas/paths.js';
16
16
  import { nodeArtifacts } from '../../../core/canvas/history.js';
17
- import { readNodeSession, readNodeSnapshot, transcriptMarkdown } from '../../../core/runtime/node-read.js';
17
+ import { readNodeMessagesPage, readNodeSession, readNodeSnapshot, transcriptMarkdown } from '../../../core/runtime/node-read.js';
18
18
  import { reviveAll } from '../../../core/runtime/revive-all.js';
19
19
  import { promote, requestYield, reshapeNode } from '../../../core/runtime/promote.js';
20
20
  import { recycleNode } from '../../../core/runtime/recycle.js';
@@ -301,6 +301,38 @@ async function handleSnapshot(ctx) {
301
301
  };
302
302
  return { status: 200, body };
303
303
  }
304
+ function parseMessagesQuery(ctx) {
305
+ for (const [field] of ctx.query) {
306
+ if (field !== 'cursor' && field !== 'limit')
307
+ throw usage(`unknown messages query parameter: ${field}`);
308
+ }
309
+ const cursors = ctx.query.getAll('cursor');
310
+ const limits = ctx.query.getAll('limit');
311
+ if (cursors.length > 1)
312
+ throw usage('cursor must appear at most once');
313
+ if (limits.length > 1)
314
+ throw usage('limit must appear at most once');
315
+ if (limits.length === 0)
316
+ return cursors.length === 0 ? {} : { cursor: cursors[0] };
317
+ const rawLimit = limits[0];
318
+ const limit = Number(rawLimit);
319
+ if (!Number.isSafeInteger(limit) || rawLimit === '') {
320
+ throw usage('limit must be an integer from 1 through 500');
321
+ }
322
+ return cursors.length === 0 ? { limit } : { cursor: cursors[0], limit };
323
+ }
324
+ /** Offline, backward-paged read of the cycle-flattened visible history. */
325
+ async function handleMessages(ctx) {
326
+ const id = ctx.params['id'];
327
+ const read = await readNodeMessagesPage(id, parseMessagesQuery(ctx));
328
+ const body = {
329
+ node_id: id,
330
+ messages: read.messages,
331
+ next_cursor: read.nextCursor,
332
+ captured_at: nowIso(),
333
+ };
334
+ return { status: 200, body };
335
+ }
304
336
  /** The faithful export read: raw `.jsonl` bytes + assembled system prompt.
305
337
  * `/snapshot` reconstructs a cycle-flattened `AgentMessage[]` for renderers and
306
338
  * drops what a renderer cannot show; this drops nothing. */
@@ -598,6 +630,7 @@ export const nodeRoutes = [
598
630
  { method: 'POST', pattern: '/v1/nodes/revive-all', handler: () => handleReviveAll() },
599
631
  { method: 'GET', pattern: '/v1/nodes/:id', handler: handleDetail },
600
632
  { method: 'GET', pattern: '/v1/nodes/:id/snapshot', handler: handleSnapshot },
633
+ { method: 'GET', pattern: '/v1/nodes/:id/messages', handler: handleMessages },
601
634
  { method: 'GET', pattern: '/v1/nodes/:id/session', handler: handleSession },
602
635
  { method: 'GET', pattern: '/v1/nodes/:id/transcript', handler: handleTranscript },
603
636
  { method: 'GET', pattern: '/v1/nodes/:id/context', handler: handleContext },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.200",
3
+ "version": "0.3.201",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.200",
3
+ "version": "0.3.201",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.200",
9
+ "version": "0.3.201",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {