@pellux/goodvibes-tui 1.9.2 → 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 (32) hide show
  1. package/CHANGELOG.md +20 -0
  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/spoken-turn-wiring.ts +7 -2
  8. package/src/cli/management-utils.ts +4 -0
  9. package/src/export/cost-utils.ts +9 -1
  10. package/src/input/command-registry.ts +11 -0
  11. package/src/input/commands/control-room-runtime.ts +12 -5
  12. package/src/input/commands/incident-runtime.ts +2 -2
  13. package/src/input/commands/memory.ts +11 -11
  14. package/src/input/commands/recall-bundle.ts +8 -8
  15. package/src/input/commands/recall-capture.ts +3 -3
  16. package/src/input/commands/recall-query.ts +41 -15
  17. package/src/input/commands/recall-review.ts +10 -10
  18. package/src/input/settings-modal-data.ts +54 -0
  19. package/src/main.ts +3 -2
  20. package/src/panels/builtin/shared.ts +3 -3
  21. package/src/panels/cost-tracker-panel.ts +2 -2
  22. package/src/panels/modals/memory-modal.ts +77 -16
  23. package/src/runtime/bootstrap-command-context.ts +4 -0
  24. package/src/runtime/bootstrap-command-parts.ts +4 -1
  25. package/src/runtime/bootstrap-shell.ts +15 -4
  26. package/src/runtime/bootstrap.ts +18 -18
  27. package/src/runtime/memory-spine-transport.ts +289 -0
  28. package/src/runtime/orchestrator-core-services.ts +27 -2
  29. package/src/runtime/services.ts +13 -14
  30. package/src/version.ts +1 -1
  31. package/src/audio/spoken-turn-controller.ts +0 -271
  32. package/src/audio/text-chunker.ts +0 -110
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "1.9.2",
3
+ "version": "1.10.0",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -99,7 +99,7 @@
99
99
  "@anthropic-ai/vertex-sdk": "^0.16.0",
100
100
  "@ast-grep/napi": "^0.42.0",
101
101
  "@aws/bedrock-token-generator": "^1.1.0",
102
- "@pellux/goodvibes-sdk": "1.0.0",
102
+ "@pellux/goodvibes-sdk": "1.3.3",
103
103
  "bash-language-server": "^5.6.0",
104
104
  "fuse.js": "^7.1.0",
105
105
  "graphql": "^16.13.2",
@@ -3,7 +3,7 @@ import type { UiRuntimeEvents } from '@/runtime/index.ts';
3
3
  import type { VoiceService } from '@pellux/goodvibes-sdk/platform/voice';
4
4
  import type { StreamingAudioPlayer } from './player.ts';
5
5
  import { LocalStreamingAudioPlayer } from './player.ts';
6
- import { SpokenTurnController } from './spoken-turn-controller.ts';
6
+ import { SpokenTurnController } from '@pellux/goodvibes-sdk/platform/voice';
7
7
 
