@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
@@ -4,11 +4,11 @@ import type { CommandContext } from '../command-registry.ts';
4
4
  import type { MemoryBundle, MemorySearchFilter } from '@pellux/goodvibes-sdk/platform/state';
5
5
  import { VALID_CLASSES, VALID_SCOPES, isValidClass, isValidScope, resolveBundlePath } from './recall-shared.ts';
6
6
  import { requireShellPaths } from './runtime-services.ts';
7
- import { getMemoryApi } from './recall-query.ts';
7
+ import { getMemorySpine } from './recall-query.ts';
8
8
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
9
9
 
10
- export function handleRecallExport(args: string[], context: CommandContext): void {
11
- const memory = getMemoryApi(context);
10
+ export async function handleRecallExport(args: string[], context: CommandContext): Promise<void> {
11
+ const memory = getMemorySpine(context);
12
12
  if (!memory) {
13
13
  return;
14
14
  }
@@ -40,7 +40,7 @@ export function handleRecallExport(args: string[], context: CommandContext): voi
40
40
  filter.cls = cls;
41
41
  }
42
42
 
43
- const bundle = memory.exportBundle(filter);
43
+ const bundle = await memory.exportBundle(filter);
44
44
  const targetPath = resolveBundlePath(pathArg, requireShellPaths(context));
45
45
  mkdirSync(dirname(targetPath), { recursive: true });
46
46
  writeFileSync(targetPath, JSON.stringify(bundle, null, 2) + '\n', 'utf-8');
@@ -48,7 +48,7 @@ export function handleRecallExport(args: string[], context: CommandContext): voi
48
48
  }
49
49
 
50
50
  export async function handleRecallImport(args: string[], context: CommandContext): Promise<void> {
51
- const memory = getMemoryApi(context);
51
+ const memory = getMemorySpine(context);
52
52
  if (!memory) {
53
53
  return;
54
54
  }
@@ -84,8 +84,8 @@ function inspectBundle(bundle: MemoryBundle): string {
84
84
  ].join('\n');
85
85
  }
86
86
 
