@pellux/goodvibes-tui 1.9.1 → 1.10.0

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 (91) hide show
  1. package/CHANGELOG.md +36 -5
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/knowledge-graphql.graphql +5 -0
  4. package/docs/foundation-artifacts/knowledge-store.sql +33 -0
  5. package/docs/foundation-artifacts/operator-contract.json +4026 -1376
  6. package/package.json +2 -2
  7. package/src/audio/player.ts +91 -5
  8. package/src/audio/spoken-turn-wiring.ts +10 -2
  9. package/src/cli/management-utils.ts +4 -0
  10. package/src/config/credential-availability.ts +1 -1
  11. package/src/config/index.ts +1 -1
  12. package/src/core/turn-cancellation.ts +1 -1
  13. package/src/daemon/cli.ts +4 -4
  14. package/src/daemon/service-commands.ts +8 -8
  15. package/src/export/cost-utils.ts +9 -1
  16. package/src/input/command-registry.ts +16 -5
  17. package/src/input/commands/config.ts +1 -1
  18. package/src/input/commands/control-room-runtime.ts +12 -5
  19. package/src/input/commands/incident-runtime.ts +2 -2
  20. package/src/input/commands/memory.ts +12 -12
  21. package/src/input/commands/planning-runtime.ts +1 -1
  22. package/src/input/commands/recall-bundle.ts +8 -8
  23. package/src/input/commands/recall-capture.ts +3 -3
  24. package/src/input/commands/recall-query.ts +41 -15
  25. package/src/input/commands/recall-review.ts +12 -12
  26. package/src/input/commands/session-content.ts +1 -1
  27. package/src/input/feed-context-factory.ts +1 -1
  28. package/src/input/handler-content-actions.ts +2 -2
  29. package/src/input/handler-feed-routes.ts +2 -2
  30. package/src/input/handler-feed.ts +1 -1
  31. package/src/input/handler-onboarding-daemon-adopt.ts +3 -3
  32. package/src/input/handler-picker-routes.ts +2 -2
  33. package/src/input/handler-shortcuts.ts +1 -1
  34. package/src/input/model-picker.ts +2 -2
  35. package/src/input/onboarding/onboarding-wizard-apply.ts +3 -3
  36. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +3 -3
  37. package/src/input/onboarding/onboarding-wizard-steps.ts +1 -1
  38. package/src/input/session-picker-modal.ts +1 -1
  39. package/src/input/settings-modal-data.ts +56 -2
  40. package/src/main.ts +8 -6
  41. package/src/panels/base-panel.ts +1 -1
  42. package/src/panels/builtin/operations.ts +4 -4
  43. package/src/panels/builtin/shared.ts +4 -4
  44. package/src/panels/cost-tracker-panel.ts +2 -2
  45. package/src/panels/fleet-panel-format.ts +1 -1
  46. package/src/panels/fleet-panel.ts +15 -15
  47. package/src/panels/fleet-read-model.ts +11 -11
  48. package/src/panels/fleet-steer.ts +2 -2
  49. package/src/panels/fleet-stop.ts +2 -2
  50. package/src/panels/fleet-tabs.ts +4 -4
  51. package/src/panels/fleet-transcript.ts +5 -5
  52. package/src/panels/modals/memory-modal.ts +77 -16
  53. package/src/panels/types.ts +1 -1
  54. package/src/renderer/compaction-quality.ts +1 -1
  55. package/src/renderer/fleet-tab-strip.ts +2 -2
  56. package/src/renderer/footer-tips.ts +1 -1
  57. package/src/renderer/model-workspace.ts +1 -1
  58. package/src/renderer/session-picker-modal.ts +4 -4
  59. package/src/renderer/shell-surface.ts +1 -1
  60. package/src/renderer/status-glyphs.ts +3 -3
  61. package/src/renderer/terminal-bg-probe.ts +1 -1
  62. package/src/renderer/theme.ts +2 -2
  63. package/src/renderer/turn-injection.ts +3 -3
  64. package/src/renderer/ui-factory.ts +7 -7
  65. package/src/renderer/ui-primitives.ts +3 -3
  66. package/src/runtime/bootstrap-command-context.ts +4 -0
  67. package/src/runtime/bootstrap-command-parts.ts +4 -1
  68. package/src/runtime/bootstrap-core.ts +3 -3
  69. package/src/runtime/bootstrap-hook-bridge.ts +2 -2
  70. package/src/runtime/bootstrap-shell.ts +18 -7
  71. package/src/runtime/bootstrap.ts +22 -22
  72. package/src/runtime/code-index-services.ts +2 -2
  73. package/src/runtime/index.ts +11 -3
  74. package/src/runtime/legacy-daemon-migration.ts +9 -9
  75. package/src/runtime/memory-fold.ts +1 -1
  76. package/src/runtime/memory-spine-transport.ts +289 -0
  77. package/src/runtime/onboarding/snapshot.ts +3 -3
  78. package/src/runtime/onboarding/types.ts +2 -2
  79. package/src/runtime/operator-token-cleanup.ts +2 -2
  80. package/src/runtime/orchestrator-core-services.ts +29 -4
  81. package/src/runtime/process-lifecycle.ts +19 -5
  82. package/src/runtime/services.ts +27 -28
  83. package/src/runtime/session-inbound-inputs.ts +1 -1
  84. package/src/runtime/session-spine-transport.ts +2 -2
  85. package/src/runtime/ui-services.ts +1 -1
  86. package/src/runtime/workstream-services.ts +1 -1
  87. package/src/runtime/wrfc-persistence.ts +2 -2
  88. package/src/shell/ui-openers.ts +2 -2
  89. package/src/version.ts +1 -1
  90. package/src/audio/spoken-turn-controller.ts +0 -243
  91. package/src/audio/text-chunker.ts +0 -110
