@pellux/goodvibes-tui 1.9.2 → 1.10.1
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.
- package/CHANGELOG.md +26 -0
- package/README.md +2 -2
- package/docs/foundation-artifacts/knowledge-graphql.graphql +5 -0
- package/docs/foundation-artifacts/knowledge-store.sql +33 -0
- package/docs/foundation-artifacts/operator-contract.json +4026 -1376
- package/package.json +2 -2
- package/src/audio/spoken-turn-wiring.ts +7 -2
- package/src/cli/management-utils.ts +4 -0
- package/src/export/cost-utils.ts +9 -1
- package/src/input/command-registry.ts +11 -0
- package/src/input/commands/control-room-runtime.ts +12 -5
- package/src/input/commands/incident-runtime.ts +2 -2
- package/src/input/commands/memory.ts +11 -11
- package/src/input/commands/recall-bundle.ts +8 -8
- package/src/input/commands/recall-capture.ts +3 -3
- package/src/input/commands/recall-query.ts +41 -15
- package/src/input/commands/recall-review.ts +10 -10
- package/src/input/settings-modal-data.ts +54 -0
- package/src/main.ts +3 -2
- package/src/panels/builtin/shared.ts +3 -3
- package/src/panels/cost-tracker-panel.ts +2 -2
- package/src/panels/modals/memory-modal.ts +77 -16
- package/src/runtime/bootstrap-command-context.ts +4 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -1
- package/src/runtime/bootstrap-shell.ts +15 -4
- package/src/runtime/bootstrap.ts +18 -18
- package/src/runtime/memory-spine-transport.ts +289 -0
- package/src/runtime/orchestrator-core-services.ts +27 -2
- package/src/runtime/services.ts +13 -14
- package/src/version.ts +1 -1
- package/src/audio/spoken-turn-controller.ts +0 -271
- 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.
|
|
3
|
+
"version": "1.10.1",
|
|
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.
|
|
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 '
|
|
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') {
|
package/src/export/cost-utils.ts
CHANGED
|
@@ -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(
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
10
|
+
import { getMemorySpine } from './recall-query.ts';
|
|
11
11
|
|
|
12
12
|
export async function handleRecallAdd(args: string[], context: CommandContext): Promise<void> {
|
|
13
|
-
const memory =
|
|
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 =
|
|
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
|
-
|
|
16
|
-
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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 {
|
|
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
|
-
/**
|
|
83
|
-
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. */
|