87
- export function handleRecallHandoffExport(args: string[], context: CommandContext): void {
88
- const memory = getMemoryApi(context);
87
+ export async function handleRecallHandoffExport(args: string[], context: CommandContext): Promise<void> {
88
+ const memory = getMemorySpine(context);
89
89
  if (!memory) {
90
90
  return;
91
91
  }
@@ -100,7 +100,7 @@ export function handleRecallHandoffExport(args: string[], context: CommandContex
100
100
  context.print(`[recall] Unknown scope "${scopeRaw ?? ''}". Valid: ${VALID_SCOPES.join(', ')}`);
101
101
  return;
102
102
  }
103
- const bundle = memory.exportBundle({ scope: scopeRaw });
103
+ const bundle = await memory.exportBundle({ scope: scopeRaw });
104
104
  const targetPath = resolveBundlePath(pathArg, requireShellPaths(context));
105
105
  mkdirSync(dirname(targetPath), { recursive: true });
106
106
  writeFileSync(targetPath, JSON.stringify(bundle, null, 2) + '\n', 'utf-8');
@@ -7,10 +7,10 @@ import {
7
7
  buildPolicyPreflightMemoryAddOptions,
8
8
  } from '@pellux/goodvibes-sdk/platform/state';
9
9
  import { VALID_CLASSES, VALID_SCOPES, isValidClass, isValidScope } from './recall-shared.ts';
10
- import { getMemoryApi } from './recall-query.ts';
10
+ import { getMemorySpine } from './recall-query.ts';
11
11
 
12
12
  export async function handleRecallAdd(args: string[], context: CommandContext): Promise<void> {
13
- const memory = getMemoryApi(context);
13
+ const memory = getMemorySpine(context);
14
14
  if (!memory) {
15
15
  return;
16
16
  }
@@ -70,7 +70,7 @@ export async function handleRecallAdd(args: string[], context: CommandContext):
70
70
  }
71
71
 
72
72
  export async function handleRecallCapture(args: string[], context: CommandContext): Promise<void> {
73
- const memory = getMemoryApi(context);
73
+ const memory = getMemorySpine(context);
74
74
  const pluginManager = context.extensions.pluginManager;
75
75
  if (!memory) {
76
76
  return;
@@ -1,8 +1,19 @@
1
1
  import type { CommandContext } from '../command-registry.ts';
2
2
  import type { MemoryApi } from '@pellux/goodvibes-sdk/platform/knowledge';
3
+ import type { MemorySpineClient } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
3
4
  import type { MemorySearchFilter } from '@pellux/goodvibes-sdk/platform/state';
4
5
  import { VALID_CLASSES, VALID_SCOPES, isValidClass, isValidScope } from './recall-shared.ts';
5
6
 
7
+ /**
8
+ * The local, host-only `MemoryApi` — kept for `/recall explain` and `/recall
9
+ * vector` only. Both are ruled explicitly out of the wire catalog in
10
+ * docs/decisions/2026-07-06-memory-wire-full-detach.md (SDK repo): `explain`
11
+ * is a client-side projection over whatever read surface is active, not a
12
+ * store operation, and vector-index rebuild is host/admin maintenance a wire
13
+ * client does not own. Every other `/recall` subcommand reads/writes through
14
+ * {@link getMemorySpine} instead, so it detaches fully from the local store
15
+ * file when a daemon is adopted.
16
+ */
6
17
  export function getMemoryApi(context: CommandContext): MemoryApi | null {
7
18
  const memoryApi = context.clients?.knowledgeApi?.memory;
8
19
  if (!memoryApi) {
@@ -12,8 +23,23 @@ export function getMemoryApi(context: CommandContext): MemoryApi | null {
12
23
  return memoryApi;
13
24
  }
14
25
 
15
- export function handleRecallSearch(args: string[], context: CommandContext): void {
16
- const memory = getMemoryApi(context);
26
+ /**
27
+ * The cross-surface memory spine client — routes local when offline/embedded,
28
+ * over the wire when a daemon has been adopted, never both. `/recall`'s
29
+ * browse/link/queue/export/import subcommands use this so they never read a
30
+ * divergent local copy while a daemon owns the canonical store.
31
+ */
32
+ export function getMemorySpine(context: CommandContext): MemorySpineClient | null {
33
+ const memorySpine = context.clients?.memorySpine;
34
+ if (!memorySpine) {
35
+ context.print('[recall] Memory spine is not available in this runtime.');
36
+ return null;
37
+ }
38
+ return memorySpine;
39
+ }
40
+
41
+ export async function handleRecallSearch(args: string[], context: CommandContext): Promise<void> {
42
+ const memory = getMemorySpine(context);
17
43
  if (!memory) {
18
44
  return;
19
45
  }
@@ -54,8 +80,8 @@ export function handleRecallSearch(args: string[], context: CommandContext): voi
54
80
  if (queryTokens.length) filter.query = queryTokens.join(' ');
55
81
  if (semantic) filter.semantic = true;
56
82
 
57
- const semanticResults = semantic ? memory.searchSemantic(filter) : [];
58
- const results = semantic ? semanticResults.map((entry) => entry.record) : memory.search(filter);
83
+ const semanticResults = semantic ? await memory.searchSemantic(filter) : [];
84
+ const results = semantic ? semanticResults.map((entry) => entry.record) : await memory.list(filter);
59
85
  if (!results.length) {
60
86
  context.print('[recall] No records found.');
61
87
  return;
@@ -110,8 +136,8 @@ function formatVectorStats(
110
136
  ].join('\n');
111
137
  }
112
138
 
113
- export function handleRecallGet(args: string[], context: CommandContext): void {
114
- const memory = getMemoryApi(context);
139
+ export async function handleRecallGet(args: string[], context: CommandContext): Promise<void> {
140
+ const memory = getMemorySpine(context);
115
141
  if (!memory) {
116
142
  return;
117
143
  }
@@ -120,7 +146,7 @@ export function handleRecallGet(args: string[], context: CommandContext): void {
120
146
  context.print('[recall] Usage: /recall get <id>');
121
147
  return;
122
148
  }
123
- const record = memory.get(id);
149
+ const record = await memory.get(id);
124
150
  if (!record) {
125
151
  context.print(`[recall] Record not found: ${id}`);
126
152
  return;
@@ -142,7 +168,7 @@ export function handleRecallGet(args: string[], context: CommandContext): void {
142
168
  }
143
169
  }
144
170
 
145
- const links = memory.linksFor(id);
171
+ const links = await memory.linksFor(id);
146
172
  if (links.length) {
147
173
  context.print(' Links:');
148
174
  for (const link of links) {
@@ -154,7 +180,7 @@ export function handleRecallGet(args: string[], context: CommandContext): void {
154
180
  }
155
181
 
156
182
  export async function handleRecallLink(args: string[], context: CommandContext): Promise<void> {
157
- const memory = getMemoryApi(context);
183
+ const memory = getMemorySpine(context);
158
184
  if (!memory) {
159
185
  return;
160
186
  }
@@ -171,8 +197,8 @@ export async function handleRecallLink(args: string[], context: CommandContext):
171
197
  context.print(`[recall] Linked: ${fromId} -> ${toId} [${relation}]`);
172
198
  }
173
199
 
174
- export function handleRecallRemove(args: string[], context: CommandContext): void {
175
- const memory = getMemoryApi(context);
200
+ export async function handleRecallRemove(args: string[], context: CommandContext): Promise<void> {
201
+ const memory = getMemorySpine(context);
176
202
  if (!memory) {
177
203
  return;
178
204
  }
@@ -181,7 +207,7 @@ export function handleRecallRemove(args: string[], context: CommandContext): voi
181
207
  context.print('[recall] Usage: /recall remove <id>');
182
208
  return;
183
209
  }
184
- const removed = memory.delete(id);
210
+ const removed = await memory.delete(id);
185
211
  if (!removed) {
186
212
  context.print(`[recall] Record not found: ${id}`);
187
213
  return;
@@ -189,8 +215,8 @@ export function handleRecallRemove(args: string[], context: CommandContext): voi
189
215
  context.print(`[recall] Deleted: ${id}`);
190
216
  }
191
217
 
192
- export function handleRecallList(args: string[], context: CommandContext): void {
193
- const memory = getMemoryApi(context);
218
+ export async function handleRecallList(args: string[], context: CommandContext): Promise<void> {
219
+ const memory = getMemorySpine(context);
194
220
  if (!memory) {
195
221
  return;
196
222
  }
@@ -207,7 +233,7 @@ export function handleRecallList(args: string[], context: CommandContext): void
207
233
  filter.scope = scope;
208
234
  }
209
235
 
210
- const records = memory.search(filter);
236
+ const records = await memory.list(filter);
211
237
  if (!records.length) {
212
238
  context.print('[recall] No records.');
213
239
  return;
@@ -1,10 +1,10 @@
1
1
  import type { CommandContext } from '../command-registry.ts';
2
2
  import { VALID_REVIEW_STATES, VALID_SCOPES, isValidReviewState, isValidScope } from './recall-shared.ts';
3
- import { getMemoryApi } from './recall-query.ts';
3
+ import { getMemoryApi, getMemorySpine } from './recall-query.ts';
4
4
  import { buildMainSessionTurnInjectionsText, buildTurnInjectionsText } from '../../renderer/turn-injection.ts';
5
5
 
6
- export function handleRecallQueue(args: string[], context: CommandContext): void {
7
- const memory = getMemoryApi(context);
6
+ export async function handleRecallQueue(args: string[], context: CommandContext): Promise<void> {
7
+ const memory = getMemorySpine(context);
8
8
  if (!memory) {
9
9
  return;
10
10
  }
@@ -29,7 +29,7 @@ export function handleRecallQueue(args: string[], context: CommandContext): void
29
29
  // When a scope filter is requested, fetch a larger pool then slice to the
30
30
  // requested limit so scope-sparse queues still return meaningful results.
31
31
  const fetchLimit = scopeFilter ? 1000 : limit;
32
- const rawQueue = memory.reviewQueue(fetchLimit);
32
+ const rawQueue = await memory.reviewQueue(fetchLimit);
33
33
  const queue = scopeFilter
34
34
  ? rawQueue.filter((record) => record.scope === scopeFilter).slice(0, limit)
35
35
  : rawQueue;
@@ -45,8 +45,8 @@ export function handleRecallQueue(args: string[], context: CommandContext): void
45
45
  }
46
46
  }
47
47
 
48
- export function handleRecallReview(args: string[], context: CommandContext): void {
49
- const memory = getMemoryApi(context);
48
+ export async function handleRecallReview(args: string[], context: CommandContext): Promise<void> {
49
+ const memory = getMemorySpine(context);
50
50
  if (!memory) {
51
51
  return;
52
52
  }
@@ -64,7 +64,7 @@ export function handleRecallReview(args: string[], context: CommandContext): voi
64
64
  const reviewedBy = byIdx !== -1 ? rest[byIdx + 1] : 'operator';
65
65
  const staleReason = reasonIdx !== -1 ? rest.slice(reasonIdx + 1).join(' ') : undefined;
66
66
 
67
- const record = memory.review(id, {
67
+ const record = await memory.updateReview(id, {
68
68
  state: stateRaw,
69
69
  confidence: Number.isFinite(confidence) ? confidence : undefined,
70
70
  reviewedBy,
@@ -104,9 +104,9 @@ export function handleRecallExplain(args: string[], context: CommandContext): vo
104
104
  }
105
105
 
106
106
  /**
107
- * W5.2 (wo803) — per-turn passive knowledge injection inspectability.
107
+ * Per-turn passive knowledge injection inspectability.
108
108
  *
109
- * With an explicit agent id, reads `AgentRecord.turnInjections` (Wave-5 wo801)
109
+ * With an explicit agent id, reads `AgentRecord.turnInjections`
110
110
  * via `context.ops.agentManager.exportState()` — the CommandContext-exposed
111
111
  * `ShellAgentManagerService` doesn't have a `getStatus`/`list` pair, but
112
112
  * `exportState()` returns the same full `AgentRecord[]` (it's what
@@ -137,8 +137,8 @@ export function handleRecallInjections(args: string[], context: CommandContext):
137
137
  context.print(buildTurnInjectionsText(agentId, record.turnInjections ?? []));
138
138
  }
139
139
 
140
- export function handleRecallPromote(args: string[], context: CommandContext): void {
141
- const memory = getMemoryApi(context);
140
+ export async function handleRecallPromote(args: string[], context: CommandContext): Promise<void> {
141
+ const memory = getMemorySpine(context);
142
142
  if (!memory) {
143
143
  return;
144
144
  }
@@ -148,7 +148,7 @@ export function handleRecallPromote(args: string[], context: CommandContext): vo
148
148
  context.print(`[recall] Usage: /recall promote <id> <${VALID_SCOPES.join('|')}>`);
149
149
  return;
150
150
  }
151
- const record = memory.update(id, { scope });
151
+ const record = await memory.update(id, { scope });
152
152
  if (!record) {
153
153
  context.print(`[recall] Record not found: ${id}`);
154
154
  return;
@@ -384,7 +384,7 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
384
384
  },
385
385
  });
386
386
 
387
- // W6-C3 (Wave 6 core-verb pass, MEMORY fragmentation — worst-class
387
+ // From the core-verb naming pass (MEMORY fragmentation — worst-class
388
388
  // collision #2): this command used to be named /memory, colliding with
389
389
  // the agent's /memory (an alias that forwards to /recall, the durable
390
390
  // cross-session MemoryStore) — same command name, two unrelated features.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * feed-context-factory.ts — Construction and mutable-field sync for InputFeedContext.
3
3
  *
4
- * Extracted from handler.ts (Wave 4α review, 0.18.23) to keep handler.ts under the
4
+ * Extracted from handler.ts (review follow-up, 0.18.23) to keep handler.ts under the
5
5
  * 800-line architecture cap.
6
6
  *
7
7
  * Two exported functions:
@@ -447,8 +447,8 @@ export function handleCtrlC(
447
447
  } catch {
448
448
  // Non-fatal to the exit chord; the quit-window bookkeeping still runs.
449
449
  }
450
- // A press that silenced LIVE speech is consumed by that job (batch replay
451
- // D5) — symmetric with the prompt-clearing press above. The quit chord
450
+ // A press that silenced LIVE speech is consumed by that job (an earlier replay
451
+ // fix) — symmetric with the prompt-clearing press above. The quit chord
452
452
  // ("Ctrl+C x2") starts from a quiet state; turn-aborts still count toward
453
453
  // the double-press.
454
454
  if (stoppedSpeech) return;
@@ -189,7 +189,7 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
189
189
  export type IndicatorFocusRouteState = {
190
190
  indicatorFocused: boolean;
191
191
  modalOpened: (name: string) => void;
192
- /** W2.2: the footer process indicator's [Enter] opens the Fleet panel (F2 also opens the Fleet panel; the retired process modal was removed in W6.2). */
192
+ /** The footer process indicator's [Enter] opens the Fleet panel (F2 also opens it; the process modal was retired). */
193
193
  openFleetPanel: () => void;
194
194
  requestRender: () => void;
195
195
  };
@@ -337,7 +337,7 @@ export type KeyRouteState = {
337
337
  commandRegistry?: CommandRegistry | null;
338
338
  autocomplete: AutocompleteEngine | null;
339
339
  blockActionsMenu: { open: (block: BlockMeta) => void };
340
- /** W6.2 e: F2 opens+focuses the Fleet panel (which subsumes the retired process modal). */
340
+ /** F2 opens+focuses the Fleet panel (which subsumes the retired process modal). */
341
341
  openFleetPanel: () => void;
342
342
  modalOpened: (name: string) => void;
343
343
  saveUndoState: () => void;
@@ -186,7 +186,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
186
186
  // focus to it. panelManager.open() only makes the panel active — focus is a
187
187
  // separate axis (see PanelManager.focusPanels()/getFocusTarget()); without the
188
188
  // focusPanels() call, j/k/i/K land silently in the composer until Tab. Used by
189
- // the footer indicator's [Enter] and by F2 (W6.2 e: F2 and the footer
189
+ // the footer indicator's [Enter] and by F2 (F2 and the footer
190
190
  // indicator both subsume the retired process modal). Mirrors the Ctrl+P
191
191
  // panel-picker launcher (ui-openers.ts openPanelPicker).
192
192
  const openFleetPanel = (): void => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * F1 (One-Platform Wave 2 friction fix): the onboarding wizard's "Connect to
2
+ * Friction fix: the onboarding wizard's "Connect to
3
3
  * this daemon now" action, wired from the Network step's `network.daemon-source`
4
4
  * = 'adopt' fields. Extracted from handler-onboarding.ts to keep that file
5
5
  * under the architecture line cap.
@@ -88,8 +88,8 @@ export async function handleConnectExistingDaemonForHandler(handler: InputHandle
88
88
  }
89
89
 
90
90
  /**
91
- * W4-D1: the guided UX entry point for migrating a legacy
92
- * `goodvibes-daemon.service` unit (Wave 3 shipped detect+disclose only; this
91
+ * The guided UX entry point for migrating a legacy
92
+ * `goodvibes-daemon.service` unit (an earlier release shipped detect+disclose only; this
93
93
  * closes that inheritance). This is a thin wrapper over
94
94
  * `detectLegacyUnit` + `runLegacyDaemonMigration`
95
95
  * (`../runtime/legacy-daemon-migration.ts`) — the actual migration mechanics
@@ -22,7 +22,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
22
22
 
23
23
  if (token.type === 'key') {
24
24
  if (token.logicalName === 'escape') {
25
- // W3-T2: search now starts focused by default (see
25
+ // Search now starts focused by default (see
26
26
  // ModelPickerModal.openAllModels() doc comment). Escape with an empty
27
27
  // query used to only blurSearch() here, leaving the picker open and
28
28
  // requiring a SECOND Escape to actually close it — confusing on a
@@ -133,7 +133,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
133
133
  state.modelPicker.focusSearch();
134
134
  } else if (state.modelPicker.searchFocused) {
135
135
  // Symmetric with 'down' (below): search now starts focused by default
136
- // (W3-T2), so 'up' must have a way out of it too, or it would be a
136
+ // so 'up' must have a way out of it too, or it would be a
137
137
  // silent no-op the very first time a user reaches for the list.
138
138
  state.modelPicker.blurSearch();
139
139
  } else {
@@ -121,7 +121,7 @@ export function handleGlobalShortcutToken(
121
121
  case 'panel-close': {
122
122
  const pm = state.panelManager;
123
123
  const active = pm.getActivePanel();
124
- // Wave-3: give the active panel a chance to consume Ctrl+X for an
124
+ // Give the active panel a chance to consume Ctrl+X for an
125
125
  // in-panel action (FleetPanel session-tab detach) before it closes the
126
126
  // panel outright — see Panel.interceptPanelClose's doc comment.
127
127
  if (active?.interceptPanelClose?.()) {
@@ -264,7 +264,7 @@ export class ModelPickerModal {
264
264
  /**
265
265
  * Open showing all models — entry point for /model.
266
266
  *
267
- * W3-T2: search starts FOCUSED. Live tmux repro (cc-w3-t2) showed the actual
267
+ * Search starts FOCUSED. A live tmux repro showed the actual
268
268
  * friction wasn't the filter itself (it already works — fuzzy multi-word
269
269
  * substring over id/displayName/provider) but that with search unfocused by
270
270
  * default, typing a search term went character-by-character into single-key
@@ -295,7 +295,7 @@ export class ModelPickerModal {
295
295
  this.scrollOffset = 0;
296
296
  }
297
297
 
298
- /** Open showing providers first — entry point for /provider. W3-T2: search starts focused — see openAllModels(). */
298
+ /** Open showing providers first — entry point for /provider. Search starts focused — see openAllModels(). */
299
299
  openProviders(providers: string[], currentProvider: string): void {
300
300
  this.previousMode = null;
301
301
  this.providers = providers;
@@ -86,13 +86,13 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
86
86
  setConfig('service.autostart', hasServers);
87
87
  setConfig('service.restartOnFailure', true);
88
88
  // One-Platform daemon-by-default (SDK docs/decisions/2026-07-05-daemon-by-default.md):
89
- // the loopback-only cross-surface session daemon is safe-by-design (Wave 1:
90
- // loopback bind, auth-gated, rate-limited) and runs regardless of whether
89
+ // the loopback-only cross-surface session daemon is safe-by-design
90
+ // (loopback bind, auth-gated, rate-limited) and runs regardless of whether
91
91
  // browser/LAN/webhook/external-app capabilities are selected here — it is no
92
92
  // longer bundled with those network-exposing surfaces, so onboarding leaves
93
93
  // daemon.enabled untouched and lets the SDK's own default-true (or an existing
94
94
  // explicit user override) govern. (The deprecated danger.daemon alias this
95
- // comment used to also name was removed in Wave 6.)
95
+ // comment used to also name was later removed.)
96
96
  setConfig('controlPlane.enabled', hasServers);
97
97
  setConfig('danger.httpListener', httpListener);
98
98
  setConfig('web.enabled', browserAccess);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * F1 (One-Platform Wave 2 friction fix): the Network step's daemon-source choice
2
+ * Friction fix: the Network step's daemon-source choice
3
3
  * — start a new daemon owned by this TUI (default) vs connect to one that is
4
4
  * already running elsewhere with a known token. Extracted from
5
5
  * onboarding-wizard-steps.ts to keep that file under the architecture line cap.
@@ -78,7 +78,7 @@ export function pushDaemonAdoptionFields(
78
78
  }
79
79
 
80
80
  /**
81
- * W4-D1: the guided, visible entry point for migrating a detected legacy
81
+ * The guided, visible entry point for migrating a detected legacy
82
82
  * `goodvibes-daemon.service` unit — closes the fast-follow-up flagged when
83
83
  * `handleMigrateLegacyDaemonServiceForHandler` (handler-onboarding-daemon-adopt.ts)
84
84
  * shipped fully built but not wired to any onboarding control. Independent of
@@ -107,7 +107,7 @@ export function pushLegacyDaemonMigrationFields(
107
107
  kind: 'status',
108
108
  id: 'network.migrate-legacy-daemon-detected',
109
109
  label: 'Legacy daemon service detected',
110
- // F2 follow-up: name the unit this host actually resolves (carried on the
110
+ // Follow-up: name the unit this host actually resolves (carried on the
111
111
  // snapshot from `resolveConfiguredServiceName` at collection time) — the
112
112
  // MANAGED_SERVICE_NAME fallback only covers snapshots built without it.
113
113
  hint: legacyUnitNote(legacy, legacy.trackedServiceName ?? MANAGED_SERVICE_NAME),
@@ -533,7 +533,7 @@ export function buildNetworkStep(controller: OnboardingWizardControllerLike): On
533
533
  },
534
534
  ];
535
535
 
536
- // F1: recognize an adoptable already-running daemon (see onboarding-wizard-network-adopt.ts).
536
+ // Recognize an adoptable already-running daemon (see onboarding-wizard-network-adopt.ts).
537
537
  const daemonSource = getDaemonSource(controller);
538
538
  pushDaemonAdoptionFields(fields, controller, daemonSource, bindSettings);
539
539
  pushLegacyDaemonMigrationFields(fields, controller);
@@ -6,7 +6,7 @@
6
6
  * control-plane session, see below), tracks selected index, and handles
7
7
  * load/delete actions.
8
8
  *
9
- * W3-T2 (union-sessions surface): additionally surfaces the cross-surface
9
+ * Union-sessions surface: additionally surfaces the cross-surface
10
10
  * session union from `sessionBroker` (a SessionReadFacade — normally
11
11
  * `uiServices.sessions.sessionBroker`, the SessionUnionCache) so a user can
12
12
  * SEE what sessions are live/closed across every surface sharing this
@@ -15,6 +15,7 @@ import { buildSubscriptionEntries } from './settings-modal-subscriptions.ts';
15
15
  import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
16
16
  import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
17
17
  import { CODE_INDEX_ENABLED_CONFIG_KEY } from '../runtime/code-index-services.ts';
18
+ import { BUDGET_ALERT_USD_CONFIG_KEY, BUDGET_ALERT_USD_DEFAULT, readBudgetAlertUsd } from '../export/cost-utils.ts';
18
19
  import {
19
20
  THEME_MODE_CONFIG_KEY,
20
21
  THEME_MODE_VALUES,
@@ -146,6 +147,18 @@ export function buildSettingGroups(
146
147
  behaviorEntries.push(buildNotifyAfterSecondsSyntheticEntry(configManager));
147
148
  }
148
149
 
150
+ // Inject the synthetic behavior.budgetAlertUsd entry into the behavior
151
+ // category. This key is TUI-local (not in the SDK ConfigKey union), which
152
+ // previously left it with no schema-driven inspection surface at all: it
153
+ // never appeared in /config, and /settings-sync show rejects any key not
154
+ // in CONFIG_KEYS. The Cost panel's 'b' key and /cost budget <usd> remain
155
+ // the primary way to change it; this entry makes the current effective
156
+ // value (and whether it's still the "no budget configured" default)
157
+ // visible from /config behavior too, same rationale as notifyAfterSeconds.
158
+ if (behaviorEntries && !behaviorEntries.some((e) => e.setting.key === (BUDGET_ALERT_USD_CONFIG_KEY as ConfigKey))) {
159
+ behaviorEntries.push(buildBudgetAlertUsdSyntheticEntry(configManager));
160
+ }
161
+
149
162
  // Inject the W2.3 alert-class toggles + master focus gate. TUI-local
150
163
  // synthetic settings, same rationale as notifyAfterSeconds above.
151
164
  if (behaviorEntries) {
@@ -156,7 +169,7 @@ export function buildSettingGroups(
156
169
  }
157
170
  }
158
171
 
159
- // Wave 5 (wo804): inject the storage.codeIndexEnabled toggle into the
172
+ // Inject the storage.codeIndexEnabled toggle into the
160
173
  // storage category. TUI-local synthetic setting (not in the SDK ConfigKey
161
174
  // union — see code-index-services.ts), same rationale as
162
175
  // notifyAfterSeconds above: opt-in, default off, states its own bounds.
@@ -287,6 +300,47 @@ export function buildNotifyAfterSecondsSyntheticEntry(configManager: Pick<Config
287
300
  };
288
301
  }
289
302
 
303
+ // ---------------------------------------------------------------------------
304
+ // behavior.budgetAlertUsd synthetic setting
305
+ // ---------------------------------------------------------------------------
306
+
307
+ /**
308
+ * The synthetic ConfigSetting descriptor for behavior.budgetAlertUsd.
309
+ *
310
+ * This key is TUI-local and is not yet in the SDK ConfigKey union — it never
311
+ * appeared in CONFIG_SCHEMA, so it was invisible to every schema-driven
312
+ * inspection surface (/config, /settings-sync show <key>) even though the
313
+ * value round-trips correctly through configManager.get/set. The descriptor
314
+ * is injected into the behavior settings group so /config behavior shows the
315
+ * real current threshold, not just the behavior.notifyOnBudgetBreach gate.
316
+ *
317
+ * 0 = no budget configured (disabled). Any positive number = the USD
318
+ * threshold. Default matches BUDGET_ALERT_USD_DEFAULT in cost-utils.ts, the
319
+ * single source of truth CostTrackerPanel and budget-breach-notifier.ts share.
320
+ */
321
+ export const BUDGET_ALERT_USD_SYNTHETIC_SETTING: ConfigSetting = {
322
+ key: BUDGET_ALERT_USD_CONFIG_KEY as ConfigKey,
323
+ type: 'number',
324
+ default: BUDGET_ALERT_USD_DEFAULT,
325
+ description: 'Session cost-budget alert threshold in USD (0 = no budget configured). Set via the Cost panel\'s "b" key or /cost budget <usd>; this entry only displays the current effective value.',
326
+ };
327
+
328
+ /**
329
+ * Build the synthetic SettingEntry for behavior.budgetAlertUsd.
330
+ *
331
+ * Delegates parsing/fallback to readBudgetAlertUsd (cost-utils.ts) so this
332
+ * display entry can never disagree with what CostTrackerPanel and the
333
+ * background budget-breach notifier actually read.
334
+ */
335
+ export function buildBudgetAlertUsdSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
336
+ const currentValue = readBudgetAlertUsd((key) => configManager.get(key as ConfigKey));
337
+ return {
338
+ setting: BUDGET_ALERT_USD_SYNTHETIC_SETTING,
339
+ currentValue,
340
+ isDefault: currentValue === BUDGET_ALERT_USD_DEFAULT,
341
+ };
342
+ }
343
+
290
344
  // ---------------------------------------------------------------------------
291
345
  // W2.3 alert-class synthetic settings — behavior.notifyOn* + notifyOnlyWhenUnfocused
292
346
  // ---------------------------------------------------------------------------
@@ -342,7 +396,7 @@ export function buildNotifyAlertSyntheticEntries(configManager: Pick<ConfigManag
342
396
  }
343
397
 
344
398
  // ---------------------------------------------------------------------------
345
- // Wave 5 (wo804) — storage.codeIndexEnabled synthetic setting
399
+ // storage.codeIndexEnabled synthetic setting
346
400
  // ---------------------------------------------------------------------------
347
401
 
348
402
  /**
package/src/main.ts CHANGED
@@ -32,7 +32,7 @@ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
32
32
  import { registerBuiltinPanels } from './panels/builtin-panels.ts';
33
33
  import { bootstrapRuntime } from './runtime/bootstrap.ts';
34
34
  import type { BootstrapContext } from './runtime/bootstrap.ts';
35
- import { buildSharedOrchestratorCoreServices } from './runtime/orchestrator-core-services.ts';
35
+ import { buildSharedOrchestratorCoreServices, refreshMemoryRecallSnapshot } from './runtime/orchestrator-core-services.ts';
36
36
  import type { HITLMode } from '@pellux/goodvibes-sdk/platform/state';
37
37
  import { readLastSessionPointer, writeRecoveryFile } from '@/runtime/index.ts';
38
38
  import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
@@ -202,7 +202,7 @@ async function main() {
202
202
 
203
203
  const unsubs: Array<() => void> = [];
204
204
  let recoveryInterval: ReturnType<typeof setInterval> | null = null;
205
- let stopSpokenOutputForExit: (() => void) | null = null;
205
+ let stopSpokenOutputForExit: (() => Promise<void>) | null = null;
206
206
  let recoveryPending = false;
207
207
  // Which file the current recovery prompt should load/delete from (see recovery-input-helpers.ts).
208
208
  let recoverySource: 'live' | 'preserved' = 'live';
@@ -240,7 +240,8 @@ async function main() {
240
240
  events: uiServices.events,
241
241
  notify: (message) => { systemMessageRouter.high(message); render(); },
242
242
  });
243
- stopSpokenOutputForExit = () => spokenTurns.stop();
243
+ // Exit-path stop: bounded drain of the audio already playing (see stopForExit).
244
+ stopSpokenOutputForExit = () => spokenTurns.stopForExit();
244
245
  unsubs.push(...spokenTurns.unsubs);
245
246
  unsubs.push(attachSpokenTurnModelRouting({
246
247
  orchestrator,
@@ -287,6 +288,7 @@ async function main() {
287
288
  // Snapshot pre-submission state for failover retryTurn; also clears visited set.
288
289
  retryCtx = { count: conversation.getMessageCount(), text: processedText, content, opts: inputOptions };
289
290
  streamResult.clearFailoverVisited();
291
+ await refreshMemoryRecallSnapshot(ctx.services); // pre-turn recall-snapshot refresh (SDK 1.2.0 full detach)
290
292
  orchestrator.handleUserInput(processedText, content, inputOptions).catch((err: unknown) => {
291
293
  logger.debug('handleUserInput safety catch (already handled by runTurn)', { error: summarizeError(err) });
292
294
  });
@@ -497,7 +499,7 @@ async function main() {
497
499
  lastInputTokens: orchestrator.lastInputTokens,
498
500
  commandArgsHint,
499
501
  hitlMode: modeManager.getHITLMode(),
500
- // S3d: cross-surface spine posture segment (adopted-daemon mode only).
502
+ // Cross-surface spine posture segment (adopted-daemon mode only).
501
503
  sessionSpineStatus: (() => { const s = uiServices.platform.externalServices?.inspect(); return s?.sessionSpineActive && s.sessionSpineStatus && s.sessionSpineStatus !== 'unknown' ? s.sessionSpineStatus : undefined; })(), runningAgentCount, runningProcessCount,
502
504
  // Composer must not read as focused while the panel/process indicator owns keyboard focus.
503
505
  promptFocused: !input.panelFocused && !input.indicatorFocused,
@@ -646,7 +648,7 @@ async function main() {
646
648
  const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, onCapture: (total) => { commandContext.session.runtime.terminalWritesIntercepted = total; render(); } });
647
649
 
648
650
  setRenderRequest(renderNow); // bootstrap's 16ms coalescer calls the composite directly
649
- setPanelFrameRequester(render); // live panels repaint when idle (Wave-6 replay: fleet sat stale until keypress)
651
+ setPanelFrameRequester(render); // live panels repaint when idle (a replay finding: fleet sat stale until keypress)
650
652
  orchestratorRefs.requestRender = render;
651
653
  commandContext.renderRequest = render;
652
654
  wireShellUiOpeners({
@@ -699,7 +701,7 @@ async function main() {
699
701
  const { count, text, content: rContent, opts: rOpts } = retryCtx;
700
702
  // Roll back to pre-submission count, then re-submit. SDK gap — no retry-in-place (see handoff).
701
703
  conversation.removeMessagesAfter(count);
702
- orchestrator.handleUserInput(text, rContent, rOpts).catch((e: unknown) => logger.debug('retryTurn', { error: summarizeError(e) }));
704
+ void refreshMemoryRecallSnapshot(ctx.services).then(() => orchestrator.handleUserInput(text, rContent, rOpts)).catch((e: unknown) => logger.debug('retryTurn', { error: summarizeError(e) }));
703
705
  };
704
706
  const streamResult: WireStreamEventMetricsResult = wireStreamEventMetrics({
705
707
  events: uiServices.events, orchestrator, providerRegistry,
@@ -14,7 +14,7 @@ const ERROR_FG = '#ef4444';
14
14
  * markDirty() only sets a flag the compositor reads when a frame is ALREADY
15
15
  * being composed for another reason (input, streaming) — so a live panel
16
16
  * (fleet ticks, registry subscriptions) sat visibly stale while the app was
17
- * idle until the next keypress (Wave-6 replay finding). The scheduler
17
+ * idle until the next keypress (a replay finding). The scheduler
18
18
  * coalesces same-tick requests, so this stays cheap.
19
19
  */
20
20
  let panelFrameRequester: (() => void) | null = null;