@@ -38,7 +38,7 @@ export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBu
38
38
  // process registry constructed once in runtime/services.ts (shared with
39
39
  // every other consumer rather than duplicated here; the registry owns its
40
40
  // own coalesced tick, so no manual lifecycle-event wiring is needed).
41
- // Wave-3 (W3.2): runtimeBus is also passed so the read model can subscribe
41
+ // runtimeBus is also passed so the read model can subscribe
42
42
  // to the honest COMMUNICATION_CONSUMED steer-ack signal (fleet-read-model.ts).
43
43
  const fleetReadModel = createFleetReadModel(ui.runtime.processRegistry, ui.runtime.runtimeBus);
44
44
 
@@ -51,16 +51,16 @@ export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBu
51
51
  description: 'Live unified process tree: agents, WRFC chains, workflows, watchers, and background processes, with interrupt/kill/steer controls',
52
52
  factory: () => new FleetPanel(fleetReadModel, {
53
53
  interrupt: (id: string) => fleetReadModel.interrupt(id),
54
- // Wave-6 (wo-F item d2): re-arm a paused trigger/schedule/automation job.
54
+ // Re-arm a paused trigger/schedule/automation job.
55
55
  resume: (id: string) => fleetReadModel.resume(id),
56
56
  kill: (id: string, opts: { cascade: boolean }) => fleetReadModel.kill(id, opts),
57
- // Wave-3 (C6): full-fidelity transcript source for an attached agent
57
+ // Full-fidelity transcript source for an attached agent
58
58
  // tab — live while the agent runs, frozen briefly after it completes
59
59
  // (AgentManager's bounded retention ring), empty once evicted (the
60
60
  // panel degrades to the on-disk ledger fallback in that case).
61
61
  getConversationSnapshot: (agentId: string) => ui.agents.agentManager.getConversationSnapshot(agentId),
62
62
  resolveSessionLogPath: (agentId: string) => ui.environment.shellPaths.resolveProjectPath('tui', 'sessions', `${agentId}.jsonl`),
63
- // Wave-3 (W3.2): queue a message for a live in-process agent/wrfc-subtask member.
63
+ // Queue a message for a live in-process agent/wrfc-subtask member.
64
64
  steer: (id: string, text: string) => fleetReadModel.steer(id, text),
65
65
  }, deps.configManager),
66
66
  });
@@ -3,7 +3,7 @@ import type { ServiceRegistry } from '@pellux/goodvibes-sdk/platform/config';
3
3
  import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
4
4
  import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
5
5
  import type { Orchestrator } from '../../core/orchestrator';
