@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.
- package/CHANGELOG.md +36 -5
- package/README.md +1 -1
- 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/player.ts +91 -5
- package/src/audio/spoken-turn-wiring.ts +10 -2
- package/src/cli/management-utils.ts +4 -0
- package/src/config/credential-availability.ts +1 -1
- package/src/config/index.ts +1 -1
- package/src/core/turn-cancellation.ts +1 -1
- package/src/daemon/cli.ts +4 -4
- package/src/daemon/service-commands.ts +8 -8
- package/src/export/cost-utils.ts +9 -1
- package/src/input/command-registry.ts +16 -5
- package/src/input/commands/config.ts +1 -1
- 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 +12 -12
- package/src/input/commands/planning-runtime.ts +1 -1
- 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 +12 -12
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/feed-context-factory.ts +1 -1
- package/src/input/handler-content-actions.ts +2 -2
- package/src/input/handler-feed-routes.ts +2 -2
- package/src/input/handler-feed.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +3 -3
- package/src/input/handler-picker-routes.ts +2 -2
- package/src/input/handler-shortcuts.ts +1 -1
- package/src/input/model-picker.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-apply.ts +3 -3
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +3 -3
- package/src/input/onboarding/onboarding-wizard-steps.ts +1 -1
- package/src/input/session-picker-modal.ts +1 -1
- package/src/input/settings-modal-data.ts +56 -2
- package/src/main.ts +8 -6
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/operations.ts +4 -4
- package/src/panels/builtin/shared.ts +4 -4
- package/src/panels/cost-tracker-panel.ts +2 -2
- package/src/panels/fleet-panel-format.ts +1 -1
- package/src/panels/fleet-panel.ts +15 -15
- package/src/panels/fleet-read-model.ts +11 -11
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +2 -2
- package/src/panels/fleet-tabs.ts +4 -4
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/modals/memory-modal.ts +77 -16
- package/src/panels/types.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/fleet-tab-strip.ts +2 -2
- package/src/renderer/footer-tips.ts +1 -1
- package/src/renderer/model-workspace.ts +1 -1
- package/src/renderer/session-picker-modal.ts +4 -4
- package/src/renderer/shell-surface.ts +1 -1
- package/src/renderer/status-glyphs.ts +3 -3
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +2 -2
- package/src/renderer/turn-injection.ts +3 -3
- package/src/renderer/ui-factory.ts +7 -7
- package/src/renderer/ui-primitives.ts +3 -3
- package/src/runtime/bootstrap-command-context.ts +4 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -1
- package/src/runtime/bootstrap-core.ts +3 -3
- package/src/runtime/bootstrap-hook-bridge.ts +2 -2
- package/src/runtime/bootstrap-shell.ts +18 -7
- package/src/runtime/bootstrap.ts +22 -22
- package/src/runtime/code-index-services.ts +2 -2
- package/src/runtime/index.ts +11 -3
- package/src/runtime/legacy-daemon-migration.ts +9 -9
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/memory-spine-transport.ts +289 -0
- package/src/runtime/onboarding/snapshot.ts +3 -3
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/operator-token-cleanup.ts +2 -2
- package/src/runtime/orchestrator-core-services.ts +29 -4
- package/src/runtime/process-lifecycle.ts +19 -5
- package/src/runtime/services.ts +27 -28
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-spine-transport.ts +2 -2
- package/src/runtime/ui-services.ts +1 -1
- package/src/runtime/workstream-services.ts +1 -1
- package/src/runtime/wrfc-persistence.ts +2 -2
- package/src/shell/ui-openers.ts +2 -2
- package/src/version.ts +1 -1
- package/src/audio/spoken-turn-controller.ts +0 -243
- 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.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.
|
|
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",
|
package/src/audio/player.ts
CHANGED
|
@@ -15,6 +15,14 @@ export interface StreamingAudioPlayer {
|
|
|
15
15
|
readonly available: boolean;
|
|
16
16
|
play(chunks: AsyncIterable<VoiceAudioChunk>, options: StreamingAudioPlaybackOptions): Promise<void>;
|
|
17
17
|
stop(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Resolves once the currently playing sink has finished naturally (its
|
|
20
|
+
* process closed) or after `timeoutMs`, whichever comes first; resolves
|
|
21
|
+
* immediately when nothing is playing. The exit path uses this to let the
|
|
22
|
+
* audio the user is already hearing finish inside a short bounded window
|
|
23
|
+
* instead of killing it mid-drain. stop() remains the instant cut.
|
|
24
|
+
*/
|
|
25
|
+
waitForDrain(timeoutMs: number): Promise<void>;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export interface StreamingAudioPlaybackOptions {
|
|
@@ -25,6 +33,8 @@ export interface StreamingAudioPlaybackOptions {
|
|
|
25
33
|
interface SpawnProcess {
|
|
26
34
|
readonly stdin: Writable;
|
|
27
35
|
once(event: 'close', listener: () => void): this;
|
|
36
|
+
once(event: 'spawn', listener: () => void): this;
|
|
37
|
+
once(event: 'error', listener: (error: unknown) => void): this;
|
|
28
38
|
kill(signal?: NodeJS.Signals | number): boolean;
|
|
29
39
|
}
|
|
30
40
|
type SpawnProcessFactory = (command: string, args: readonly string[]) => SpawnProcess;
|
|
@@ -32,6 +42,13 @@ type SpawnProcessFactory = (command: string, args: readonly string[]) => SpawnPr
|
|
|
32
42
|
export interface LocalStreamingAudioPlayerOptions {
|
|
33
43
|
readonly env?: NodeJS.ProcessEnv;
|
|
34
44
|
readonly spawnProcess?: SpawnProcessFactory;
|
|
45
|
+
/**
|
|
46
|
+
* Injected player command — the test seam companion to `spawnProcess`.
|
|
47
|
+
* When set, the constructor uses it verbatim instead of scanning PATH, so
|
|
48
|
+
* deterministic fake-sink tests never depend on mpv/ffplay being installed
|
|
49
|
+
* on the machine running them. Pass null to model "no player found".
|
|
50
|
+
*/
|
|
51
|
+
readonly command?: StreamingAudioPlayerCommand | null;
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
|
|
@@ -40,7 +57,9 @@ export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
|
|
|
40
57
|
private readonly spawnProcess: SpawnProcessFactory;
|
|
41
58
|
|
|
42
59
|
constructor(options: LocalStreamingAudioPlayerOptions = {}) {
|
|
43
|
-
this.command =
|
|
60
|
+
this.command = options.command !== undefined
|
|
61
|
+
? options.command
|
|
62
|
+
: resolveStreamingAudioPlayerCommand(options.env ?? process.env);
|
|
44
63
|
this.spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
45
64
|
}
|
|
46
65
|
|
|
@@ -67,12 +86,27 @@ export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
|
|
|
67
86
|
options.signal?.addEventListener('abort', abort, { once: true });
|
|
68
87
|
|
|
69
88
|
try {
|
|
89
|
+
// Head survival: hold the first audio byte until the sink has actually
|
|
90
|
+
// started (its 'spawn' event) instead of writing into a process that has
|
|
91
|
+
// not exec'd yet. Writing before the player is up is the spawn race that
|
|
92
|
+
// clipped the beginning of playback. A spawn failure rejects here so the
|
|
93
|
+
// caller can report it honestly rather than swallowing a dead player.
|
|
94
|
+
await awaitReady(proc, options.signal);
|
|
95
|
+
if (options.signal?.aborted) return;
|
|
96
|
+
|
|
70
97
|
for await (const chunk of chunks) {
|
|
71
98
|
if (options.signal?.aborted) break;
|
|
72
99
|
if (chunk.data.byteLength === 0) continue;
|
|
73
100
|
await writeStdin(proc, chunk.data);
|
|
74
101
|
}
|
|
75
|
-
|
|
102
|
+
|
|
103
|
+
// An intentional interrupt (turn cancel / quit chord / /tts stop) has
|
|
104
|
+
// already torn the process down via `abort` and must cut immediately —
|
|
105
|
+
// do not wait on a graceful drain. A natural end-of-speech, by contrast,
|
|
106
|
+
// closes stdin and waits for the sink to play out every buffered sample
|
|
107
|
+
// so the tail of the response is never truncated.
|
|
108
|
+
if (options.signal?.aborted) return;
|
|
109
|
+
try { proc.stdin.end(); } catch { /* ignore */ }
|
|
76
110
|
await waitForExit(proc);
|
|
77
111
|
} finally {
|
|
78
112
|
options.signal?.removeEventListener('abort', abort);
|
|
@@ -87,14 +121,33 @@ export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
|
|
|
87
121
|
try { proc.stdin.destroy(); } catch { /* ignore */ }
|
|
88
122
|
try { proc.kill('SIGTERM'); } catch { /* ignore */ }
|
|
89
123
|
}
|
|
124
|
+
|
|
125
|
+
waitForDrain(timeoutMs: number): Promise<void> {
|
|
126
|
+
const proc = this.activeProcess;
|
|
127
|
+
if (!proc) return Promise.resolve();
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
let settled = false;
|
|
130
|
+
const settle = () => {
|
|
131
|
+
if (settled) return;
|
|
132
|
+
settled = true;
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
resolve();
|
|
135
|
+
};
|
|
136
|
+
const timer = setTimeout(settle, timeoutMs);
|
|
137
|
+
proc.once('close', settle);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
90
140
|
}
|
|
91
141
|
|
|
92
142
|
export function resolveStreamingAudioPlayerCommand(env: NodeJS.ProcessEnv = process.env): StreamingAudioPlayerCommand | null {
|
|
93
143
|
const mpv = findExecutable('mpv', env);
|
|
94
144
|
if (mpv) {
|
|
145
|
+
// No --cache=no: mpv's read-ahead cache buffers the incoming pipe so the
|
|
146
|
+
// opening audio survives device-open latency and network jitter instead of
|
|
147
|
+
// underrunning while the output device is still spinning up.
|
|
95
148
|
return {
|
|
96
149
|
command: mpv,
|
|
97
|
-
args: ['--no-terminal', '--really-quiet', '--force-window=no', '
|
|
150
|
+
args: ['--no-terminal', '--really-quiet', '--force-window=no', '-'],
|
|
98
151
|
label: 'mpv',
|
|
99
152
|
};
|
|
100
153
|
}
|
|
@@ -102,18 +155,51 @@ export function resolveStreamingAudioPlayerCommand(env: NodeJS.ProcessEnv = proc
|
|
|
102
155
|
if (ffplay) {
|
|
103
156
|
return {
|
|
104
157
|
command: ffplay,
|
|
105
|
-
args:
|
|
158
|
+
args: FFPLAY_BASE_ARGS,
|
|
106
159
|
label: 'ffplay',
|
|
107
160
|
};
|
|
108
161
|
}
|
|
109
162
|
return null;
|
|
110
163
|
}
|
|
111
164
|
|
|
165
|
+
// ffplay -autoexit quits as soon as its input ends, before the audio output
|
|
166
|
+
// buffer has drained — that clips the tail of the response. `apad` appends a
|
|
167
|
+
// short run of silence so the real audio is fully played out and only the
|
|
168
|
+
// trailing silence gets trimmed.
|
|
169
|
+
const FFPLAY_APAD = ['-af', 'apad=pad_dur=0.3'] as const;
|
|
170
|
+
const FFPLAY_BASE_ARGS = ['-nodisp', '-autoexit', '-loglevel', 'error', ...FFPLAY_APAD, '-i', 'pipe:0'] as const;
|
|
171
|
+
|
|
112
172
|
function buildPlayerArgs(command: StreamingAudioPlayerCommand, format?: string): readonly string[] {
|
|
113
173
|
if (command.label !== 'ffplay' || !format) return command.args;
|
|
114
174
|
const normalized = format.trim().toLowerCase();
|
|
115
175
|
if (!normalized || normalized.includes('/')) return command.args;
|
|
116
|
-
return ['-nodisp', '-autoexit', '-loglevel', 'error', '-f', normalized, '-i', 'pipe:0'];
|
|
176
|
+
return ['-nodisp', '-autoexit', '-loglevel', 'error', ...FFPLAY_APAD, '-f', normalized, '-i', 'pipe:0'];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* awaitReady — resolves once the spawned player has actually started (its
|
|
181
|
+
* 'spawn' event), rejects if it fails to start ('error'), and resolves early
|
|
182
|
+
* if the caller aborts during startup so an intentional interrupt is never
|
|
183
|
+
* blocked. This is the readiness gate that keeps the first audio byte from
|
|
184
|
+
* being written into a not-yet-running sink.
|
|
185
|
+
*/
|
|
186
|
+
function awaitReady(proc: SpawnProcess, signal?: AbortSignal): Promise<void> {
|
|
187
|
+
if (signal?.aborted) return Promise.resolve();
|
|
188
|
+
return new Promise<void>((resolve, reject) => {
|
|
189
|
+
let settled = false;
|
|
190
|
+
const settle = (action: () => void) => {
|
|
191
|
+
if (settled) return;
|
|
192
|
+
settled = true;
|
|
193
|
+
signal?.removeEventListener('abort', onAbort);
|
|
194
|
+
action();
|
|
195
|
+
};
|
|
196
|
+
const onSpawn = () => settle(resolve);
|
|
197
|
+
const onError = (error: unknown) => settle(() => reject(error instanceof Error ? error : new Error(String(error))));
|
|
198
|
+
const onAbort = () => settle(resolve);
|
|
199
|
+
proc.once('spawn', onSpawn);
|
|
200
|
+
proc.once('error', onError);
|
|
201
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
202
|
+
});
|
|
117
203
|
}
|
|
118
204
|
|
|
119
205
|
function findExecutable(name: string, env: NodeJS.ProcessEnv): string | null {
|
|
@@ -3,13 +3,15 @@ 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)[];
|
|
10
10
|
submitNextTurn(prompt: string): boolean;
|
|
11
11
|
/** Returns whether speech was actually active (see controller.stop). */
|
|
12
12
|
stop(message?: string): boolean;
|
|
13
|
+
/** Exit-path stop: lets the audio already playing drain, bounded (see controller.stopForExit). */
|
|
14
|
+
stopForExit(drainTimeoutMs?: number): Promise<void>;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface WireSpokenTurnRuntimeOptions {
|
|
@@ -35,10 +37,15 @@ export interface WireSpokenTurnRuntimeOptions {
|
|
|
35
37
|
*/
|
|
36
38
|
export function wireSpokenTurnRuntime(options: WireSpokenTurnRuntimeOptions): SpokenTurnRuntime {
|
|
37
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.
|
|
38
44
|
const controller = new SpokenTurnController({
|
|
39
45
|
voiceService: options.voiceService,
|
|
40
46
|
configManager: options.configManager,
|
|
41
|
-
player,
|
|
47
|
+
sink: player,
|
|
48
|
+
source: 'goodvibes-tui',
|
|
42
49
|
notify: options.notify,
|
|
43
50
|
});
|
|
44
51
|
|
|
@@ -63,5 +70,6 @@ export function wireSpokenTurnRuntime(options: WireSpokenTurnRuntimeOptions): Sp
|
|
|
63
70
|
unsubs,
|
|
64
71
|
submitNextTurn: (prompt) => controller.submitNextTurn(prompt),
|
|
65
72
|
stop: (message) => controller.stop(message),
|
|
73
|
+
stopForExit: (drainTimeoutMs) => controller.stopForExit(drainTimeoutMs),
|
|
66
74
|
};
|
|
67
75
|
}
|
|
@@ -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') {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* credential-availability.ts —
|
|
2
|
+
* credential-availability.ts — client-side credential-status read.
|
|
3
3
|
*
|
|
4
4
|
* When the TUI acts as a CLIENT of an adopted external daemon (the host-service
|
|
5
5
|
* `mode === 'external'` topology — see runtime/bootstrap.ts), provider/model/secret
|
package/src/config/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function getConfiguredSystemPrompt(configManager: Pick<ConfigManager, 'ge
|
|
|
56
56
|
|
|
57
57
|
export { getConfiguredApiKeys, resolveApiKeys } from '@pellux/goodvibes-sdk/platform/config';
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// Client-side credential-status read (honest-degrade, status-only). When the
|
|
60
60
|
// TUI acts as a client of an adopted external daemon it reads provider/model/secret
|
|
61
61
|
// STATUS from that daemon's `credentials.get`, never from its own surfaceRoot store.
|
|
62
62
|
export {
|
|
@@ -8,7 +8,7 @@ import type { SpokenTurnRuntime } from '../audio/spoken-turn-wiring.ts';
|
|
|
8
8
|
* cap while still wiring `commandContext.isGenerating` (see W1.6 FIX 1).
|
|
9
9
|
*
|
|
10
10
|
* Returns whether SPEECH was actively stopped — a Ctrl+C that silenced live
|
|
11
|
-
* TTS is consumed by that job (
|
|
11
|
+
* TTS is consumed by that job (an earlier replay fix), same as a press that
|
|
12
12
|
* cleared a non-empty prompt; the quit chord starts from a quiet state.
|
|
13
13
|
*/
|
|
14
14
|
export function createCancelGeneration(
|
package/src/daemon/cli.ts
CHANGED
|
@@ -163,7 +163,7 @@ async function main(): Promise<void> {
|
|
|
163
163
|
logger.info('daemon: --hostname flag applied', { hostname: cliFlags.hostname });
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
//
|
|
166
|
+
// Service lifecycle: `install-service` / `uninstall-service` / `service-status` manage
|
|
167
167
|
// the systemd USER unit for the shared daemon. They run BEFORE the daemon boots —
|
|
168
168
|
// no runtime/services are constructed — and exit with the honest result code.
|
|
169
169
|
const serviceSubcommand = cli.positionals[0];
|
|
@@ -204,7 +204,7 @@ async function main(): Promise<void> {
|
|
|
204
204
|
homeDirectory,
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
//
|
|
207
|
+
// Load persisted providers from disk so the provider registry is pre-populated
|
|
208
208
|
// on standalone daemon startup (same machinery the TUI uses after /scan).
|
|
209
209
|
const discoveryRoots = { homeDirectory, surfaceRoot: 'tui' };
|
|
210
210
|
const persistedProviders = loadPersistedProviders(discoveryRoots);
|
|
@@ -213,7 +213,7 @@ async function main(): Promise<void> {
|
|
|
213
213
|
logger.info('daemon: loaded persisted providers', { count: persistedProviders.length });
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
//
|
|
216
|
+
// Run a background LAN scan (non-blocking). Discovered servers are registered
|
|
217
217
|
// and persisted so subsequent daemon restarts benefit from the warm cache.
|
|
218
218
|
void scan().then((result) => {
|
|
219
219
|
if (result.servers.length > 0) {
|
|
@@ -245,7 +245,7 @@ async function main(): Promise<void> {
|
|
|
245
245
|
// If no explicit daemon token is set, use the companion token so mobile apps can connect.
|
|
246
246
|
const daemonHomeDir = join(homeDirectory, '.goodvibes', 'daemon');
|
|
247
247
|
const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
248
|
-
//
|
|
248
|
+
// Fix (TUI 0.19.20): remove stale pre-0.21.28 workspace-scoped operator
|
|
249
249
|
// token files so only the canonical <daemonHomeDir>/operator-tokens.json survives.
|
|
250
250
|
const prune = pruneStaleOperatorTokens({
|
|
251
251
|
daemonHomeDir,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* TUI wiring — `goodvibes-daemon install-service | uninstall-service
|
|
3
3
|
* | service-status`.
|
|
4
4
|
*
|
|
5
5
|
* The daemon is a SYSTEM SERVICE. These subcommands install it as a durable host
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* on Windows) so N surfaces share ONE daemon that survives reboots, instead of
|
|
8
8
|
* every surface spawning a session-scoped daemon.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* bespoke systemd shim) was dead code and got deleted
|
|
10
|
+
* Drift note: the SDK's older `systemd-user-service.ts` (a Linux-only,
|
|
11
|
+
* bespoke systemd shim) was dead code and got deleted from the SDK. The SDK's
|
|
12
12
|
* REAL wired service machinery — reached in production by the daemon's own HTTP
|
|
13
13
|
* `/api/service/*` routes via facade-composition.ts — is
|
|
14
14
|
* `PlatformServiceManager` (`@pellux/goodvibes-sdk/platform/daemon`): a single
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* stray "enabled" symlink may remain until `systemctl --user daemon-reload`
|
|
27
27
|
* (offered back as a suggested follow-up) or the next login cleans it up.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* A fourth subcommand, `migrate-service`, closes the earlier "detect and
|
|
30
30
|
* disclose only" inheritance (W3 Finding 4, below) with a GUIDED, CONSENTED
|
|
31
31
|
* takeover of the legacy `goodvibes-daemon.service` unit. Design constraints,
|
|
32
32
|
* all load-bearing:
|
|
@@ -77,9 +77,9 @@ export {
|
|
|
77
77
|
export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migration.ts';
|
|
78
78
|
|
|
79
79
|
// ---------------------------------------------------------------------------
|
|
80
|
-
//
|
|
80
|
+
// Legacy-unit detection, plus guided migration.
|
|
81
81
|
//
|
|
82
|
-
// The prior
|
|
82
|
+
// The prior command installed the daemon's systemd unit under the
|
|
83
83
|
// literal name `goodvibes-daemon.service`. This module (rewired onto
|
|
84
84
|
// PlatformServiceManager, see the file banner above) manages a DIFFERENT
|
|
85
85
|
// unit name (`goodvibes`, SERVICE_NAME). A host that still has the legacy
|
|
@@ -243,7 +243,7 @@ function failed(action: 'install' | 'uninstall' | 'status', status: ManagedServi
|
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
245
|
* `migrate-service`: the guided, consented takeover of the legacy
|
|
246
|
-
* `goodvibes-daemon.service` unit
|
|
246
|
+
* `goodvibes-daemon.service` unit. Thin wrapper over
|
|
247
247
|
* `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`) — see
|
|
248
248
|
* that module for the design constraints (never auto-migrate,
|
|
249
249
|
* new-up-then-old-down, adopt-or-warn/never kill an unrecognized process,
|
|
@@ -314,7 +314,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
314
314
|
const extra: string[] = [];
|
|
315
315
|
if (stopped.actionError) extra.push(`(it may not have been running: ${stopped.actionError})`);
|
|
316
316
|
// W3 Finding 4: this command only ever touches the TRACKED unit
|
|
317
|
-
// (whatever name/path actually resolved
|
|
317
|
+
// (whatever name/path actually resolved — not necessarily the
|
|
318
318
|
// SERVICE_NAME constant) above — say so explicitly when a legacy unit
|
|
319
319
|
// also exists, so its continued presence is never a silent surprise.
|
|
320
320
|
if (legacy.present) extra.push(legacyUnitNote(legacy, resolveManagedUnitName(uninstalled)));
|
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';
|
|
@@ -192,16 +193,16 @@ export interface CommandSessionServices {
|
|
|
192
193
|
* fresh Orchestrator's zeroed default (W0.9).
|
|
193
194
|
*/
|
|
194
195
|
readonly hydrateSessionUsage?: () => void;
|
|
195
|
-
/**
|
|
196
|
+
/** The orchestration engine's command-facing facade — see runtime/workstream-services.ts. */
|
|
196
197
|
readonly workstreamEngine?: import('../runtime/workstream-services.ts').WorkstreamCommandService;
|
|
197
|
-
/**
|
|
198
|
+
/** The repo source-tree code index — see runtime/code-index-services.ts. */
|
|
198
199
|
readonly codeIndexStore?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexStore;
|
|
199
|
-
/**
|
|
200
|
+
/** Tool-site reindex scheduler — `/codebase status` reports its last activity. */
|
|
200
201
|
readonly codeIndexReindexScheduler?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexReindexScheduler;
|
|
201
|
-
/**
|
|
202
|
+
/** Whether the (default-off) `agent-passive-code-injection` flag is on — for `/codebase status`. */
|
|
202
203
|
readonly isPassiveCodeInjectionFlagEnabled?: () => boolean;
|
|
203
204
|
/**
|
|
204
|
-
*
|
|
205
|
+
* The MAIN interactive session's per-turn passive-injection
|
|
205
206
|
* honesty ring — `Orchestrator.getTurnInjections()`, the main-session
|
|
206
207
|
* counterpart to `AgentRecord.turnInjections` (wo801). `/recall injections`
|
|
207
208
|
* with no agent id reads this. Optional so command contexts built without an
|
|
@@ -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;
|
|
@@ -10,7 +10,7 @@ export function registerConfigCommand(registry: CommandRegistry): void {
|
|
|
10
10
|
usage: '[category|key] | set <key> <value>',
|
|
11
11
|
argsHint: '[category|key] | set <key> <value>',
|
|
12
12
|
handler(args, ctx) {
|
|
13
|
-
//
|
|
13
|
+
// An earlier replay fix: `/config set <key> <value>` used to fall through to
|
|
14
14
|
// the workspace with the assignment silently ignored — the dishonest-
|
|
15
15
|
// fallthrough class. `set` is now a real verb.
|
|
16
16
|
if (args[0] === 'set') {
|
|
@@ -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'}`);
|
|
@@ -29,7 +29,7 @@ import { VALID_CLASSES, VALID_REVIEW_STATES, VALID_SCOPES } from './recall-share
|
|
|
29
29
|
* src/input/commands/session-content.ts) with no modal surface — so that
|
|
30
30
|
* work order deliberately did NOT touch /memory and used /recall instead.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
32
|
+
* Update from the core-verb naming pass (MEMORY fragmentation — worst-class
|
|
33
33
|
* collision #2): the agent's own `/memory` command was a plain alias for its
|
|
34
34
|
* `/recall`-equivalent the whole time, meaning "/memory" meant two unrelated
|
|
35
35
|
* things depending which surface you were on. The session-notes command was
|
|
@@ -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':
|
|
@@ -262,7 +262,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
|
|
|
262
262
|
return;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
// Defense (
|
|
265
|
+
// Defense (review finding): a single verb-looking token is almost never a
|
|
266
266
|
// real planning goal — it is a mistyped or removed subcommand. The
|
|
267
267
|
// Planning modal used to dispatch `/plan dismiss`, which has no
|
|
268
268
|
// subcommand and silently fell through to this free-form branch, seeding
|