@pellux/goodvibes-agent 1.5.7 → 1.5.9
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 +16 -3
- package/README.md +1 -1
- package/dist/package/main.js +309 -150
- package/package.json +1 -1
- package/src/agent/memory-prompt.ts +3 -3
- package/src/agent/operator-actions.ts +1 -1
- package/src/agent/prompt-context-receipts.ts +3 -3
- package/src/agent/session-registration.ts +1 -1
- package/src/agent/vibe-file.ts +5 -5
- package/src/audio/player.ts +91 -5
- package/src/audio/spoken-turn-controller.ts +281 -29
- package/src/audio/spoken-turn-wiring.ts +13 -2
- package/src/cli/local-library-command-shared.ts +1 -1
- package/src/cli/memory-command.ts +9 -6
- package/src/cli/resume-relaunch-notice.ts +1 -1
- package/src/cli/tui-startup.ts +1 -1
- package/src/config/credential-status.ts +1 -1
- package/src/config/index.ts +1 -1
- package/src/config/secrets.ts +1 -1
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/system-message-noise.ts +3 -3
- package/src/core/thinking-overlay.ts +1 -1
- package/src/input/agent-workspace-basic-command-editor-submission.ts +1 -1
- package/src/input/agent-workspace-basic-command-editors.ts +1 -1
- package/src/input/agent-workspace-calendar-connect-editor.ts +1 -1
- package/src/input/agent-workspace-calendar-oauth-editor.ts +1 -1
- package/src/input/agent-workspace-calendar-subscribe-editor.ts +1 -1
- package/src/input/agent-workspace-command-editor-engine.ts +2 -2
- package/src/input/agent-workspace-direct-editor-submission.ts +1 -1
- package/src/input/agent-workspace-email-connect-editor.ts +1 -1
- package/src/input/agent-workspace-live-counters.ts +1 -1
- package/src/input/agent-workspace-settings.ts +1 -1
- package/src/input/agent-workspace-snapshot-builders.ts +2 -2
- package/src/input/agent-workspace-snapshot-config.ts +1 -1
- package/src/input/agent-workspace-snapshot.ts +9 -9
- package/src/input/agent-workspace-types.ts +4 -4
- package/src/input/agent-workspace.ts +1 -1
- package/src/input/commands/calendar-connect-runtime.ts +1 -1
- package/src/input/commands/calendar-subscription-runtime.ts +2 -2
- package/src/input/commands/operator-actions-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/feed-context-factory.ts +4 -4
- package/src/input/handler-feed.ts +9 -9
- package/src/input/handler.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +3 -3
- package/src/input/settings-modal.ts +1 -1
- package/src/main.ts +28 -33
- package/src/renderer/agent-workspace-context-lines.ts +1 -1
- package/src/renderer/startup-theme-probe.ts +1 -1
- package/src/renderer/status-glyphs.ts +2 -2
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/terminal-escapes.ts +3 -2
- package/src/renderer/theme-mode-config.ts +2 -2
- package/src/renderer/theme.ts +4 -4
- package/src/renderer/thinking.ts +1 -1
- package/src/renderer/ui-factory.ts +1 -1
- package/src/renderer/ui-primitives.ts +2 -2
- package/src/runtime/bootstrap-core.ts +2 -2
- package/src/runtime/bootstrap.ts +3 -3
- package/src/runtime/calendar-boot-refresh.ts +1 -1
- package/src/runtime/lan-scan-consent.ts +1 -1
- package/src/runtime/services.ts +7 -7
- package/src/runtime/terminal-output-guard.ts +1 -1
- package/src/runtime/ui-services.ts +1 -1
- package/src/runtime/unhandled-rejection-guard.ts +41 -0
- package/src/shell/agent-workspace-fullscreen.ts +1 -1
- package/src/shell/terminal-focus-mode.ts +9 -9
- package/src/shell/ui-openers.ts +1 -1
- package/src/tools/agent-harness-operator-methods.ts +1 -1
- package/src/tools/agent-harness-personal-ops-discovery.ts +2 -2
- package/src/tools/agent-harness-personal-ops-lanes.ts +1 -1
- package/src/tools/agent-harness-personal-ops-types.ts +1 -1
- package/src/tools/agent-harness-prompt-context.ts +3 -3
- package/src/tools/agent-local-registry-memory.ts +1 -1
- package/src/tools/agent-operator-method-tool.ts +1 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
|
|
6
6
|
"type": "module",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MemoryRecord, MemoryRegistry, MemoryVectorStats } from '@pellux/goodvibes-sdk/platform/state';
|
|
2
|
-
//
|
|
2
|
+
// The recall-honesty floor + eligibility receipt now live in the SDK as
|
|
3
3
|
// the ONE cross-surface contract (memory-recall-contract.ts) instead of being defined
|
|
4
4
|
// per surface. This file re-exports them unchanged so every existing agent consumer
|
|
5
5
|
// keeps importing from './memory-prompt.ts', while the SDK is the single source of the
|
|
@@ -32,9 +32,9 @@ function formatMemoryLine(record: MemoryRecord): string {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Per-turn semantic ranking of an already-eligible memory set
|
|
35
|
+
* Per-turn semantic ranking of an already-eligible memory set.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
37
|
+
* Eligibility (confidence + reviewState + provenance) shipped as the hard trust
|
|
38
38
|
* gate but had no per-turn query to rank WITHIN that eligible set — records were only
|
|
39
39
|
* ever ordered by stored confidence/recency, regardless of whether they had anything to
|
|
40
40
|
* do with what the user actually just asked. `rankMemoryForTurn` never touches the gate
|
|
@@ -103,7 +103,7 @@ export const OPERATOR_ACTIONS: Record<OperatorActionId, OperatorActionDescriptor
|
|
|
103
103
|
pathTemplate: '/api/automation/jobs/{jobId}/run',
|
|
104
104
|
targetField: 'jobId',
|
|
105
105
|
},
|
|
106
|
-
//
|
|
106
|
+
// automation.jobs.pause/resume were retired (redundant with
|
|
107
107
|
// disable/enable — same {id,enabled} output, same semantics). The
|
|
108
108
|
// user-facing "pause"/"resume" verb is unchanged; only the wire action +
|
|
109
109
|
// path moved to the canonical disable/enable methods.
|
|
@@ -140,7 +140,7 @@ function receiptSegment(input: Omit<PromptContextReceiptSegment, 'approxTokens'>
|
|
|
140
140
|
|
|
141
141
|
function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput): readonly PromptContextReceiptSegment[] {
|
|
142
142
|
const vibe = discoverVibeFiles(input.shellPaths);
|
|
143
|
-
//
|
|
143
|
+
// The VIBE prompt is a PROJECTION of persona/constraint records, not a
|
|
144
144
|
// re-read of the files (discoverVibeFiles above stays for the file-discovery receipt).
|
|
145
145
|
const vibePrompt = buildVibeProjectionPrompt(input.memoryRegistry) ?? '';
|
|
146
146
|
const projectContext = discoverProjectContextFiles(input.shellPaths);
|
|
@@ -234,7 +234,7 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
|
|
|
234
234
|
suppressedCount: suppressedMemory.length,
|
|
235
235
|
promptChars: memoryPrompt.length,
|
|
236
236
|
promptText: memoryPrompt,
|
|
237
|
-
// Honest degrade note
|
|
237
|
+
// Honest degrade note: when per-turn relevance scoring did not
|
|
238
238
|
// run — no active-turn text, semantic index unavailable, or no vector match —
|
|
239
239
|
// say so instead of silently presenting the fallback confidence/recency order as
|
|
240
240
|
// if it were a relevance ranking.
|
|
@@ -245,7 +245,7 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
|
|
|
245
245
|
class: record.cls,
|
|
246
246
|
confidence: record.confidence,
|
|
247
247
|
reason: describeMemoryPromptEligibility(record).reason,
|
|
248
|
-
// Per-turn relevance
|
|
248
|
+
// Per-turn relevance: honest wording, only present when actually scored.
|
|
249
249
|
// F7a: the raw percent is paired with a qualitative band (see relevanceBand)
|
|
250
250
|
// so a genuinely-lower-but-real score like "28%" doesn't read as noise.
|
|
251
251
|
...(memoryRanking.scored ? { relevance: `relevance to this turn: ${memoryRanking.relevanceById.get(record.id) ?? 0}% (${relevanceBand(memoryRanking.relevanceById.get(record.id) ?? 0)})` } : {}),
|
|
@@ -86,7 +86,7 @@ export interface SessionRecordSummary {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* Honest outcome discriminator. 'still_closed' is the
|
|
89
|
+
* Honest outcome discriminator. 'still_closed' is the heartbeat-resurrection
|
|
90
90
|
* guard: a 200 that carries conflict:{status:'closed'} with reopened:false MUST NOT
|
|
91
91
|
* be read as 'registered' or 'reopened'.
|
|
92
92
|
*/
|
package/src/agent/vibe-file.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ShellPathService } from '@/runtime/index.ts';
|
|
|
5
5
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
6
6
|
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
7
7
|
import { parseMarkdownFrontmatter, stripMarkdownFrontmatter } from './markdown-frontmatter.ts';
|
|
8
|
-
//
|
|
8
|
+
// VIBE.md is now a PROJECTION of persona/constraint records in the
|
|
9
9
|
// canonical memory store, not a separate source of truth. renderVibeProjection emits
|
|
10
10
|
// the same '## VIBE.md' block from those records (caveat preserved); the file is
|
|
11
11
|
// demoted to an import/export FORMAT folded in via vibeBodyToConstraintOptions.
|
|
@@ -213,7 +213,7 @@ export function buildVibePrompt(shellPaths: AgentVibePaths): string | null {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
/**
|
|
216
|
-
*
|
|
216
|
+
* The VIBE.md prompt block as a PROJECTION of persona/constraint records.
|
|
217
217
|
*
|
|
218
218
|
* This is the store-sourced replacement for buildVibePrompt (which reads the file
|
|
219
219
|
* directly). It renders the same '## GoodVibes Agent VIBE.md' block — including the
|
|
@@ -226,7 +226,7 @@ export function buildVibeProjectionPrompt(memoryRecords: { getAll(): readonly Me
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
|
-
*
|
|
229
|
+
* Fold discovered VIBE.md files into the store as persona/constraint
|
|
230
230
|
* records — the file demoted to an IMPORT FORMAT. Each bullet becomes one record so a
|
|
231
231
|
* later single-record edit changes exactly one projected line. Secret-like content is
|
|
232
232
|
* already rejected by discoverVibeFiles (readVibeCandidate → assertNoSecretLikeText),
|
|
@@ -257,7 +257,7 @@ export async function importVibeFilesIntoMemory(
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
/**
|
|
260
|
-
*
|
|
260
|
+
* The persisted marker that makes the VIBE.md → memory import a strictly
|
|
261
261
|
* ONE-TIME migration. Keyed by absolute file path → content hash, so importing the same
|
|
262
262
|
* VIBE.md twice is a no-op (re-import would create near-duplicate persona records), while
|
|
263
263
|
* a NEW project's VIBE.md still migrates exactly once. Mirrors the sessions.spine-folded
|
|
@@ -300,7 +300,7 @@ function hashVibeBody(body: string): string {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
/**
|
|
303
|
-
*
|
|
303
|
+
* Fold discovered VIBE.md files into persona/constraint records ONCE.
|
|
304
304
|
* Guarded by a persisted path→hash marker so it never re-imports the same file (which
|
|
305
305
|
* would create near-duplicate persona records). Called at boot AFTER memoryStore.init().
|
|
306
306
|
* Returns the number of records created this run (0 when everything is already migrated).
|
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 {
|