@pellux/goodvibes-agent 1.5.9 → 1.6.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 +11 -0
- package/README.md +1 -1
- package/dist/package/main.js +11456 -4998
- package/docs/README.md +1 -1
- package/docs/voice-and-live-tts.md +47 -0
- package/package.json +2 -2
- package/src/agent/memory-prompt.ts +12 -1
- package/src/agent/prompt-context-receipts.ts +71 -6
- package/src/agent/vibe-file.ts +9 -5
- package/src/audio/spoken-turn-wiring.ts +3 -2
- package/src/cli/memory-command-wire.ts +373 -0
- package/src/cli/memory-command.ts +31 -22
- package/src/runtime/bootstrap-core.ts +2 -2
- package/src/runtime/bootstrap.ts +68 -0
- package/src/runtime/memory-spine-adoption.ts +50 -0
- package/src/runtime/memory-spine-rest-transport.ts +327 -0
- package/src/runtime/services.ts +33 -0
- package/src/tools/agent-local-registry-memory.ts +42 -11
- package/src/tools/agent-local-registry-tool.ts +5 -4
- package/src/version.ts +1 -1
- package/src/audio/spoken-turn-controller.ts +0 -455
- package/src/audio/text-chunker.ts +0 -110
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-command-wire.ts
|
|
3
|
+
*
|
|
4
|
+
* Split out of memory-command.ts to stay under the 800-line architecture cap.
|
|
5
|
+
* Owns the CLI's wire-first path onto the memory spine (SDK 1.2.0 full-detach
|
|
6
|
+
* catalog) — see the CLI ruling below for why this exists and what it
|
|
7
|
+
* deliberately still does NOT cover.
|
|
8
|
+
*/
|
|
9
|
+
import type { MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
|
|
10
|
+
import type { LocalMemoryStore, MemoryAccess } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
|
|
11
|
+
import { MemorySpineClient, createLocalMemoryAccess } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
|
|
12
|
+
import { assertNoSecretLikeMemoryText } from '../agent/memory-safety.ts';
|
|
13
|
+
import { formatAgentRecordReviewState } from '../agent/record-labels.ts';
|
|
14
|
+
import { createSpineConnectionResolver, createSpineRestProbe } from '../runtime/session-spine-rest-transport.ts';
|
|
15
|
+
import { createMemorySpineRestTransport } from '../runtime/memory-spine-rest-transport.ts';
|
|
16
|
+
import type { CliCommandOutput } from './types.ts';
|
|
17
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
18
|
+
import {
|
|
19
|
+
VALID_REVIEW_STATES,
|
|
20
|
+
csvOption,
|
|
21
|
+
failure,
|
|
22
|
+
filterFromOptions,
|
|
23
|
+
hasFlag,
|
|
24
|
+
isReviewState,
|
|
25
|
+
optionValue,
|
|
26
|
+
optionalScope,
|
|
27
|
+
parseConfidence,
|
|
28
|
+
parseOptions,
|
|
29
|
+
provenanceFromOptions,
|
|
30
|
+
readBundle,
|
|
31
|
+
renderRecord,
|
|
32
|
+
renderRecordList,
|
|
33
|
+
requireClass,
|
|
34
|
+
requireScope,
|
|
35
|
+
resolvePath,
|
|
36
|
+
success,
|
|
37
|
+
writeBundle,
|
|
38
|
+
type MemoryListData,
|
|
39
|
+
type MemorySearchData,
|
|
40
|
+
} from './memory-command.ts';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* CLI RULING (memory-spine adoption, SDK 1.2.0 full-detach catalog).
|
|
44
|
+
*
|
|
45
|
+
* The CLI is a one-shot process invoked by `goodvibes-agent memory ...` with no
|
|
46
|
+
* lasting daemon context — it doesn't get to sit through a boot-time reachability
|
|
47
|
+
* probe like the interactive runtime does. But the daemon's canonical store is
|
|
48
|
+
* single-writer: if a daemon IS running and owns that store, this process opening
|
|
49
|
+
* the same sqlite file underneath it and calling `store.save()` at the end (see
|
|
50
|
+
* `withMemory` in memory-command.ts, unconditional in its `finally`) would race the
|
|
51
|
+
* daemon's own writes and silently drop whichever side saved last. Staying
|
|
52
|
+
* "local-direct always" would be safe ONLY if no daemon could ever be running
|
|
53
|
+
* concurrently — which is not true for this product (Connected Host is the normal
|
|
54
|
+
* mode, not the exception).
|
|
55
|
+
*
|
|
56
|
+
* So the honest resolution: for every subcommand that has a wire-covered
|
|
57
|
+
* equivalent on `MemoryAccess` (the five 1.1.0 core verbs plus the 1.2.0 extended
|
|
58
|
+
* verbs list/update/link/linksFor/searchSemantic/exportBundle/importBundle — see
|
|
59
|
+
* memory-spine-rest-transport.ts), the CLI probes for a reachable daemon FIRST and,
|
|
60
|
+
* when one answers, runs the ENTIRE command over the wire and never opens the local
|
|
61
|
+
* store file at all. Only when the probe finds no daemon does it fall back to the
|
|
62
|
+
* existing direct-local path.
|
|
63
|
+
*
|
|
64
|
+
* 1.2.0 closes most of the prior gap: `show` (get + linksFor), `link`, `export`,
|
|
65
|
+
* `import`, and the `search --semantic`/`--vector` path, plus `promote` (which is
|
|
66
|
+
* not a distinct wire verb — it is `update({ scope })`, per the SDK's full-detach
|
|
67
|
+
* ruling) now all run over the wire when a daemon is adopted.
|
|
68
|
+
*
|
|
69
|
+
* A real, stated gap remains: `queue` (reviewQueue) and `vector` (vectorStats /
|
|
70
|
+
* doctor / rebuild) have no route on this CLI's wire transport by deliberate
|
|
71
|
+
* choice — vector-index diagnostics are maintenance a store performs on its OWN
|
|
72
|
+
* index, so they stay local-direct honestly rather than reporting a daemon's index
|
|
73
|
+
* health as if it were this process's own. Those two subcommands still run
|
|
74
|
+
* local-direct even while a daemon is adopted; that is a known, named scope limit,
|
|
75
|
+
* not a silent oversight.
|
|
76
|
+
*/
|
|
77
|
+
const WIRE_STORE_LABEL = 'wire:connected-daemon';
|
|
78
|
+
const CLI_PROBE_TIMEOUT_MS = 800;
|
|
79
|
+
|
|
80
|
+
const WIRE_ELIGIBLE_SUBCOMMANDS = new Set([
|
|
81
|
+
'list', 'ls',
|
|
82
|
+
'add', 'create',
|
|
83
|
+
'show', 'get',
|
|
84
|
+
'review',
|
|
85
|
+
'stale',
|
|
86
|
+
'contradict', 'contradicted',
|
|
87
|
+
'promote',
|
|
88
|
+
'link',
|
|
89
|
+
'delete', 'remove', 'rm',
|
|
90
|
+
'export', 'handoff-export', 'share',
|
|
91
|
+
'import', 'handoff-import',
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
/** `search`/`find` covers both the literal path (honestSearch) and, since 1.2.0, the `--semantic`/`--vector` path (searchSemantic) — both are wire-covered verbs. */
|
|
95
|
+
function isWireEligibleMemorySubcommand(normalized: string): boolean {
|
|
96
|
+
return WIRE_ELIGIBLE_SUBCOMMANDS.has(normalized) || normalized === 'search' || normalized === 'find';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A `LocalMemoryStore` stub that must never actually run. `MemorySpineClient`
|
|
101
|
+
* routes EVERY op to the transport once constructed with one attached (mode is
|
|
102
|
+
* 'client' immediately — see the SDK's one-writer enforcement ruling in
|
|
103
|
+
* client.ts), so this stub exists only to satisfy the constructor's required
|
|
104
|
+
* `local` argument without opening the CLI's own copy of the store file — the
|
|
105
|
+
* exact single-writer race the whole wire-first path exists to avoid. A call
|
|
106
|
+
* that somehow reached it would be a bug in that routing guarantee, so it fails
|
|
107
|
+
* loudly rather than silently reading/writing a file this process must not touch.
|
|
108
|
+
*/
|
|
109
|
+
function neverReachedLocalMemoryStore(): LocalMemoryStore {
|
|
110
|
+
const unreachable = (): never => {
|
|
111
|
+
throw new Error('memory spine: CLI wire mode reached the local store stub — this must never happen while a transport is attached.');
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
add: unreachable,
|
|
115
|
+
honestSearch: unreachable,
|
|
116
|
+
get: unreachable,
|
|
117
|
+
review: unreachable,
|
|
118
|
+
delete: unreachable,
|
|
119
|
+
search: unreachable,
|
|
120
|
+
searchSemantic: unreachable,
|
|
121
|
+
update: unreachable,
|
|
122
|
+
link: unreachable,
|
|
123
|
+
linksFor: unreachable,
|
|
124
|
+
reviewQueue: unreachable,
|
|
125
|
+
exportBundle: unreachable,
|
|
126
|
+
importBundle: unreachable,
|
|
127
|
+
vectorStats: unreachable,
|
|
128
|
+
doctor: unreachable,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Probes for a reachable connected daemon and, only when one answers, returns the wire MemoryAccess. Returns null (never throws) so the caller can fall back to local. */
|
|
133
|
+
async function resolveWireMemoryAccess(runtime: CliCommandRuntime): Promise<MemoryAccess | null> {
|
|
134
|
+
const resolveConnection = createSpineConnectionResolver(runtime.configManager, runtime.homeDirectory);
|
|
135
|
+
const probe = createSpineRestProbe({ resolveConnection, probeTimeoutMs: CLI_PROBE_TIMEOUT_MS });
|
|
136
|
+
const reachable = await probe();
|
|
137
|
+
if (!reachable) return null;
|
|
138
|
+
const transport = createMemorySpineRestTransport({ resolveConnection });
|
|
139
|
+
// MemorySpineClient (not the raw transport) is the MemoryAccess: the raw
|
|
140
|
+
// REST transport's type permits missing extended verbs (MemoryTransport =
|
|
141
|
+
// MemoryCoreAccess & Partial<MemoryExtendedAccess>) even though this specific
|
|
142
|
+
// transport implements every one the CLI calls; routing through the client
|
|
143
|
+
// gets full MemoryAccess typing plus its honest-rejection behavior for any
|
|
144
|
+
// verb a future older daemon doesn't support.
|
|
145
|
+
return new MemorySpineClient({ local: createLocalMemoryAccess(neverReachedLocalMemoryStore()), transport });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function handleListWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
149
|
+
const options = parseOptions(args);
|
|
150
|
+
const positionalClass = options.positionals[0];
|
|
151
|
+
const filter = filterFromOptions(options, 50);
|
|
152
|
+
if (positionalClass !== undefined) filter.cls = requireClass(positionalClass);
|
|
153
|
+
const { records } = await memorySpine.honestSearch(filter);
|
|
154
|
+
const data: MemoryListData = { path: WIRE_STORE_LABEL, records, filter };
|
|
155
|
+
return success(runtime, 'agent.memory.list', data, renderRecordList('Agent memory', WIRE_STORE_LABEL, records));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function handleSearchWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
159
|
+
const options = parseOptions(args);
|
|
160
|
+
const query = options.positionals.join(' ').trim();
|
|
161
|
+
if (!query) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory search <query> [--semantic] [--cls <class>] [--scope <scope>] [--limit <n>]', 2);
|
|
162
|
+
const filter = filterFromOptions(options, 20);
|
|
163
|
+
filter.query = query;
|
|
164
|
+
const semantic = hasFlag(options, 'semantic') || hasFlag(options, 'vector');
|
|
165
|
+
if (semantic) filter.semantic = true;
|
|
166
|
+
if (semantic) {
|
|
167
|
+
// 1.2.0: searchSemantic is a wire-covered extended verb (memory.records.search-semantic).
|
|
168
|
+
const semanticResults = await memorySpine.searchSemantic(filter);
|
|
169
|
+
const records = semanticResults.map((entry) => entry.record);
|
|
170
|
+
const data: MemorySearchData = { path: WIRE_STORE_LABEL, records, filter, semantic: true, semanticResults };
|
|
171
|
+
return success(runtime, 'agent.memory.search', data, renderRecordList(`Agent memory matching "${query}"`, WIRE_STORE_LABEL, records, semanticResults));
|
|
172
|
+
}
|
|
173
|
+
const { records } = await memorySpine.honestSearch(filter);
|
|
174
|
+
const data: MemorySearchData = { path: WIRE_STORE_LABEL, records, filter, semantic: false, semanticResults: [] };
|
|
175
|
+
return success(runtime, 'agent.memory.search', data, renderRecordList(`Agent memory matching "${query}"`, WIRE_STORE_LABEL, records));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function handleShowWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
179
|
+
const id = args[0];
|
|
180
|
+
if (!id) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory show <id>', 2);
|
|
181
|
+
const record = await memorySpine.get(id);
|
|
182
|
+
if (!record) return failure(runtime, 'memory_not_found', `Memory record not found ${id}`, 1);
|
|
183
|
+
const links = await memorySpine.linksFor(id);
|
|
184
|
+
return success(runtime, 'agent.memory.show', { record, links }, renderRecord(record, links));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function handlePromoteWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
188
|
+
const options = parseOptions(args);
|
|
189
|
+
const [id, scopeRaw] = options.positionals;
|
|
190
|
+
if (!id || !scopeRaw) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory promote <id> <session|project|team> --yes', 2);
|
|
191
|
+
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to promote memory record ${id} without --yes.`, 2);
|
|
192
|
+
// "Promote" is not a distinct wire verb — it is a scope edit, per the SDK's
|
|
193
|
+
// full-detach ruling (a scope promotion is update({ scope })).
|
|
194
|
+
//
|
|
195
|
+
// `update` now returns null ONLY for a genuine record-miss (a
|
|
196
|
+
// MEMORY_RECORD_NOT_FOUND 404); a daemon that does not serve the update verb
|
|
197
|
+
// (an older daemon, a route-not-found 404) makes it THROW an honest
|
|
198
|
+
// "verb unavailable" error that surfaces via handleMemoryCommand's errorOutput —
|
|
199
|
+
// it is no longer folded to null here and mislabelled as "record not found".
|
|
200
|
+
const record = await memorySpine.update(id, { scope: requireScope(scopeRaw) });
|
|
201
|
+
if (!record) return failure(runtime, 'memory_not_found', `Memory record not found ${id}`, 1);
|
|
202
|
+
return success(runtime, 'agent.memory.promote', record, [
|
|
203
|
+
'Agent memory promoted',
|
|
204
|
+
` id ${record.id}`,
|
|
205
|
+
` scope ${record.scope}`,
|
|
206
|
+
].join('\n'));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function handleLinkWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
210
|
+
const options = parseOptions(args);
|
|
211
|
+
const [fromId, toId, relation] = options.positionals;
|
|
212
|
+
if (!fromId || !toId || !relation) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory link <fromId> <toId> <relation> --yes', 2);
|
|
213
|
+
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to link memory records ${fromId} and ${toId} without --yes.`, 2);
|
|
214
|
+
const link = await memorySpine.link(fromId, toId, relation);
|
|
215
|
+
if (!link) return failure(runtime, 'memory_link_failed', 'Memory link failed; check that both records exist.', 1);
|
|
216
|
+
return success(runtime, 'agent.memory.link', link, [
|
|
217
|
+
'Agent memory linked',
|
|
218
|
+
` from ${fromId}`,
|
|
219
|
+
` to ${toId}`,
|
|
220
|
+
` relation ${relation}`,
|
|
221
|
+
].join('\n'));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function handleExportWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
225
|
+
const options = parseOptions(args);
|
|
226
|
+
const pathArg = options.positionals[0];
|
|
227
|
+
if (!pathArg) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory export <path> [--scope <scope>] [--cls <class>] --yes', 2);
|
|
228
|
+
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to export Agent memory bundle to ${pathArg} without --yes.`, 2);
|
|
229
|
+
const filter = filterFromOptions(options, 200);
|
|
230
|
+
const path = resolvePath(runtime, pathArg);
|
|
231
|
+
const bundle = await memorySpine.exportBundle(filter);
|
|
232
|
+
writeBundle(path, bundle);
|
|
233
|
+
return success(runtime, 'agent.memory.export', { path, bundle }, [
|
|
234
|
+
'Agent memory exported',
|
|
235
|
+
` records ${bundle.recordCount}`,
|
|
236
|
+
` links ${bundle.linkCount}`,
|
|
237
|
+
` path ${path}`,
|
|
238
|
+
].join('\n'));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function handleImportWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
242
|
+
const options = parseOptions(args);
|
|
243
|
+
const pathArg = options.positionals[0];
|
|
244
|
+
if (!pathArg) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory import <path> --yes', 2);
|
|
245
|
+
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to import Agent memory bundle from ${pathArg} without --yes.`, 2);
|
|
246
|
+
const path = resolvePath(runtime, pathArg);
|
|
247
|
+
const bundle = readBundle(path);
|
|
248
|
+
const result = await memorySpine.importBundle(bundle);
|
|
249
|
+
return success(runtime, 'agent.memory.import', { path, result }, [
|
|
250
|
+
`Agent memory imported: ${result.importedRecords} record${result.importedRecords === 1 ? '' : 's'}`,
|
|
251
|
+
` records: ${result.importedRecords}`,
|
|
252
|
+
` links: ${result.importedLinks}`,
|
|
253
|
+
` skipped: ${result.skippedRecords}`,
|
|
254
|
+
].join('\n'));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function handleAddWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
258
|
+
const options = parseOptions(args);
|
|
259
|
+
const [classRaw, ...summaryParts] = options.positionals;
|
|
260
|
+
const cls = requireClass(classRaw);
|
|
261
|
+
const summary = summaryParts.join(' ').trim();
|
|
262
|
+
if (!summary) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory add <class> <summary> [--scope <scope>] [--detail <text>] [--tags a,b]', 2);
|
|
263
|
+
const detail = optionValue(options, 'detail');
|
|
264
|
+
const tags = csvOption(options, 'tags');
|
|
265
|
+
assertNoSecretLikeMemoryText([summary, detail ?? '', ...(tags ?? [])]);
|
|
266
|
+
const reviewState = optionValue(options, 'review-state');
|
|
267
|
+
if (reviewState !== undefined && !isReviewState(reviewState)) {
|
|
268
|
+
return failure(runtime, 'invalid_memory_command', `Invalid review state "${reviewState}". Valid values ${VALID_REVIEW_STATES.join(', ')}`, 2);
|
|
269
|
+
}
|
|
270
|
+
const record = await memorySpine.add({
|
|
271
|
+
scope: optionalScope(optionValue(options, 'scope')) ?? 'project',
|
|
272
|
+
cls,
|
|
273
|
+
summary,
|
|
274
|
+
detail,
|
|
275
|
+
tags: tags === undefined ? undefined : [...tags],
|
|
276
|
+
provenance: [...provenanceFromOptions(options)],
|
|
277
|
+
review: {
|
|
278
|
+
state: reviewState,
|
|
279
|
+
confidence: parseConfidence(optionValue(options, 'confidence')),
|
|
280
|
+
reviewedBy: optionValue(options, 'by'),
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
return success(runtime, 'agent.memory.add', record, [
|
|
284
|
+
'Agent memory added',
|
|
285
|
+
` id ${record.id}`,
|
|
286
|
+
].join('\n'));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function handleReviewWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
290
|
+
const options = parseOptions(args);
|
|
291
|
+
const [id, stateRaw] = options.positionals;
|
|
292
|
+
if (!id || !stateRaw || !isReviewState(stateRaw)) {
|
|
293
|
+
return failure(runtime, 'invalid_memory_command', `Usage: goodvibes-agent memory review <id> <${VALID_REVIEW_STATES.join('|')}> [--confidence <0-100>]`, 2);
|
|
294
|
+
}
|
|
295
|
+
const record = await memorySpine.updateReview(id, {
|
|
296
|
+
state: stateRaw,
|
|
297
|
+
confidence: parseConfidence(optionValue(options, 'confidence')),
|
|
298
|
+
reviewedBy: optionValue(options, 'by') ?? 'operator',
|
|
299
|
+
staleReason: optionValue(options, 'reason'),
|
|
300
|
+
});
|
|
301
|
+
if (!record) return failure(runtime, 'memory_not_found', `Memory record not found ${id}`, 1);
|
|
302
|
+
return success(runtime, 'agent.memory.review', record, [
|
|
303
|
+
'Agent memory reviewed',
|
|
304
|
+
` id ${record.id}`,
|
|
305
|
+
` review ${formatAgentRecordReviewState(record.reviewState)}`,
|
|
306
|
+
` confidence ${record.confidence}%`,
|
|
307
|
+
].join('\n'));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function handleReviewShortcutWire(
|
|
311
|
+
runtime: CliCommandRuntime,
|
|
312
|
+
memorySpine: MemoryAccess,
|
|
313
|
+
state: Extract<MemoryReviewState, 'stale' | 'contradicted'>,
|
|
314
|
+
args: readonly string[],
|
|
315
|
+
): Promise<CliCommandOutput> {
|
|
316
|
+
const [id, ...reasonParts] = args;
|
|
317
|
+
if (!id || reasonParts.length === 0) {
|
|
318
|
+
return failure(runtime, 'invalid_memory_command', `Usage: goodvibes-agent memory ${state === 'stale' ? 'stale' : 'contradict'} <id> <reason>`, 2);
|
|
319
|
+
}
|
|
320
|
+
const record = await memorySpine.updateReview(id, {
|
|
321
|
+
state,
|
|
322
|
+
reviewedBy: 'operator',
|
|
323
|
+
staleReason: reasonParts.join(' '),
|
|
324
|
+
});
|
|
325
|
+
if (!record) return failure(runtime, 'memory_not_found', `Memory record not found ${id}`, 1);
|
|
326
|
+
return success(runtime, `agent.memory.${state}`, record, [
|
|
327
|
+
`Agent memory marked ${state}`,
|
|
328
|
+
` id ${record.id}`,
|
|
329
|
+
].join('\n'));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function handleDeleteWire(runtime: CliCommandRuntime, memorySpine: MemoryAccess, args: readonly string[]): Promise<CliCommandOutput> {
|
|
333
|
+
const options = parseOptions(args);
|
|
334
|
+
const id = options.positionals[0];
|
|
335
|
+
if (!id) return failure(runtime, 'invalid_memory_command', 'Usage: goodvibes-agent memory delete <id> --yes', 2);
|
|
336
|
+
if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete memory record ${id} without --yes.`, 2);
|
|
337
|
+
if (!(await memorySpine.delete(id))) return failure(runtime, 'memory_not_found', `Memory record not found ${id}`, 1);
|
|
338
|
+
return success(runtime, 'agent.memory.delete', { id }, [
|
|
339
|
+
`Agent memory deleted: ${id}`,
|
|
340
|
+
` id ${id}`,
|
|
341
|
+
].join('\n'));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Runs a wire-eligible subcommand over the memory spine when a daemon is reachable.
|
|
346
|
+
* Returns null (never a failure output) when the subcommand has no wire path, or
|
|
347
|
+
* when the probe finds no daemon — either way the caller falls through to the
|
|
348
|
+
* existing local-direct path unchanged. Once a wire call is actually in flight, a
|
|
349
|
+
* failure surfaces honestly (thrown, caught by handleMemoryCommand's try/catch) —
|
|
350
|
+
* it never silently retries against the local file after starting a wire attempt.
|
|
351
|
+
*/
|
|
352
|
+
export async function tryWireMemoryCommand(
|
|
353
|
+
runtime: CliCommandRuntime,
|
|
354
|
+
normalized: string,
|
|
355
|
+
rest: readonly string[],
|
|
356
|
+
): Promise<CliCommandOutput | null> {
|
|
357
|
+
if (!isWireEligibleMemorySubcommand(normalized)) return null;
|
|
358
|
+
const memorySpine = await resolveWireMemoryAccess(runtime);
|
|
359
|
+
if (!memorySpine) return null;
|
|
360
|
+
if (normalized === 'list' || normalized === 'ls') return handleListWire(runtime, memorySpine, rest);
|
|
361
|
+
if (normalized === 'search' || normalized === 'find') return handleSearchWire(runtime, memorySpine, rest);
|
|
362
|
+
if (normalized === 'add' || normalized === 'create') return handleAddWire(runtime, memorySpine, rest);
|
|
363
|
+
if (normalized === 'show' || normalized === 'get') return handleShowWire(runtime, memorySpine, rest);
|
|
364
|
+
if (normalized === 'review') return handleReviewWire(runtime, memorySpine, rest);
|
|
365
|
+
if (normalized === 'stale') return handleReviewShortcutWire(runtime, memorySpine, 'stale', rest);
|
|
366
|
+
if (normalized === 'contradict' || normalized === 'contradicted') return handleReviewShortcutWire(runtime, memorySpine, 'contradicted', rest);
|
|
367
|
+
if (normalized === 'promote') return handlePromoteWire(runtime, memorySpine, rest);
|
|
368
|
+
if (normalized === 'link') return handleLinkWire(runtime, memorySpine, rest);
|
|
369
|
+
if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') return handleDeleteWire(runtime, memorySpine, rest);
|
|
370
|
+
if (normalized === 'export' || normalized === 'handoff-export' || normalized === 'share') return handleExportWire(runtime, memorySpine, rest);
|
|
371
|
+
if (normalized === 'import' || normalized === 'handoff-import') return handleImportWire(runtime, memorySpine, rest);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
@@ -20,10 +20,13 @@ import { assertNoSecretLikeMemoryText } from '../agent/memory-safety.ts';
|
|
|
20
20
|
import { formatAgentRecordReference, formatAgentRecordReviewState } from '../agent/record-labels.ts';
|
|
21
21
|
import type { CliCommandOutput } from './types.ts';
|
|
22
22
|
import type { CliCommandRuntime } from './management.ts';
|
|
23
|
+
// Split out to stay under the architecture line cap; see its file header for the
|
|
24
|
+
// CLI ruling on when a memory subcommand goes over the wire vs local-direct.
|
|
25
|
+
import { tryWireMemoryCommand } from './memory-command-wire.ts';
|
|
23
26
|
|
|
24
27
|
const VALID_CLASSES: readonly MemoryClass[] = ['decision', 'constraint', 'incident', 'pattern', 'fact', 'risk', 'runbook', 'architecture', 'ownership'];
|
|
25
28
|
const VALID_SCOPES: readonly MemoryScope[] = ['session', 'project', 'team'];
|
|
26
|
-
const VALID_REVIEW_STATES: readonly MemoryReviewState[] = ['fresh', 'reviewed', 'stale', 'contradicted'];
|
|
29
|
+
export const VALID_REVIEW_STATES: readonly MemoryReviewState[] = ['fresh', 'reviewed', 'stale', 'contradicted'];
|
|
27
30
|
const VALID_PROVENANCE_KINDS: readonly ProvenanceLinkKind[] = ['session', 'turn', 'task', 'event', 'file'];
|
|
28
31
|
const VALUE_OPTIONS = new Set([
|
|
29
32
|
'by',
|
|
@@ -53,7 +56,7 @@ interface CommandFailure {
|
|
|
53
56
|
readonly error: string;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
interface ParsedOptions {
|
|
59
|
+
export interface ParsedOptions {
|
|
57
60
|
readonly values: ReadonlyMap<string, string>;
|
|
58
61
|
readonly flags: ReadonlySet<string>;
|
|
59
62
|
readonly positionals: readonly string[];
|
|
@@ -65,13 +68,13 @@ interface MemoryContext {
|
|
|
65
68
|
readonly path: string;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
interface MemoryListData {
|
|
71
|
+
export interface MemoryListData {
|
|
69
72
|
readonly path: string;
|
|
70
73
|
readonly records: readonly MemoryRecord[];
|
|
71
74
|
readonly filter: MemorySearchFilter;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
interface MemorySearchData extends MemoryListData {
|
|
77
|
+
export interface MemorySearchData extends MemoryListData {
|
|
75
78
|
readonly semantic: boolean;
|
|
76
79
|
readonly semanticResults: readonly MemorySemanticSearchResult[];
|
|
77
80
|
}
|
|
@@ -80,12 +83,12 @@ function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): s
|
|
|
80
83
|
return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
function success<TData>(runtime: CliCommandRuntime, kind: string, data: TData, text: string): CliCommandOutput {
|
|
86
|
+
export function success<TData>(runtime: CliCommandRuntime, kind: string, data: TData, text: string): CliCommandOutput {
|
|
84
87
|
const value: CommandSuccess<TData> = { ok: true, kind, data };
|
|
85
88
|
return { output: jsonOrText(runtime, value, text), exitCode: 0 };
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
function failure(runtime: CliCommandRuntime, kind: string, error: string, exitCode: number): CliCommandOutput {
|
|
91
|
+
export function failure(runtime: CliCommandRuntime, kind: string, error: string, exitCode: number): CliCommandOutput {
|
|
89
92
|
const value: CommandFailure = { ok: false, kind, error };
|
|
90
93
|
return {
|
|
91
94
|
output: runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : error,
|
|
@@ -93,7 +96,7 @@ function failure(runtime: CliCommandRuntime, kind: string, error: string, exitCo
|
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
function parseOptions(args: readonly string[]): ParsedOptions {
|
|
99
|
+
export function parseOptions(args: readonly string[]): ParsedOptions {
|
|
97
100
|
const values = new Map<string, string>();
|
|
98
101
|
const flags = new Set<string>();
|
|
99
102
|
const positionals: string[] = [];
|
|
@@ -124,18 +127,18 @@ function parseOptions(args: readonly string[]): ParsedOptions {
|
|
|
124
127
|
return { values, flags, positionals };
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
function optionValue(options: ParsedOptions, name: string): string | undefined {
|
|
130
|
+
export function optionValue(options: ParsedOptions, name: string): string | undefined {
|
|
128
131
|
const value = options.values.get(name);
|
|
129
132
|
if (value === undefined) return undefined;
|
|
130
133
|
const trimmed = value.trim();
|
|
131
134
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
function hasFlag(options: ParsedOptions, name: string): boolean {
|
|
137
|
+
export function hasFlag(options: ParsedOptions, name: string): boolean {
|
|
135
138
|
return options.flags.has(name);
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
function csvOption(options: ParsedOptions, name: string): readonly string[] | undefined {
|
|
141
|
+
export function csvOption(options: ParsedOptions, name: string): readonly string[] | undefined {
|
|
139
142
|
const value = optionValue(options, name);
|
|
140
143
|
if (value === undefined) return undefined;
|
|
141
144
|
return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
@@ -148,7 +151,7 @@ function parseLimit(value: string | undefined, fallback: number): number {
|
|
|
148
151
|
return Math.min(parsed, 200);
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
function parseConfidence(value: string | undefined): number | undefined {
|
|
154
|
+
export function parseConfidence(value: string | undefined): number | undefined {
|
|
152
155
|
if (value === undefined) return undefined;
|
|
153
156
|
const parsed = Number.parseInt(value, 10);
|
|
154
157
|
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 100) throw new Error('--confidence must be an integer from 0 to 100.');
|
|
@@ -163,7 +166,7 @@ function isMemoryScope(value: string): value is MemoryScope {
|
|
|
163
166
|
return VALID_SCOPES.includes(value as MemoryScope);
|
|
164
167
|
}
|
|
165
168
|
|
|
166
|
-
function isReviewState(value: string): value is MemoryReviewState {
|
|
169
|
+
export function isReviewState(value: string): value is MemoryReviewState {
|
|
167
170
|
return VALID_REVIEW_STATES.includes(value as MemoryReviewState);
|
|
168
171
|
}
|
|
169
172
|
|
|
@@ -171,17 +174,17 @@ function isProvenanceKind(value: string): value is ProvenanceLinkKind {
|
|
|
171
174
|
return VALID_PROVENANCE_KINDS.includes(value as ProvenanceLinkKind);
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
function requireClass(value: string | undefined): MemoryClass {
|
|
177
|
+
export function requireClass(value: string | undefined): MemoryClass {
|
|
175
178
|
if (!value || !isMemoryClass(value)) throw new Error(`Invalid memory class "${value ?? ''}". Valid values ${VALID_CLASSES.join(', ')}`);
|
|
176
179
|
return value;
|
|
177
180
|
}
|
|
178
181
|
|
|
179
|
-
function requireScope(value: string | undefined): MemoryScope {
|
|
182
|
+
export function requireScope(value: string | undefined): MemoryScope {
|
|
180
183
|
if (!value || !isMemoryScope(value)) throw new Error(`Invalid memory scope "${value ?? ''}". Valid values ${VALID_SCOPES.join(', ')}`);
|
|
181
184
|
return value;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
function optionalScope(value: string | undefined): MemoryScope | undefined {
|
|
187
|
+
export function optionalScope(value: string | undefined): MemoryScope | undefined {
|
|
185
188
|
if (value === undefined) return undefined;
|
|
186
189
|
return requireScope(value);
|
|
187
190
|
}
|
|
@@ -226,7 +229,7 @@ function renderRecordLine(record: MemoryRecord, semanticEntry?: MemorySemanticSe
|
|
|
226
229
|
return ` ${record.id} ${record.scope}/${record.cls} ${formatAgentRecordReviewState(record.reviewState)} ${record.confidence}%${score}${tags} ${record.summary}`;
|
|
227
230
|
}
|
|
228
231
|
|
|
229
|
-
function renderRecordList(title: string, path: string, records: readonly MemoryRecord[], semanticResults: readonly MemorySemanticSearchResult[] = []): string {
|
|
232
|
+
export function renderRecordList(title: string, path: string, records: readonly MemoryRecord[], semanticResults: readonly MemorySemanticSearchResult[] = []): string {
|
|
230
233
|
if (records.length === 0) {
|
|
231
234
|
return [
|
|
232
235
|
title,
|
|
@@ -242,7 +245,7 @@ function renderRecordList(title: string, path: string, records: readonly MemoryR
|
|
|
242
245
|
].join('\n');
|
|
243
246
|
}
|
|
244
247
|
|
|
245
|
-
function renderRecord(record: MemoryRecord, links: readonly MemoryLink[]): string {
|
|
248
|
+
export function renderRecord(record: MemoryRecord, links: readonly MemoryLink[]): string {
|
|
246
249
|
return [
|
|
247
250
|
`Memory ${record.id}`,
|
|
248
251
|
` scope: ${record.scope}`,
|
|
@@ -264,7 +267,7 @@ function renderRecord(record: MemoryRecord, links: readonly MemoryLink[]): strin
|
|
|
264
267
|
].filter((line): line is string => Boolean(line)).join('\n');
|
|
265
268
|
}
|
|
266
269
|
|
|
267
|
-
function filterFromOptions(options: ParsedOptions, defaultLimit: number): MemorySearchFilter {
|
|
270
|
+
export function filterFromOptions(options: ParsedOptions, defaultLimit: number): MemorySearchFilter {
|
|
268
271
|
const filter: MemorySearchFilter = {
|
|
269
272
|
limit: parseLimit(optionValue(options, 'limit'), defaultLimit),
|
|
270
273
|
};
|
|
@@ -281,7 +284,7 @@ function addProvenance(provenance: ProvenanceLink[], kind: ProvenanceLinkKind, r
|
|
|
281
284
|
if (ref !== undefined && ref.trim().length > 0) provenance.push({ kind, ref: ref.trim() });
|
|
282
285
|
}
|
|
283
286
|
|
|
284
|
-
function provenanceFromOptions(options: ParsedOptions): readonly ProvenanceLink[] {
|
|
287
|
+
export function provenanceFromOptions(options: ParsedOptions): readonly ProvenanceLink[] {
|
|
285
288
|
const provenance: ProvenanceLink[] = [];
|
|
286
289
|
addProvenance(provenance, 'session', optionValue(options, 'session'));
|
|
287
290
|
addProvenance(provenance, 'turn', optionValue(options, 'turn'));
|
|
@@ -369,7 +372,7 @@ function isMemoryBundle(value: unknown): value is MemoryBundle {
|
|
|
369
372
|
&& value.links.every(isMemoryLink);
|
|
370
373
|
}
|
|
371
374
|
|
|
372
|
-
function readBundle(path: string): MemoryBundle {
|
|
375
|
+
export function readBundle(path: string): MemoryBundle {
|
|
373
376
|
const parsed: unknown = JSON.parse(readFileSync(path, 'utf-8'));
|
|
374
377
|
if (!isMemoryBundle(parsed)) throw new Error('Invalid Agent memory bundle.');
|
|
375
378
|
for (const record of parsed.records) {
|
|
@@ -383,11 +386,11 @@ function readBundle(path: string): MemoryBundle {
|
|
|
383
386
|
return parsed;
|
|
384
387
|
}
|
|
385
388
|
|
|
386
|
-
function resolvePath(runtime: CliCommandRuntime, path: string): string {
|
|
389
|
+
export function resolvePath(runtime: CliCommandRuntime, path: string): string {
|
|
387
390
|
return resolve(runtime.workingDirectory, path);
|
|
388
391
|
}
|
|
389
392
|
|
|
390
|
-
function writeBundle(path: string, bundle: MemoryBundle): void {
|
|
393
|
+
export function writeBundle(path: string, bundle: MemoryBundle): void {
|
|
391
394
|
mkdirSync(dirname(path), { recursive: true });
|
|
392
395
|
writeFileSync(path, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
393
396
|
}
|
|
@@ -644,6 +647,12 @@ export async function handleMemoryCommand(runtime: CliCommandRuntime): Promise<C
|
|
|
644
647
|
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
645
648
|
const normalized = sub.toLowerCase();
|
|
646
649
|
if (normalized === 'handoff-inspect' || normalized === 'inspect') return handleInspect(runtime, rest);
|
|
650
|
+
// Wire-first: try the memory spine when a daemon is reachable and the
|
|
651
|
+
// subcommand has a wire-covered equivalent — see the CLI ruling in
|
|
652
|
+
// memory-command-wire.ts. Falls through (null) to the unchanged local-direct
|
|
653
|
+
// path below for everything else, or when no daemon answers.
|
|
654
|
+
const wireResult = await tryWireMemoryCommand(runtime, normalized, rest);
|
|
655
|
+
if (wireResult) return wireResult;
|
|
647
656
|
return await withMemory(runtime, async (context) => {
|
|
648
657
|
if (normalized === 'list' || normalized === 'ls') return handleList(runtime, context, rest);
|
|
649
658
|
if (normalized === 'search' || normalized === 'find') return handleSearch(runtime, context, rest);
|
|
@@ -275,7 +275,7 @@ export async function initializeBootstrapCore(
|
|
|
275
275
|
registerAgentChannelSendTool(toolRegistry, services.channelDeliveryRouter, { shellPaths: services.shellPaths });
|
|
276
276
|
registerAgentKnowledgeTool(toolRegistry, services.shellPaths, configManager);
|
|
277
277
|
registerAgentLearningConsolidationTool(toolRegistry, services.shellPaths, services.memoryRegistry);
|
|
278
|
-
registerAgentLocalRegistryTool(toolRegistry, services.shellPaths, services.memoryRegistry);
|
|
278
|
+
registerAgentLocalRegistryTool(toolRegistry, services.shellPaths, services.memoryRegistry, services.memorySpineClient);
|
|
279
279
|
registerAgentMediaGenerateTool(toolRegistry, services.mediaProviders, services.artifactStore);
|
|
280
280
|
registerAgentResearchRunsTool(toolRegistry, services.shellPaths);
|
|
281
281
|
registerAgentResearchSourcesTool(toolRegistry, services.shellPaths);
|
|
@@ -368,7 +368,7 @@ export async function initializeBootstrapCore(
|
|
|
368
368
|
// file (which would create near-duplicate persona records). After this, the VIBE
|
|
369
369
|
// prompt is a PROJECTION of those records (buildVibeProjectionPrompt).
|
|
370
370
|
try {
|
|
371
|
-
const importedPersona = await importVibeFilesIntoMemoryOnce(services.
|
|
371
|
+
const importedPersona = await importVibeFilesIntoMemoryOnce(services.memorySpineClient, services.shellPaths);
|
|
372
372
|
if (importedPersona > 0) {
|
|
373
373
|
logger.info('agent VIBE.md persona migration', { records: importedPersona });
|
|
374
374
|
}
|