8
8
  export interface SpokenTurnRuntime {
9
9
  readonly unsubs: readonly (() => void)[];
@@ -37,10 +37,15 @@ export interface WireSpokenTurnRuntimeOptions {
37
37
  */
38
38
  export function wireSpokenTurnRuntime(options: WireSpokenTurnRuntimeOptions): SpokenTurnRuntime {
39
39
  const player = options.playerFactory ? options.playerFactory() : new LocalStreamingAudioPlayer();
40
+ // The TUI's subprocess-based player already satisfies the SDK's AudioSink
41
+ // interface unchanged (see docs/decisions/2026-07-06-spoken-turn-tts-policy-sdk-hoist.md
42
+ // in the SDK repo) — only the option name (`sink` not `player`) and the
43
+ // attribution `source` are new.
40
44
  const controller = new SpokenTurnController({
41
45
  voiceService: options.voiceService,
42
46
  configManager: options.configManager,
43
- player,
47
+ sink: player,
48
+ source: 'goodvibes-tui',
44
49
  notify: options.notify,
45
50
  });
46
51
 
@@ -20,6 +20,7 @@ import { spawn } from 'node:child_process';
20
20
  import { networkInterfaces } from 'node:os';
21
21
  import type { ConfigManager, GoodVibesConfig } from '../config/index.ts';
22
22
  import { bootstrapRuntime } from '../runtime/bootstrap.ts';
23
+ import { refreshMemoryRecallSnapshot } from '../runtime/orchestrator-core-services.ts';
23
24
  import { createRuntimeServices } from '../runtime/services.ts';
24
25
  import { createRuntimeStore } from '../runtime/store/index.ts';
25
26
  import type { RuntimeServices } from '../runtime/services.ts';
@@ -317,6 +318,9 @@ export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promis
317
318
  });
318
319
 
319
320
  try {
321
+ // Async pre-turn refresh of the memory-spine recall snapshot (SDK 1.2.0
322
+ // full-detach) — see the matching comment in main.ts's submitInput.
323
+ await refreshMemoryRecallSnapshot(ctx.services);
320
324
  await ctx.orchestrator.handleUserInput(prompt);
321
325
  await done;
322
326
  if (outputFormat === 'json') {
@@ -174,6 +174,14 @@ export interface BudgetAlertConfigAccess {
174
174
  /** Default for the `behavior.budgetAlertUsd` synthetic setting: 0 = no budget configured. */
175
175
  export const BUDGET_ALERT_USD_DEFAULT = 0;
176
176
 
177
+ /**
178
+ * The dot-path config key backing the session cost-budget alert threshold.
179
+ * Shared by cost-tracker-panel.ts (get/set), settings-modal-data.ts (the
180
+ * settings-modal-visible synthetic entry), and readBudgetAlertUsd below, so
181
+ * the string literal lives in exactly one place.
182
+ */
183
+ export const BUDGET_ALERT_USD_CONFIG_KEY = 'behavior.budgetAlertUsd';
184
+
177
185
  /**
178
186
  * readBudgetAlertUsd — read the session cost-budget alert threshold (USD) from
179
187
  * config. This is the single source of truth CostTrackerPanel and the
@@ -183,7 +191,7 @@ export const BUDGET_ALERT_USD_DEFAULT = 0;
183
191
  * when the key is absent or invalid.
184
192
  */
185
193
  export function readBudgetAlertUsd(configGet: (key: string) => unknown): number {
186
- const raw = configGet('behavior.budgetAlertUsd');
194
+ const raw = configGet(BUDGET_ALERT_USD_CONFIG_KEY);
187
195
  const parsed = typeof raw === 'number' ? raw : Number(raw);
188
196
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : BUDGET_ALERT_USD_DEFAULT;
189
197
  }
@@ -13,6 +13,7 @@ import type { KeybindingsManager } from './keybindings.ts';
13
13
  import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
14
14
  import type { OpenOnboardingWizardOptions } from './handler-ui-state.ts';
15
15
  import type { KnowledgeApi } from '@pellux/goodvibes-sdk/platform/knowledge';
16
+ import type { MemorySpineClient } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
16
17
  import type { HookApi } from '@pellux/goodvibes-sdk/platform/hooks';
17
18
  import type { McpApi } from '@pellux/goodvibes-sdk/platform/mcp';
18
19
  import type { ProviderApi } from '@pellux/goodvibes-sdk/platform/providers';
@@ -285,6 +286,16 @@ export interface CommandContext
285
286
  readonly peer?: PeerClient;
286
287
  readonly providerApi?: ProviderApi;
287
288
  readonly knowledgeApi?: KnowledgeApi;
289
+ /**
290
+ * The cross-surface memory spine client (SDK 1.2.0 full-detach). `/recall`'s
291
+ * browse/link/queue/export/import subcommands read and write through this —
292
+ * not `knowledgeApi.memory` — so they fully detach from the local store file
293
+ * when a daemon has been adopted, per
294
+ * docs/decisions/2026-07-06-memory-wire-full-detach.md in the SDK repo.
295
+ * `explain` and the `vector` diagnostic subcommand stay on `knowledgeApi.memory`
296
+ * (host-side projection / host-only maintenance, ruled explicitly in that decision).
297
+ */
298
+ readonly memorySpine?: MemorySpineClient;
288
299
  readonly hookApi?: HookApi;
289
300
  readonly mcpApi?: McpApi;
290
301
  readonly opsApi?: OpsApi;
@@ -3,7 +3,7 @@ import { buildMcpAttackPathReview } from '@/runtime/index.ts';
3
3
  import { buildKnowledgeInjectionPrompt, selectKnowledgeForTask } from '@pellux/goodvibes-sdk/platform/state';
4
4
  import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
5
5
  import { requireReadModels, requireSubscriptionManager, requireTokenAuditor } from './runtime-services.ts';
6
- import { getMemoryApi } from './recall-query.ts';
6
+ import { getMemoryApi, getMemorySpine } from './recall-query.ts';
7
7
 
8
8
  export function registerControlRoomRuntimeCommands(registry: CommandRegistry): void {
9
9
  registry.register({
@@ -198,7 +198,7 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
198
198
  aliases: ['pmem'],
199
199
  description: 'Inspect durable project memory: risks, runbooks, and architecture notes',
200
200
  usage: '[open | queue [limit] | explain <task...> [--scope <path> ...]]',
201
- handler(args, ctx) {
201
+ async handler(args, ctx) {
202
202
  const subcommand = (args[0] ?? 'open').toLowerCase();
203
203
  if (subcommand === 'open') {
204
204
  if (ctx.openMemoryPanel) {
@@ -208,11 +208,13 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
208
208
  ctx.print('Memory panel is not available in this runtime.');
209
209
  return;
210
210
  }
211
- const memory = getMemoryApi(ctx);
212
- if (!memory) return;
213
211
  if (subcommand === 'queue') {
212
+ // Repointed onto the memory spine (SDK 1.2.0 full-detach) so the
213
+ // queue reflects the daemon's own canonical store when adopted.
214
+ const memory = getMemorySpine(ctx);
215
+ if (!memory) return;
214
216
  const limit = Math.max(1, parseInt(args[1] ?? '10', 10) || 10);
215
- const queue = memory.reviewQueue(limit);
217
+ const queue = await memory.reviewQueue(limit);
216
218
  if (queue.length === 0) {
217
219
  ctx.print('Knowledge review queue is empty.');
218
220
  return;
@@ -224,6 +226,11 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
224
226
  return;
225
227
  }
226
228
  if (subcommand === 'explain') {
229
+ // Stays on the local MemoryApi: `explain` is a host-side projection
230
+ // over whatever read surface is active, not a store operation
231
+ // (docs/decisions/2026-07-06-memory-wire-full-detach.md, SDK repo).
232
+ const memory = getMemoryApi(ctx);
233
+ if (!memory) return;
227
234
  const scopeIdx = args.indexOf('--scope');
228
235
  const scopeValues = scopeIdx !== -1
229
236
  ? args.slice(scopeIdx + 1).filter((token) => !token.startsWith('--'))
@@ -3,7 +3,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
3
3
  import type { CommandRegistry } from '../command-registry.ts';
4
4
  import { buildIncidentMemoryAddOptions } from '@pellux/goodvibes-sdk/platform/state';
5
5
  import { requireShellPaths } from './runtime-services.ts';
6
- import { getMemoryApi } from './recall-query.ts';
6
+ import { getMemorySpine } from './recall-query.ts';
7
7
 
8
8
  export function registerIncidentRuntimeCommands(registry: CommandRegistry): void {
9
9
  registry.register({
@@ -74,7 +74,7 @@ export function registerIncidentRuntimeCommands(registry: CommandRegistry): void
74
74
  return;
75
75
  }
76
76
  if (subcommand === 'capture') {
77
- const memory = getMemoryApi(ctx);
77
+ const memory = getMemorySpine(ctx);
78
78
  if (!memory) return;
79
79
  if (!report) {
80
80
  ctx.print(`Incident not found: ${requestedId ?? 'latest'}`);
@@ -99,7 +99,7 @@ export const recallCommand: SlashCommand = {
99
99
 
100
100
  case 'search':
101
101
  case 'find':
102
- handleRecallSearch(rest, context);
102
+ await handleRecallSearch(rest, context);
103
103
  break;
104
104
 
105
105
  case 'vector':
@@ -113,24 +113,24 @@ export const recallCommand: SlashCommand = {
113
113
 
114
114
  case 'get':
115
115
  case 'show':
116
- handleRecallGet(rest, context);
116
+ await handleRecallGet(rest, context);
117
117
  break;
118
118
 
119
119
  case 'queue':
120
- handleRecallQueue(rest, context);
120
+ await handleRecallQueue(rest, context);
121
121
  break;
122
122
 
123
123
  case 'review':
124
- handleRecallReview(rest, context);
124
+ await handleRecallReview(rest, context);
125
125
  break;
126
126
 
127
127
  case 'stale':
128
- handleRecallReview([rest[0] ?? '', 'stale', '--reason', ...rest.slice(1)], context);
128
+ await handleRecallReview([rest[0] ?? '', 'stale', '--reason', ...rest.slice(1)], context);
129
129
  break;
130
130
 
131
131
  case 'contradict':
132
132
  case 'contradicted':
133
- handleRecallReview([rest[0] ?? '', 'contradicted', '--reason', ...rest.slice(1)], context);
133
+ await handleRecallReview([rest[0] ?? '', 'contradicted', '--reason', ...rest.slice(1)], context);
134
134
  break;
135
135
 
136
136
  case 'explain':
@@ -142,7 +142,7 @@ export const recallCommand: SlashCommand = {
142
142
  break;
143
143
 
144
144
  case 'promote':
145
- handleRecallPromote(rest, context);
145
+ await handleRecallPromote(rest, context);
146
146
  break;
147
147
 
148
148
  case 'link':
@@ -151,17 +151,17 @@ export const recallCommand: SlashCommand = {
151
151
 
152
152
  case 'list':
153
153
  case 'ls':
154
- handleRecallList(rest, context);
154
+ await handleRecallList(rest, context);
155
155
  break;
156
156
 
157
157
  case 'remove':
158
158
  case 'delete':
159
159
  case 'rm':
160
- handleRecallRemove(rest, context);
160
+ await handleRecallRemove(rest, context);
161
161
  break;
162
162
 
163
163
  case 'export':
164
- handleRecallExport(rest, context);
164
+ await handleRecallExport(rest, context);
165
165
  break;
166
166
 
167
167
  case 'import':
@@ -170,7 +170,7 @@ export const recallCommand: SlashCommand = {
170
170
 
171
171
  case 'handoff-export':
172
172
  case 'share':
173
- handleRecallHandoffExport(rest, context);
173
+ await handleRecallHandoffExport(rest, context);
174
174
  break;
175
175
 
176
176
  case 'handoff-inspect':
@@ -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,
@@ -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;
@@ -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) {
@@ -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
  // ---------------------------------------------------------------------------
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';
@@ -288,6 +288,7 @@ async function main() {
288
288
  // Snapshot pre-submission state for failover retryTurn; also clears visited set.
289
289
  retryCtx = { count: conversation.getMessageCount(), text: processedText, content, opts: inputOptions };
290
290
  streamResult.clearFailoverVisited();
291
+ await refreshMemoryRecallSnapshot(ctx.services); // pre-turn recall-snapshot refresh (SDK 1.2.0 full detach)
291
292
  orchestrator.handleUserInput(processedText, content, inputOptions).catch((err: unknown) => {
292
293
  logger.debug('handleUserInput safety catch (already handled by runTurn)', { error: summarizeError(err) });
293
294
  });
@@ -700,7 +701,7 @@ async function main() {
700
701
  const { count, text, content: rContent, opts: rOpts } = retryCtx;
701
702
  // Roll back to pre-submission count, then re-submit. SDK gap — no retry-in-place (see handoff).
702
703
  conversation.removeMessagesAfter(count);
703
- 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) }));
704
705
  };
705
706
  const streamResult: WireStreamEventMetricsResult = wireStreamEventMetrics({
706
707
  events: uiServices.events, orchestrator, providerRegistry,
@@ -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,8 +79,8 @@ 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. */