6
- import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
6
+ import type { MemoryAccess } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
7
7
  import type { ApprovalBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
8
8
  import type { SessionReadFacade } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
9
9
  import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
@@ -79,14 +79,14 @@ export interface BuiltinPanelDeps {
79
79
  * copy via CommandContext.extensions.evalRegistry).
80
80
  */
81
81
  evalRegistry?: import('../eval-registry.ts').EvalRegistry;
82
- /** MemoryRegistry for the Memory panel. */
83
- memoryRegistry?: MemoryRegistry;
82
+ /** Host-vs-client memory access for the Memory modal — the spine client, never the raw registry (routes over the wire when a daemon is adopted). */
83
+ memoryRegistry?: MemoryAccess;
84
84
  /** Shared policy runtime state for governance/policy diagnostics. */
85
85
  policyRuntimeState?: import('@/runtime/index.ts').PolicyRuntimeState;
86
86
  /** Approval broker for control-plane/operator panels. */
87
87
  approvalBroker?: ApprovalBroker;
88
88
  /**
89
- * S3d: cross-surface session READ facade for control-plane/operator panels.
89
+ * Cross-surface session READ facade for control-plane/operator panels.
90
90
  * Sync listSessions()/getSession() shape preserved; in adopted-daemon mode it
91
91
  * serves the daemon-hosted union (with an honest offline note when the wire is
92
92
  * down) instead of only this process's local broker. See session-union-cache.ts.
@@ -22,7 +22,7 @@ import {
22
22
  DEFAULT_PANEL_PALETTE,
23
23
  type PanelWorkspaceSection,
24
24
  } from './polish.ts';
25
- import { calcSessionCost, isModelPriced, computeBudgetBreach, readBudgetAlertUsd, type BudgetAlertConfigAccess } from '../export/cost-utils.ts';
25
+ import { calcSessionCost, isModelPriced, computeBudgetBreach, readBudgetAlertUsd, BUDGET_ALERT_USD_CONFIG_KEY, type BudgetAlertConfigAccess } from '../export/cost-utils.ts';
26
26
  import { abbreviateCount } from '../utils/format-number.ts';
27
27
  import { isTextBackspace } from '../input/delete-key-policy.ts';
28
28
 
@@ -332,7 +332,7 @@ export class CostTrackerPanel extends BasePanel {
332
332
  public setBudgetThreshold(usd: number): void {
333
333
  if (!Number.isFinite(usd) || usd < 0) return;
334
334
  this.budgetThreshold = usd;
335
- this.configAccess?.set('behavior.budgetAlertUsd', usd);
335
+ this.configAccess?.set(BUDGET_ALERT_USD_CONFIG_KEY, usd);
336
336
  this.markDirty();
337
337
  }
338
338
 
@@ -2,7 +2,7 @@
2
2
  // fleet-panel-format.ts
3
3
  //
4
4
  // Pure column-layout + cell-formatting helpers for FleetPanel's tree rows.
5
- // Extracted from fleet-panel.ts (S3b hygiene) to hold that file under the
5
+ // Extracted from fleet-panel.ts (file-size hygiene) to hold that file under the
6
6
  // 800-line architecture cap; these are stateless functions the panel's
7
7
  // renderItem/renderDetail call, with no `this` dependency.
8
8
  // ---------------------------------------------------------------------------
@@ -58,21 +58,21 @@ const C = DEFAULT_PANEL_PALETTE;
58
58
  export interface FleetActionCallbacks {
59
59
  /** Graceful interruption (AgentManager.cancel / trigger-schedule disable, via the registry). */
60
60
  readonly interrupt: (id: string) => boolean;
61
- /** Wave-6 (wo-F item d2): re-arm a `paused` node (disabled trigger/schedule or automation job) — interrupt()'s inverse. Honest false when not paused / non-resumable. */
61
+ /** Re-arm a `paused` node (disabled trigger/schedule or automation job) — interrupt()'s inverse. Honest false when not paused / non-resumable. */
62
62
  readonly resume: (id: string) => boolean;
63
63
  /** Hard stop, optionally cascading to killable descendants. Returns the node ids acted on. */
64
64
  readonly kill: (id: string, opts: { readonly cascade: boolean }) => readonly string[];
65
65
  /**
66
- * Wave-3 (C6): fetch an agent's conversation history — live (running) or
66
+ * Fetch an agent's conversation history — live (running) or
67
67
  * frozen (just-completed, still in the SDK's retention ring). Empty array
68
68
  * means "unavailable" (evicted, or never registered) — FleetPanel degrades
69
69
  * to the on-disk ledger fallback in that case, never fabricating content.
70
70
  */
71
71
  readonly getConversationSnapshot: (agentId: string) => readonly ConversationMessageSnapshot[];
72
- /** Wave-3 (C6): resolve the on-disk `<agentId>.jsonl` event-ledger path for the degraded fallback view. */
72
+ /** Resolve the on-disk `<agentId>.jsonl` event-ledger path for the degraded fallback view. */
73
73
  readonly resolveSessionLogPath: (agentId: string) => string;
74
74
  /**
75
- * Wave-3 (W3.2): queue a human message for a live in-process agent (or a
75
+ * Queue a human message for a live in-process agent (or a
76
76
  * wrfc-subtask's current live member agent), delivered at the target's
77
77
  * next turn boundary — never mid-token. Honest refusal
78
78
  * (`{queued:false,reason}`) for anything that cannot take mid-run input
@@ -128,7 +128,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
128
128
  this.reconcileSteerBadges();
129
129
  this.markDirty();
130
130
  });
131
- // Wave-3 (W3.2): the honest "the agent actually consumed this steer at
131
+ // The honest "the agent actually consumed this steer at
132
132
  // its turn boundary" signal — see fleet-tabs.ts's SteerBadgeStatus doc.
133
133
  // A read-model without a runtimeBus dep never invokes this (graceful
134
134
  // no-op), same degrade as steer()/steerable without a messageBus dep.
@@ -179,13 +179,13 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
179
179
  for (const tab of this.tabsState.tabs) tab.lineCache.clear();
180
180
  }
181
181
 
182
- /** True while a session tab (not the root tree) is focused. Replaces the old Wave-2 isDetailFocused() seam. */
182
+ /** True while a session tab (not the root tree) is focused. Replaces the old isDetailFocused() seam. */
183
183
  public isTabActive(): boolean {
184
184
  return this.tabsState.activeTabIndex > 0;
185
185
  }
186
186
 
187
187
  /**
188
- * Wave-3 (W3.2): true while the steer composer on the active tab is open —
188
+ * True while the steer composer on the active tab is open —
189
189
  * every character of a burst (paste, or several fast-typed chars in one
190
190
  * stdin chunk) must land in the draft one at a time rather than being
191
191
  * routed away as panel hotkeys or exploded to the main composer. See
@@ -207,7 +207,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
207
207
  }
208
208
 
209
209
  /**
210
- * Ctrl+X (panel-close) collision resolution (Wave-3 Part C4): the global
210
+ * Ctrl+X (panel-close) collision resolution: the global
211
211
  * shortcut route (handler-shortcuts.ts) calls this BEFORE closing the
212
212
  * panel. Detaching the active tab consumes the key; on the root tree tab
213
213
  * there is nothing to detach, so this returns false and Ctrl+X falls
@@ -317,7 +317,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
317
317
  }
318
318
 
319
319
  /**
320
- * Wave-3 (W3.2, risk #2 — "dropped inference"): called on every read-model
320
+ * The "dropped inference" reconciliation: called on every read-model
321
321
  * snapshot update and on the panel's 1s tick. See fleet-steer.ts's
322
322
  * reconcileSteerBadges doc for why this is needed (the SDK emits no
323
323
  * cancelled/expired signal for a queued steer).
@@ -350,7 +350,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
350
350
  // Confirm overlay owns input first (K-armed kill confirm).
351
351
  if (this.confirmOverlay.handleInput(key)) return true;
352
352
 
353
- // Wave-3 (W3.2): once the steer composer is open on the active tab, it
353
+ // Once the steer composer is open on the active tab, it
354
354
  // owns EVERY key (same full-priority-while-composing contract as
355
355
  // git-panel.ts's commitMessage entry) until Enter (submit) or Esc
356
356
  // (cancel) — a burst/paste and single hotkeys like 'j'/'s'/'[' all land
@@ -458,7 +458,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
458
458
  }
459
459
 
460
460
  if (key === 's') {
461
- // Batch replay D4: 's' from the TREE was silently dead — steering
461
+ // An earlier replay fix: 's' from the TREE was silently dead — steering
462
462
  // required an undiscoverable Enter-attach first. Attach-and-steer in
463
463
  // one press for a steerable node; honest refusal otherwise.
464
464
  if (!selected) return false;
@@ -485,7 +485,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
485
485
  }
486
486
 
487
487
  /**
488
- * Wave-3 (W3.2): the `s` gate — mirrors the i/K capability-gating template
488
+ * The `s` gate — mirrors the i/K capability-gating template
489
489
  * exactly (guard on the live node existing, non-terminal, and the specific
490
490
  * capability; consume the key and say so rather than silently no-op-ing).
491
491
  */
@@ -649,13 +649,13 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
649
649
 
650
650
  const snapshotRows = this.readModel.getSnapshot().rows;
651
651
  const liveNode = snapshotRows.find((row) => row.node.id === tab.nodeId)?.node ?? null;
652
- // Wave-3 (W3.2): the steer composer only ever applies to 'agent' tabs
652
+ // The steer composer only ever applies to 'agent' tabs
653
653
  // (wrfc-chain has no conversation loop of its own — see
654
654
  // fleet-tabs.ts's FleetAttachableKind doc); the live-node lookup is
655
655
  // shared with the isTerminal computation below either way.
656
656
  const canSteer = liveNode !== null && !isTerminalProcessState(liveNode.state) && liveNode.capabilities.steerable;
657
657
 
658
- // Wave-3 (W3.2): while composing, Enter/Esc replace the tab-switch/detach
658
+ // While composing, Enter/Esc replace the tab-switch/detach
659
659
  // hints (mirrors git-panel.ts's renderCommitCompose footer) so the
660
660
  // footer never advertises a key the composer itself has absorbed.
661
661
  const hints: Array<{ keys: string; label: string }> = tab.steerDraft !== null
@@ -739,7 +739,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
739
739
  const hints = buildFleetTreeHints(selected?.node, this.follow, this.tabsState.tabs.length > 0);
740
740
 
741
741
  // Tab strip renders only when tabs exist — omitting it entirely with no
742
- // tabs attached keeps the pre-Wave-3 root-tree rendering byte-identical.
742
+ // tabs attached keeps the pre-session-tab root-tree rendering byte-identical.
743
743
  const stripLine = renderFleetTabStrip(this.tabsState, width);
744
744
  const header = stripLine ? [stripLine] : undefined;
745
745
 
@@ -71,7 +71,7 @@ export interface FleetSnapshot {
71
71
 
72
72
  export type FleetStateTone = 'active' | 'success' | 'failure' | 'warn' | 'muted';
73
73
 
74
- // W6-P1 ruling: this is a DIFFERENT table from the SDK presentation
74
+ // Design ruling: this is a DIFFERENT table from the SDK presentation
75
75
  // contract's STATE_GLYPHS (@pellux/goodvibes-sdk/platform/presentation) — that
76
76
  // one is a 4-state semantic alias (good/warn/bad/info) shared by the TUI and
77
77
  // agent renderers; this one is a 12-state ProcessNode taxonomy specific to the
@@ -90,16 +90,16 @@ const STATE_GLYPHS: Record<ProcessState, string> = {
90
90
  done: '✓',
91
91
  failed: '✗',
92
92
  killed: '⊘',
93
- // Wave-3 verb formalization: a distinct TERMINAL outcome from 'killed' —
93
+ // A distinct TERMINAL outcome from 'killed' —
94
94
  // both come from AgentManager.cancel(), but a graceful interrupt request
95
95
  // ('the operator asked nicely') is display-distinguishable from a hard
96
96
  // kill. '◌' verified free against every other glyph in this table.
97
97
  interrupted: '◌',
98
98
  idle: '·',
99
99
  queued: '…',
100
- // Wave-6 SDK: schedules/triggers/automation jobs report 'paused' when
100
+ // SDK behavior: schedules/triggers/automation jobs report 'paused' when
101
101
  // disabled (previously mislabeled 'killed'). NOT terminal — resumable via
102
- // ProcessRegistry.resume() (W6.2 d2 wires the full pause/resume UI in
102
+ // ProcessRegistry.resume() (the full pause/resume UI lives in
103
103
  // fleet-stop.ts). '❚' verified free against every other glyph in this table.
104
104
  paused: '❚',
105
105
  };
@@ -140,11 +140,11 @@ const KIND_TAGS: Record<ProcessKind, string> = {
140
140
  schedule: 'sched',
141
141
  watcher: 'watch',
142
142
  'background-process': 'exec',
143
- // Wave 4 (wo703): orchestration-engine kinds (adapters/orchestration.ts).
143
+ // Orchestration-engine kinds (adapters/orchestration.ts).
144
144
  workstream: 'stream',
145
145
  phase: 'phase',
146
146
  'work-item': 'item',
147
- // Wave 5 (wo804): the repo source-tree code index (adapters/code-index.ts).
147
+ // The repo source-tree code index (adapters/code-index.ts).
148
148
  // A single leaf node — never a rollup of other flat-list nodes (see
149
149
  // ROLLUP_KINDS below) — so no other list in this module needs updating.
150
150
  'code-index': 'index',
@@ -288,7 +288,7 @@ export function buildFleetRows(nodes: readonly ProcessNode[]): FleetTreeRow[] {
288
288
  * nonzero cost/token reading (defense in depth against a future adapter
289
289
  * change, not load-bearing).
290
290
  *
291
- * Wave 4 (wo703) additions — verified against adapters/orchestration.ts:
291
+ * Orchestration-engine additions — verified against adapters/orchestration.ts:
292
292
  * - 'workstream': adaptWorkstream SUMS every work-item's usage/costUsd
293
293
  * exactly once (sumWorkItemUsage/aggregateWorkItemCost), the same
294
294
  * "rollup of nodes that also appear individually" shape as adaptChain —
@@ -308,7 +308,7 @@ export function buildFleetRows(nodes: readonly ProcessNode[]): FleetTreeRow[] {
308
308
  * 'wrfc-subtask'-for-capabilities analogue but the 'agent'-for-usage
309
309
  * analogue. Excluding it would silently zero out real cost/token totals.
310
310
  *
311
- * Wave 5 (wo804): 'code-index' is deliberately NOT in this set either —
311
+ * 'code-index' is deliberately NOT in this set either —
312
312
  * adaptCodeIndex yields exactly one standalone ProcessNode per registry
313
313
  * (never a parent whose children ALSO appear individually in the flat
314
314
  * list), so it is a leaf like 'agent'/'work-item', not a grouping construct.
@@ -435,7 +435,7 @@ export interface FleetReadModel {
435
435
  /** Graceful interruption where the source supports one. Returns true when accepted. */
436
436
  interrupt(id: string): boolean;
437
437
  /**
438
- * Wave-6 (wo-F item d2): re-arm a `paused` node (a disabled trigger/schedule,
438
+ * Re-arm a `paused` node (a disabled trigger/schedule,
439
439
  * or an automation job). The inverse of interrupt()'s pause. Returns true when
440
440
  * accepted; false for a node that is not currently `paused` or whose kind has
441
441
  * no resume path (honest refusal).
@@ -444,14 +444,14 @@ export interface FleetReadModel {
444
444
  /** Hard stop, optionally cascading to descendants. Returns the node ids acted on. */
445
445
  kill(id: string, opts?: { readonly cascade?: boolean }): readonly string[];
446
446
  /**
447
- * Wave-3 (W3.2): queue a human message for a live in-process agent (or a
447
+ * Queue a human message for a live in-process agent (or a
448
448
  * wrfc-subtask's current live member), delivered at its next turn
449
449
  * boundary. Honest refusal (`{queued:false,reason}`) for anything that
450
450
  * cannot take mid-run input — see ProcessRegistry.steer's doc comment.
451
451
  */
452
452
  steer(id: string, text: string): SteerResult;
453
453
  /**
454
- * Wave-3 (W3.2): subscribe to the honest "the agent actually consumed this
454
+ * Subscribe to the honest "the agent actually consumed this
455
455
  * steer at its turn boundary" signal (COMMUNICATION_CONSUMED on the
456
456
  * runtime bus's 'communication' domain). Returns an unsubscribe function.
457
457
  * A read-model constructed without a runtimeBus (e.g. the static factory,
@@ -1,7 +1,7 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // fleet-steer.ts
3
3
  //
4
- // Wave-3 (W3.2) — steer-badge rendering helpers and the "dropped inference"
4
+ // Steer-badge rendering helpers and the "dropped inference"
5
5
  // reconciliation pass, split out of fleet-panel.ts to keep that file under
6
6
  // the architecture line cap (see check-architecture.ts's 800-line gate).
7
7
  // Pure functions only; FleetPanel still owns the mutable FleetTab.steerBadge
@@ -67,7 +67,7 @@ export function renderSteerBadgeLine(badge: SteerBadge, width: number, palette:
67
67
  }
68
68
 
69
69
  /**
70
- * Wave-3 (W3.2, risk #2 — "dropped inference"): the SDK emits no
70
+ * The "dropped inference" risk: the SDK emits no
71
71
  * cancelled/expired signal for a queued steer, so a badge left `queued`
72
72
  * after its target node goes terminal (done/failed/killed/interrupted)
73
73
  * would hang forever with no honest resolution. Resolves any such badge to
@@ -115,7 +115,7 @@ export function buildFleetTreeHints(
115
115
  { keys: 'j/k', label: 'navigate' },
116
116
  { keys: 'Enter', label: 'attach' },
117
117
  ];
118
- if (live && selected.capabilities.steerable) hints.push({ keys: 's', label: 'steer' }); // D4: discoverable from the tree (attach-and-steer)
118
+ if (live && selected.capabilities.steerable) hints.push({ keys: 's', label: 'steer' }); // discoverable from the tree (attach-and-steer)
119
119
  if (live && selected.capabilities.interruptible) hints.push({ keys: 'i', label: 'interrupt' });
120
120
  if (live && selected.capabilities.killable) hints.push({ keys: 'K', label: 'kill' });
121
121
  if (live && !isPaused && selected.capabilities.pausable) hints.push({ keys: 'p', label: 'pause' });
@@ -125,7 +125,7 @@ export function buildFleetTreeHints(
125
125
  return hints;
126
126
  }
127
127
 
128
- /** K-confirm descendant stats (UX-C item 6): total = every non-terminal descendant (what a cascade kill takes down); active = the individually-killable subset. Was "active only" (Wave-3 C7), undercounting a mixed subtree. */
128
+ /** K-confirm descendant stats (UX-C item 6): total = every non-terminal descendant (what a cascade kill takes down); active = the individually-killable subset. Was "active only" in an earlier version, undercounting a mixed subtree. */
129
129
  export function countDescendantStats(rows: readonly FleetTreeRow[], nodeId: string): { total: number; active: number } {
130
130
  const byParent = new Map<string, string[]>();
131
131
  for (const row of rows) {
@@ -1,7 +1,7 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // fleet-tabs.ts
3
3
  //
4
- // Wave-3 (W3.1 Part C) — pure, testable tab-state model for FleetPanel's
4
+ // Pure, testable tab-state model for FleetPanel's
5
5
  // session tabs. No BasePanel/rendering dependency, mirroring the
6
6
  // fleet-read-model.ts convention: this module owns attach/detach/switch as
7
7
  // pure state transitions over a small immutable FleetTabsState, so the tab
@@ -32,7 +32,7 @@ export function isAttachableFleetKind(kind: ProcessKind): kind is FleetAttachabl
32
32
  }
33
33
 
34
34
  /**
35
- * Wave-3 (W3.2 steering) — lifecycle of a queued steer message, tracked
35
+ * Lifecycle of a queued steer message, tracked
36
36
  * per-tab. `queued` means `ProcessRegistry.steer()` accepted the message
37
37
  * onto the target's inbox, NOT that the agent has seen it — that is the
38
38
  * later, honest `consumed` transition (a `COMMUNICATION_CONSUMED` runtime-bus
@@ -119,14 +119,14 @@ export interface FleetTab {
119
119
  ledgerEntries: Record<string, unknown>[] | null;
120
120
  ledgerLoadStarted: boolean;
121
121
  /**
122
- * Wave-3 (W3.2): the one-line steer composer's in-progress text, or `null`
122
+ * The one-line steer composer's in-progress text, or `null`
123
123
  * when not composing. Mirrors git-panel.ts's `commitMessage` mutable-slot
124
124
  * convention (FleetPanel.isCapturingTextBurst() gates on this being
125
125
  * non-null so a burst/paste lands here char-by-char, never as tree/tab
126
126
  * hotkeys — see FleetPanel.handleSteerInput).
127
127
  */
128
128
  steerDraft: string | null;
129
- /** Wave-3 (W3.2): this tab's most recent steer message's lifecycle, or null. */
129
+ /** This tab's most recent steer message's lifecycle, or null. */
130
130
  steerBadge: SteerBadge | null;
131
131
  }
132
132
 
@@ -1,9 +1,9 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // fleet-transcript.ts
3
3
  //
4
- // Wave-3 (W3.1 Part C6) — renders the content of an attached FleetPanel
4
+ // Renders the content of an attached FleetPanel
5
5
  // session tab. Three sources, chosen by what the SDK can actually provide
6
- // (see the W3.1 brief's "central reality check" — a full-fidelity live
6
+ // (see the design brief's "central reality check" — a full-fidelity live
7
7
  // transcript needs a ConversationMessageSnapshot[] history source, which
8
8
  // only exists for a RUNNING agent or a just-completed one still inside the
9
9
  // SDK's bounded retention ring):
@@ -163,8 +163,8 @@ export function renderFleetAgentTranscript(
163
163
  /**
164
164
  * Render a live one-line-per-member summary for an attached wrfc-chain tab.
165
165
  *
166
- * `chainDoneOrAbsent` disambiguates an empty member list (d3, W6.2): a completed
167
- * chain prunes its wrapper node (Wave-3 reap), so zero members can mean the
166
+ * `chainDoneOrAbsent` disambiguates an empty member list: a completed
167
+ * chain prunes its wrapper node (zombie reap), so zero members can mean the
168
168
  * chain FINISHED, not that it has not started. When the chain node is absent
169
169
  * (pruned) or terminal, say so honestly instead of the "(no member agents yet)"
170
170
  * not-started wording.
@@ -284,7 +284,7 @@ function renderLedgerEntry(width: number, entry: Record<string, unknown>, palett
284
284
  { text: `${status}${formatLedgerDuration(entry['durationMs'])}`, fg: tone },
285
285
  ]);
286
286
  }
287
- // W5.2 (wo803) — Wave-5 wo801 per-turn passive knowledge injection
287
+ // Per-turn passive knowledge injection
288
288
  // records, appended to this same JSONL ledger as
289
289
  // `{type:'knowledge_injection', turn, ...record}` (orchestrator-runner.ts).
290
290
  // Without this case these fell through to the generic 'event' default
@@ -17,6 +17,16 @@ import type {
17
17
  // panel's selected-record scope/class/tags/provenance detail is folded into
18
18
  // each row label. `memoryRegistry` absent → the retired MemoryPanel's
19
19
  // "not configured" copy renders as an honest degraded state.
20
+ //
21
+ // Read path (memory-spine adoption): reads go through the spine client's
22
+ // `honestSearch` — the MemoryAccess shape — not the raw registry, so a session
23
+ // that has adopted an external daemon reads the SAME wire-served records a
24
+ // wire failure is deliberately surfaced (never a silently stale local copy).
25
+ // `buildView()` stays synchronous/pure per the ConfigModalSurface contract;
26
+ // `refresh()` is async and calls the `requestRender` callback `onOpen` hands
27
+ // it when the data lands. The Review Queue ranking is recomputed client-side
28
+ // from the same honestSearch batch (see `rankForReview` below) rather than
29
+ // through a second wire call — `reviewQueue` is not part of MemoryAccess.
20
30
  // ---------------------------------------------------------------------------
21
31
 
22
32
  /** Minimal read shape of a `MemoryRecord` this modal renders. */
@@ -33,16 +43,42 @@ interface MemoryRecordLike {
33
43
  readonly reviewedAt?: number | undefined;
34
44
  readonly reviewedBy?: string | undefined;
35
45
  readonly createdAt: number;
46
+ readonly updatedAt?: number | undefined;
36
47
  readonly provenance: readonly { readonly kind: string; readonly ref: string }[];
37
48
  }
38
49
 
39
50
  export interface MemoryModalDeps {
40
51
  readonly memoryRegistry?: {
41
- search(filter?: { limit?: number }): readonly MemoryRecordLike[];
42
- reviewQueue(limit?: number): readonly MemoryRecordLike[];
52
+ honestSearch(filter?: { limit?: number }): Promise<{
53
+ readonly records: readonly MemoryRecordLike[];
54
+ readonly indexUnavailableReason?: string | null | undefined;
55
+ }>;
43
56
  };
44
57
  }
45
58
 
59
+ /**
60
+ * Client-side port of the SDK's MemoryStore.reviewQueue ranking
61
+ * (memory-store-helpers.ts reviewQueueScore/isReviewCandidate): all four
62
+ * review states are candidates, scored by state + inverse confidence (flagged
63
+ * states penalized), tie-broken by recency. Presentation ordering only — not
64
+ * a wire call, so exact parity with the server's own tie-breaking on ids it
65
+ * has never seen is not load-bearing.
66
+ */
67
+ function rankForReview(records: readonly MemoryRecordLike[], limit: number): MemoryRecordLike[] {
68
+ const score = (r: MemoryRecordLike): number => {
69
+ let s = 0;
70
+ if (r.reviewState === 'fresh') s += 40;
71
+ if (r.reviewState === 'stale') s += 20;
72
+ if (r.reviewState === 'contradicted') s += 10;
73
+ s += Math.max(0, 100 - r.confidence);
74
+ if (r.reviewState === 'stale' || r.reviewState === 'contradicted') s -= 20;
75
+ return s;
76
+ };
77
+ return [...records]
78
+ .sort((a, b) => score(b) - score(a) || (b.updatedAt ?? b.createdAt) - (a.updatedAt ?? a.createdAt) || b.createdAt - a.createdAt)
79
+ .slice(0, limit);
80
+ }
81
+
46
82
  // Mirrors registerKnowledgePanels's withUnconfiguredFallback copy for the
47
83
  // retired MemoryPanel (src/panels/builtin/knowledge.ts) verbatim.
48
84
  const NOT_CONFIGURED_TITLE = 'Memory registry not configured for this session.';
@@ -57,6 +93,9 @@ class MemoryModalSurface implements ConfigModalSurface {
57
93
  readonly title = 'Memory';
58
94
  private allRecords: MemoryRecordLike[] = [];
59
95
  private reviewRecords: MemoryRecordLike[] = [];
96
+ /** Honest note on the last read: a wire failure (client mode) or the index-unavailable fallback reason — never silently dropped. */
97
+ private loadNote: string | null = null;
98
+ private requestRender: (() => void) | null = null;
60
99
 
61
100
  constructor(private readonly deps: MemoryModalDeps) {}
62
101
 
@@ -71,12 +110,27 @@ class MemoryModalSurface implements ConfigModalSurface {
71
110
  { key: 'r', id: 'refresh', label: 'refresh' },
72
111
  ];
73
112
 
74
- onOpen(): void { this.refresh(); }
113
+ onOpen(requestRender: () => void): void {
114
+ this.requestRender = requestRender;
115
+ void this.refresh();
116
+ }
117
+
118
+ onClose(): void {
119
+ this.requestRender = null;
120
+ }
75
121
 
76
- private refresh(): void {
77
- if (!this.deps.memoryRegistry) { this.allRecords = []; this.reviewRecords = []; return; }
78
- this.allRecords = [...this.deps.memoryRegistry.search({ limit: 100 })];
79
- this.reviewRecords = [...this.deps.memoryRegistry.reviewQueue(24)];
122
+ private async refresh(): Promise<void> {
123
+ if (!this.deps.memoryRegistry) { this.allRecords = []; this.reviewRecords = []; this.loadNote = null; return; }
124
+ try {
125
+ const result = await this.deps.memoryRegistry.honestSearch({ limit: 100 });
126
+ this.allRecords = [...result.records];
127
+ this.reviewRecords = rankForReview(result.records, 24);
128
+ this.loadNote = result.indexUnavailableReason ?? null;
129
+ } catch (error) {
130
+ // Client-mode wire failure: surfaced plainly, never masked by the last-known list.
131
+ this.loadNote = `Failed to reach memory over the wire: ${error instanceof Error ? error.message : String(error)}`;
132
+ }
133
+ this.requestRender?.();
80
134
  }
81
135
 
82
136
  private recordFrom(id: string): MemoryRecordLike | undefined {
@@ -121,11 +175,15 @@ class MemoryModalSurface implements ConfigModalSurface {
121
175
  tabs: [{ id: 'all', label: 'All Records', rows: [] }],
122
176
  };
123
177
  }
124
- return { title: 'Memory', tabs: [this.allTab(), this.reviewTab()] };
178
+ return {
179
+ title: 'Memory',
180
+ ...(this.loadNote ? { degraded: this.loadNote } : {}),
181
+ tabs: [this.allTab(), this.reviewTab()],
182
+ };
125
183
  }
126
184
 
127
185
  onAction(id: string, ctx: ConfigModalActionContext): void {
128
- if (id === 'refresh') { this.refresh(); ctx.setStatus('Reloaded memory records.'); return; }
186
+ if (id === 'refresh') { void this.refresh(); ctx.setStatus('Reloading memory records...'); return; }
129
187
  const record = ctx.row ? this.recordFrom(ctx.row.id) : undefined;
130
188
  if (!record) return;
131
189
  const review = (state: string, confidence: number, reason?: string): void => {
@@ -150,9 +208,12 @@ export function createMemoryModalSurface(deps: MemoryModalDeps): ConfigModalSurf
150
208
 
151
209
  /**
152
210
  * Deterministic golden fixture: fixed memory records with frozen createdAt
153
- * timestamps — no live registry, no wall-clock, no random ids.
211
+ * timestamps — no live registry, no wall-clock, no random ids. Promise-backed
212
+ * (see ecosystem-modals-golden.test.ts): `honestSearch` is async even for this
213
+ * in-memory fixture (matching the real MemoryAccess shape), so the factory
214
+ * pre-awaits the initial `onOpen` refresh before handing back the surface.
154
215
  */
155
- export function memoryModalGoldenSurface(): ConfigModalSurface {
216
+ export async function memoryModalGoldenSurface(): Promise<ConfigModalSurface> {
156
217
  const FIXED_CREATED_AT = 1735689600000; // 2025-01-01T00:00:00.000Z
157
218
  const records: readonly MemoryRecordLike[] = [
158
219
  {
@@ -171,10 +232,10 @@ export function memoryModalGoldenSurface(): ConfigModalSurface {
171
232
  createdAt: FIXED_CREATED_AT + 86400000, provenance: [],
172
233
  },
173
234
  ];
174
- return createMemoryModalSurface({
175
- memoryRegistry: {
176
- search: () => records,
177
- reviewQueue: () => records.filter((record) => record.reviewState !== 'reviewed'),
178
- },
235
+ const surface = createMemoryModalSurface({
236
+ memoryRegistry: { honestSearch: async () => ({ records }) },
179
237
  });
238
+ surface.onOpen?.(() => {});
239
+ await new Promise((resolve) => setTimeout(resolve, 0));
240
+ return surface;
180
241
  }
@@ -142,7 +142,7 @@ export interface Panel {
142
142
  /**
143
143
  * Optional: called by the global Ctrl+X (`panel-close`) shortcut BEFORE it
144
144
  * closes this panel (handler-shortcuts.ts). Return `true` to consume
145
- * Ctrl+X for an in-panel action instead — e.g. FleetPanel (Wave-3 session
145
+ * Ctrl+X for an in-panel action instead — e.g. FleetPanel (session
146
146
  * tabs) detaches its active tab and leaves the panel open. Return `false`
147
147
  * (or omit this hook) to fall through to the ordinary close behavior.
148
148
  * Ctrl+X never reaches a panel's own `handleInput` (ctrl/meta combos are
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Compaction quality-score bridging (Wave-5 W5.4 / B28).
2
+ * Compaction quality-score bridging.
3
3
  *
4
4
  * Reality check (wo803): the SDK's `computeQualityScore()`
5
5
  * (platform/runtime/compaction/quality-score.ts) is only ever called from a
@@ -1,7 +1,7 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // fleet-tab-strip.ts
3
3
  //
4
- // Wave-3 (W3.1 Part C3) — renders FleetPanel's session-tab strip by reusing
4
+ // Renders FleetPanel's session-tab strip by reusing
5
5
  // renderTabStrip exactly as panel-workspace-bar.ts does for the workspace
6
6
  // tab bar. Deliberately a SEPARATE, visually distinct strip from the
7
7
  // workspace bar: workspace tabs switch PANELS, fleet session tabs switch
@@ -23,7 +23,7 @@ const LABEL_FG = UI_TONES.fg.secondary;
23
23
  /**
24
24
  * Render the fleet session-tab strip, or `null` when there are no attached
25
25
  * tabs — the panel omits the strip entirely in that case (root-tab-only),
26
- * which is what keeps the pre-Wave-3 fleet-panel goldens byte-identical.
26
+ * which is what keeps the pre-session-tab fleet-panel goldens byte-identical.
27
27
  */
28
28
  export function renderFleetTabStrip(
29
29
  state: FleetTabsState,
@@ -29,7 +29,7 @@ export function isAgentActive(composerStatus: string | undefined): boolean {
29
29
  // shell/ui-openers.ts). The bare noun 'panels' undersold that; naming the verb
30
30
  // keeps the tip honest about what the chord actually does.
31
31
  const TIP_PANELS = 'Ctrl+P toggle panels';
32
- // W6.2 e: F2 now opens the Fleet panel (the process modal was retired), so the
32
+ // F2 now opens the Fleet panel (the process modal was retired), so the
33
33
  // tip names 'fleet', not 'processes'.
34
34
  const TIP_PROCESSES = 'F2 fleet';
35
35
  const TIP_HELP = '? help';
@@ -189,7 +189,7 @@ function detailLines(picker: ModelPickerModal, width: number): string[] {
189
189
  } else {
190
190
  lines.push(`Context cap overrides the detected local-model context window for this selection.`);
191
191
  }
192
- // W3-T2: a visible cursor glyph on the query itself when search is focused —
192
+ // A visible cursor glyph on the query itself when search is focused —
193
193
  // the only prior affordance was a footer-hint text swap ('/ search' vs.
194
194
  // 'Typing filters search...'), buried at the end of a long hint line and
195
195
  // easy to miss (confirmed via live tmux repro). This puts the "you are