@fingerskier/augment 0.4.0 → 0.5.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/LICENSE +201 -201
- package/README.md +494 -450
- package/dist/cli.js +5 -4
- package/dist/cli.js.map +1 -1
- package/dist/daemon/client.d.ts +6 -0
- package/dist/daemon/client.js +19 -0
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/http.js +79 -9
- package/dist/daemon/http.js.map +1 -1
- package/dist/daemon/lifetime-lock.js +12 -12
- package/dist/db.js +107 -107
- package/dist/decoction.d.ts +51 -0
- package/dist/decoction.js +184 -0
- package/dist/decoction.js.map +1 -0
- package/dist/hooks.d.ts +8 -5
- package/dist/hooks.js +49 -20
- package/dist/hooks.js.map +1 -1
- package/dist/install.d.ts +14 -2
- package/dist/install.js +189 -1
- package/dist/install.js.map +1 -1
- package/dist/mcp/apps.js +480 -480
- package/dist/mcp/server.js +83 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory/files.js +21 -2
- package/dist/memory/files.js.map +1 -1
- package/dist/memory/project.d.ts +3 -0
- package/dist/memory/project.js +20 -0
- package/dist/memory/project.js.map +1 -1
- package/dist/pi/extension.d.ts +6 -0
- package/dist/pi/extension.js +74 -20
- package/dist/pi/extension.js.map +1 -1
- package/dist/proposals.js +16 -15
- package/dist/proposals.js.map +1 -1
- package/dist/service.d.ts +134 -2
- package/dist/service.js +578 -51
- package/dist/service.js.map +1 -1
- package/docs/FEATURES.md +771 -615
- package/docs/verify-memory-flow.md +4 -3
- package/integrations/claude/skills/augment-context/SKILL.md +74 -74
- package/integrations/claude/skills/augment-decoction/SKILL.md +36 -0
- package/integrations/claude/skills/augment-dream/SKILL.md +34 -0
- package/integrations/claude/skills/augment-dream/agents/openai.yaml +4 -0
- package/integrations/claude/skills/augment-sleep/SKILL.md +26 -26
- package/integrations/codex/SKILL.md +107 -74
- package/integrations/examples/claude-mcp-config.json +17 -17
- package/integrations/examples/codex-mcp-config.json +17 -17
- package/integrations/examples/grok-mcp-config.toml +18 -0
- package/integrations/grok/AGENTS.md +61 -0
- package/integrations/pi/skills/augment-decoction/SKILL.md +37 -0
- package/integrations/pi/skills/augment-dream/SKILL.md +36 -0
- package/package.json +85 -84
- package/dist/tally.d.ts +0 -46
- package/dist/tally.js +0 -94
- package/dist/tally.js.map +0 -1
package/dist/service.js
CHANGED
|
@@ -3,9 +3,10 @@ import path from "node:path";
|
|
|
3
3
|
import { ARCHIVED_SEARCH_WEIGHT, DEFAULT_MEMORY_HARD_LIMIT_CHARS, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_MAX_CHARS, DEFAULT_SEARCH_MIN_SCORE, SEMANTIC_COSINE_BASELINE, SEMANTIC_COSINE_MATCH, SLEEP_MAX_CLUSTERS, SLEEP_MIN_AGE_DAYS, SLEEP_MIN_COSINE, } from "./constants.js";
|
|
4
4
|
import { ensureConfigDirs } from "./config.js";
|
|
5
5
|
import { AugmentDatabase } from "./db.js";
|
|
6
|
+
import { discoverDecoctionCandidates, } from "./decoction.js";
|
|
6
7
|
import { TransformersEmbeddingProvider } from "./embeddings.js";
|
|
7
8
|
import { deleteMemoryFile, normalizeKind, normalizeLinkType, normalizeRelativePath, memoryRelativePath, readMemory, writeMemory } from "./memory/files.js";
|
|
8
|
-
import { inferProjectName } from "./memory/project.js";
|
|
9
|
+
import { GENERIC_PROJECT_NAME, inferProjectName, isGenericProjectName } from "./memory/project.js";
|
|
9
10
|
import { findMemoryFiles } from "./memory/scan.js";
|
|
10
11
|
import { listProposals, readProposal, renderProposalsText, setProposalStatus, writeProposal, } from "./proposals.js";
|
|
11
12
|
import { clusterSleepCandidates, renderSleepCandidatesText } from "./sleep.js";
|
|
@@ -33,6 +34,10 @@ export class AugmentService {
|
|
|
33
34
|
* searches. */
|
|
34
35
|
corpusPromise;
|
|
35
36
|
queryEmbeddingCache = new Map();
|
|
37
|
+
/** FIFO critical section for every corpus mutation. Compound operations use
|
|
38
|
+
* their private `*Core` helpers while holding this lock so their validation,
|
|
39
|
+
* frontmatter rewrites, and deletes cannot interleave with another request. */
|
|
40
|
+
mutationTail = Promise.resolve();
|
|
36
41
|
constructor(config, embeddingProvider = new TransformersEmbeddingProvider()) {
|
|
37
42
|
this.config = config;
|
|
38
43
|
this.embeddingProvider = embeddingProvider;
|
|
@@ -71,12 +76,14 @@ export class AugmentService {
|
|
|
71
76
|
return this.db.listProjects(name);
|
|
72
77
|
}
|
|
73
78
|
async rebuildIndex() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
return this.serializeMutation(async () => {
|
|
80
|
+
try {
|
|
81
|
+
return await this.rebuildIndexCore();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.invalidateCorpusSnapshot();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
80
87
|
}
|
|
81
88
|
async rebuildIndexCore() {
|
|
82
89
|
await ensureConfigDirs(this.config);
|
|
@@ -114,12 +121,15 @@ export class AugmentService {
|
|
|
114
121
|
* deletion is not authorization to rewrite other files (unlike {@link deleteMemory}).
|
|
115
122
|
*/
|
|
116
123
|
async indexPath(relativePath) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
await this.init();
|
|
125
|
+
return this.serializeMutation(async () => {
|
|
126
|
+
try {
|
|
127
|
+
await this.indexPathCore(relativePath);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
this.invalidateCorpusSnapshot();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
123
133
|
}
|
|
124
134
|
async indexPathCore(relativePath) {
|
|
125
135
|
await this.init();
|
|
@@ -154,14 +164,32 @@ export class AugmentService {
|
|
|
154
164
|
recordSelfWrite(relativePath) {
|
|
155
165
|
this.selfWrites.set(normalizeRelativePath(relativePath), Date.now());
|
|
156
166
|
}
|
|
157
|
-
async
|
|
167
|
+
async memoryFileExists(relativePath) {
|
|
158
168
|
try {
|
|
159
|
-
|
|
169
|
+
await stat(path.join(this.config.memoryRoot, normalizeRelativePath(relativePath)));
|
|
170
|
+
return true;
|
|
160
171
|
}
|
|
161
|
-
|
|
162
|
-
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (typeof error === "object" &&
|
|
174
|
+
error !== null &&
|
|
175
|
+
"code" in error &&
|
|
176
|
+
error.code === "ENOENT") {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
163
180
|
}
|
|
164
181
|
}
|
|
182
|
+
async upsert(input) {
|
|
183
|
+
await this.init();
|
|
184
|
+
return this.serializeMutation(async () => {
|
|
185
|
+
try {
|
|
186
|
+
return await this.upsertCore(input);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
this.invalidateCorpusSnapshot();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
165
193
|
async upsertCore(input) {
|
|
166
194
|
await this.init();
|
|
167
195
|
if (input.content.length > DEFAULT_MEMORY_HARD_LIMIT_CHARS) {
|
|
@@ -276,12 +304,15 @@ export class AugmentService {
|
|
|
276
304
|
return { project, memories, links };
|
|
277
305
|
}
|
|
278
306
|
async deleteMemory(id) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
307
|
+
await this.init();
|
|
308
|
+
return this.serializeMutation(async () => {
|
|
309
|
+
try {
|
|
310
|
+
return await this.deleteMemoryCore(id);
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
this.invalidateCorpusSnapshot();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
285
316
|
}
|
|
286
317
|
async deleteMemoryCore(id) {
|
|
287
318
|
await this.init();
|
|
@@ -343,12 +374,15 @@ export class AugmentService {
|
|
|
343
374
|
return memory;
|
|
344
375
|
}
|
|
345
376
|
async link(fromRef, toRef, typeInput) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
377
|
+
await this.init();
|
|
378
|
+
return this.serializeMutation(async () => {
|
|
379
|
+
try {
|
|
380
|
+
return await this.linkCore(fromRef, toRef, typeInput);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
this.invalidateCorpusSnapshot();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
352
386
|
}
|
|
353
387
|
async linkCore(fromRef, toRef, typeInput) {
|
|
354
388
|
await this.init();
|
|
@@ -387,12 +421,15 @@ export class AugmentService {
|
|
|
387
421
|
return this.db.listLinks(memoryId);
|
|
388
422
|
}
|
|
389
423
|
async unlink(linkId) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
424
|
+
await this.init();
|
|
425
|
+
return this.serializeMutation(async () => {
|
|
426
|
+
try {
|
|
427
|
+
return await this.unlinkCore(linkId);
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
this.invalidateCorpusSnapshot();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
396
433
|
}
|
|
397
434
|
async unlinkCore(linkId) {
|
|
398
435
|
await this.init();
|
|
@@ -423,6 +460,14 @@ export class AugmentService {
|
|
|
423
460
|
invalidateCorpusSnapshot() {
|
|
424
461
|
this.corpusPromise = undefined;
|
|
425
462
|
}
|
|
463
|
+
/** Queue one corpus mutation after the previous request, keeping the queue
|
|
464
|
+
* usable after failures. Assignment happens synchronously before any caller
|
|
465
|
+
* can enqueue the next request. */
|
|
466
|
+
serializeMutation(mutation) {
|
|
467
|
+
const result = this.mutationTail.then(mutation);
|
|
468
|
+
this.mutationTail = result.then(() => undefined, () => undefined);
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
426
471
|
corpus() {
|
|
427
472
|
if (!this.corpusPromise) {
|
|
428
473
|
const build = (async () => ({
|
|
@@ -463,6 +508,10 @@ export class AugmentService {
|
|
|
463
508
|
}
|
|
464
509
|
async search(input) {
|
|
465
510
|
await this.init();
|
|
511
|
+
// `.generic` is a storage pseudo-project, never an active/current project.
|
|
512
|
+
// Explicit generic CRUD remains valid, but search must not grant generics
|
|
513
|
+
// exact-project priority when a caller passes the reserved name.
|
|
514
|
+
const activeProjectName = input.project_name && isGenericProjectName(input.project_name) ? undefined : input.project_name;
|
|
466
515
|
const limit = input.limit ?? DEFAULT_SEARCH_LIMIT;
|
|
467
516
|
const maxChars = input.max_chars ?? DEFAULT_SEARCH_MAX_CHARS;
|
|
468
517
|
const minScore = clamp01(input.min_score ?? DEFAULT_SEARCH_MIN_SCORE);
|
|
@@ -477,7 +526,7 @@ export class AugmentService {
|
|
|
477
526
|
for (const memory of memories) {
|
|
478
527
|
const semanticScore = queryEmbedding ? normalizedCosine(queryEmbedding, embeddings.get(memory.id)) : 0;
|
|
479
528
|
const lexScore = lexicalScoreWith(queryTokens, searchDocument(memory));
|
|
480
|
-
const metadataScore = metadataScoreFor(
|
|
529
|
+
const metadataScore = metadataScoreFor(activeProjectName, memory);
|
|
481
530
|
let score = clamp01(semanticScore * 0.55 + lexScore * 0.25 + metadataScore * 0.1);
|
|
482
531
|
// Down-weight (never exclude) an archived memory's own relevance, after
|
|
483
532
|
// the blend and before the relevance floor below -- so a down-weighted
|
|
@@ -521,13 +570,243 @@ export class AugmentService {
|
|
|
521
570
|
.slice(0, limit);
|
|
522
571
|
return {
|
|
523
572
|
results,
|
|
524
|
-
text: renderSearchText(input.query,
|
|
573
|
+
text: renderSearchText(input.query, activeProjectName, results, maxChars),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
async decoctionGlean(input) {
|
|
577
|
+
await this.init();
|
|
578
|
+
if (input.action === "discover") {
|
|
579
|
+
const limit = input.limit ?? 20;
|
|
580
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
581
|
+
throw new RangeError("Decoction discovery limit must be an integer between 1 and 50");
|
|
582
|
+
}
|
|
583
|
+
const snapshot = await this.corpus();
|
|
584
|
+
const discovered = discoverDecoctionCandidates({
|
|
585
|
+
memories: snapshot.memories,
|
|
586
|
+
embeddings: snapshot.embeddings,
|
|
587
|
+
...(input.min_cosine !== undefined ? { minCosine: input.min_cosine } : {}),
|
|
588
|
+
});
|
|
589
|
+
const clusters = discovered.clusters.slice(0, limit);
|
|
590
|
+
return {
|
|
591
|
+
action: "discover",
|
|
592
|
+
...discovered,
|
|
593
|
+
clusters,
|
|
594
|
+
text: renderDecoctionDiscoveryText(clusters, discovered.eligible, discovered.min_cosine),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (input.action !== "write") {
|
|
598
|
+
throw new Error(`Unsupported decoction glean action: ${String(input.action)}`);
|
|
599
|
+
}
|
|
600
|
+
return this.serializeMutation(async () => {
|
|
601
|
+
try {
|
|
602
|
+
return await this.decoctionGleanWrite(input);
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
this.invalidateCorpusSnapshot();
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
async decoctionGleanWrite(input) {
|
|
610
|
+
const kind = normalizeKind(input.kind);
|
|
611
|
+
if (kind !== "SPEC" && kind !== "ARCH") {
|
|
612
|
+
throw new Error(`A generic memory created by glean must be SPEC or ARCH, not ${kind}`);
|
|
613
|
+
}
|
|
614
|
+
if (!input.name.trim()) {
|
|
615
|
+
throw new Error("A generic memory requires a non-empty name");
|
|
616
|
+
}
|
|
617
|
+
if (!Array.isArray(input.derived_from)) {
|
|
618
|
+
throw new Error("A generic memory requires derived_from evidence paths");
|
|
619
|
+
}
|
|
620
|
+
const sourcePaths = input.derived_from.map((sourcePath) => normalizeRelativePath(sourcePath));
|
|
621
|
+
if (new Set(sourcePaths).size !== sourcePaths.length) {
|
|
622
|
+
throw new Error("Generic memory evidence paths must be unique");
|
|
623
|
+
}
|
|
624
|
+
const sources = [];
|
|
625
|
+
for (const sourcePath of sourcePaths) {
|
|
626
|
+
const source = await this.db.getMemoryByPath(sourcePath);
|
|
627
|
+
if (!source) {
|
|
628
|
+
throw new Error(`Decoction evidence source not found in corpus: ${sourcePath}`);
|
|
629
|
+
}
|
|
630
|
+
if (isGenericProjectName(source.project_name) || source.relative_path.startsWith(`${GENERIC_PROJECT_NAME}/`)) {
|
|
631
|
+
throw new Error(`A generic memory cannot be used as original decoction evidence: ${sourcePath}`);
|
|
632
|
+
}
|
|
633
|
+
if (source.archived) {
|
|
634
|
+
throw new Error(`Archived memory cannot be used as decoction evidence: ${sourcePath}`);
|
|
635
|
+
}
|
|
636
|
+
if (!["WORK", "ISSUE", "SPEC", "ARCH"].includes(source.kind)) {
|
|
637
|
+
throw new Error(`Unsupported decoction evidence kind ${source.kind}: ${sourcePath}`);
|
|
638
|
+
}
|
|
639
|
+
const excludedTag = source.tags.find((tag) => ["private", "no-generalize"].includes(tag.trim().toLowerCase()));
|
|
640
|
+
if (excludedTag) {
|
|
641
|
+
throw new Error(`Memory tagged ${excludedTag} cannot be used as decoction evidence: ${sourcePath}`);
|
|
642
|
+
}
|
|
643
|
+
sources.push(source);
|
|
644
|
+
}
|
|
645
|
+
const distinctProjects = new Set(sources.map((source) => source.project_name));
|
|
646
|
+
if (distinctProjects.size < 4) {
|
|
647
|
+
throw new Error(`Generic memory evidence must span at least 4 distinct projects; received ${distinctProjects.size}`);
|
|
648
|
+
}
|
|
649
|
+
const sourcePathSet = new Set(sourcePaths);
|
|
650
|
+
const suggestedRemovals = (input.suggested_removals ?? []).map((suggestion) => {
|
|
651
|
+
const normalizedPath = normalizeRelativePath(suggestion.path);
|
|
652
|
+
if (!sourcePathSet.has(normalizedPath)) {
|
|
653
|
+
throw new Error(`Suggested removal must also be a derived_from evidence source: ${suggestion.path}`);
|
|
654
|
+
}
|
|
655
|
+
const reason = suggestion.reason.trim();
|
|
656
|
+
if (!reason) {
|
|
657
|
+
throw new Error(`Suggested removal requires a reason: ${suggestion.path}`);
|
|
658
|
+
}
|
|
659
|
+
return { path: normalizedPath, reason };
|
|
660
|
+
});
|
|
661
|
+
if (new Set(suggestedRemovals.map((suggestion) => suggestion.path)).size !== suggestedRemovals.length) {
|
|
662
|
+
throw new Error("Suggested removal paths must be unique");
|
|
663
|
+
}
|
|
664
|
+
const genericProject = await this.db.upsertProject(GENERIC_PROJECT_NAME);
|
|
665
|
+
const targetPath = memoryRelativePath(GENERIC_PROJECT_NAME, kind, input.name);
|
|
666
|
+
let existing = await this.db.getMemoryByPath(targetPath);
|
|
667
|
+
let liveTarget;
|
|
668
|
+
try {
|
|
669
|
+
liveTarget = await readMemory(this.config.memoryRoot, targetPath);
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
if (typeof error !== "object" ||
|
|
673
|
+
error === null ||
|
|
674
|
+
!("code" in error) ||
|
|
675
|
+
error.code !== "ENOENT") {
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const hasExpectedContent = input.expected_content_hash !== undefined;
|
|
680
|
+
const hasExpectedFrontmatter = input.expected_frontmatter_hash !== undefined;
|
|
681
|
+
if (liveTarget) {
|
|
682
|
+
if (!hasExpectedContent || !hasExpectedFrontmatter) {
|
|
683
|
+
throw new Error(`Generic memory ${targetPath} already exists; expected_content_hash and expected_frontmatter_hash are required for compare-and-swap update`);
|
|
684
|
+
}
|
|
685
|
+
if (liveTarget.contentHash !== input.expected_content_hash ||
|
|
686
|
+
liveTarget.frontmatterHash !== input.expected_frontmatter_hash) {
|
|
687
|
+
throw new Error(`Generic memory ${targetPath} changed since it was read; expected hashes are stale`);
|
|
688
|
+
}
|
|
689
|
+
// The filesystem is canonical. If a matching live file has not reached
|
|
690
|
+
// the watcher/index yet, bring its metadata into the DB before upsertCore
|
|
691
|
+
// preserves tags, links, timestamps, and identity from the existing row.
|
|
692
|
+
if (!existing ||
|
|
693
|
+
existing.content_hash !== liveTarget.contentHash ||
|
|
694
|
+
existing.frontmatter_hash !== liveTarget.frontmatterHash) {
|
|
695
|
+
existing = await this.db.upsertMemory(liveTarget);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
else if (existing) {
|
|
699
|
+
throw new Error(`Generic memory ${targetPath} changed since it was read; the live target file is missing`);
|
|
700
|
+
}
|
|
701
|
+
else if (hasExpectedContent || hasExpectedFrontmatter) {
|
|
702
|
+
throw new Error(`Generic memory ${targetPath} does not exist; expected hashes are only valid for updates`);
|
|
703
|
+
}
|
|
704
|
+
const upserted = await this.upsertCore({
|
|
705
|
+
project_id: genericProject.id,
|
|
706
|
+
...(existing ? { memory_id: existing.id } : {}),
|
|
707
|
+
kind,
|
|
708
|
+
name: input.name,
|
|
709
|
+
content: input.content,
|
|
710
|
+
tags: input.tags,
|
|
711
|
+
derived_from: sourcePaths,
|
|
712
|
+
skip_similarity_fold: true,
|
|
713
|
+
});
|
|
714
|
+
if (upserted.memory.relative_path !== targetPath) {
|
|
715
|
+
throw new Error(`Decoction write diverted from ${targetPath} to ${upserted.memory.relative_path}`);
|
|
716
|
+
}
|
|
717
|
+
const warnings = [
|
|
718
|
+
...credentialWarnings(input.content, "generic draft"),
|
|
719
|
+
...sources.flatMap((source) => credentialWarnings(source.body, `evidence ${source.relative_path}`)),
|
|
720
|
+
];
|
|
721
|
+
return {
|
|
722
|
+
action: "write",
|
|
723
|
+
memory: upserted.memory,
|
|
724
|
+
upsert_action: upserted.action,
|
|
725
|
+
suggested_removals: suggestedRemovals,
|
|
726
|
+
warnings,
|
|
727
|
+
text: renderDecoctionWriteText(upserted.memory, suggestedRemovals, warnings),
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
async decoctionCleanup(input) {
|
|
731
|
+
await this.init();
|
|
732
|
+
return this.serializeMutation(async () => {
|
|
733
|
+
try {
|
|
734
|
+
return await this.decoctionCleanupCore(input);
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
this.invalidateCorpusSnapshot();
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
async decoctionCleanupCore(input) {
|
|
742
|
+
await this.init();
|
|
743
|
+
const genericPath = normalizeRelativePath(input.generic_path);
|
|
744
|
+
const generic = await this.db.getMemoryByPath(genericPath);
|
|
745
|
+
if (!generic) {
|
|
746
|
+
throw new Error(`Generic memory not found in corpus: ${input.generic_path}`);
|
|
747
|
+
}
|
|
748
|
+
if (!isGenericProjectName(generic.project_name) || !generic.relative_path.startsWith(`${GENERIC_PROJECT_NAME}/`)) {
|
|
749
|
+
throw new Error(`Decoction cleanup target must be a generic memory: ${input.generic_path}`);
|
|
750
|
+
}
|
|
751
|
+
if (!Array.isArray(input.source_paths) || input.source_paths.length === 0) {
|
|
752
|
+
throw new Error("Decoction cleanup requires at least one source path");
|
|
753
|
+
}
|
|
754
|
+
const sourcePaths = input.source_paths.map((sourcePath) => normalizeRelativePath(sourcePath));
|
|
755
|
+
if (new Set(sourcePaths).size !== sourcePaths.length) {
|
|
756
|
+
throw new Error("Decoction cleanup source paths must be unique");
|
|
757
|
+
}
|
|
758
|
+
const sources = [];
|
|
759
|
+
for (const sourcePath of sourcePaths) {
|
|
760
|
+
const source = await this.db.getMemoryByPath(sourcePath);
|
|
761
|
+
if (!source) {
|
|
762
|
+
throw new Error(`Unknown memory path: ${sourcePath}`);
|
|
763
|
+
}
|
|
764
|
+
if (isGenericProjectName(source.project_name) || source.relative_path.startsWith(`${GENERIC_PROJECT_NAME}/`)) {
|
|
765
|
+
throw new Error(`A generic memory cannot be a decoction cleanup source: ${sourcePath}`);
|
|
766
|
+
}
|
|
767
|
+
sources.push(source);
|
|
768
|
+
}
|
|
769
|
+
const results = [];
|
|
770
|
+
for (const source of sources) {
|
|
771
|
+
let retargetedLinks = 0;
|
|
772
|
+
try {
|
|
773
|
+
const inboundLinks = (await this.db.listLinks(source.id)).filter((link) => link.to_memory_id === source.id && link.from_memory_id !== generic.id);
|
|
774
|
+
for (const inbound of inboundLinks) {
|
|
775
|
+
await this.linkCore(inbound.from_memory_id, generic.id, inbound.type);
|
|
776
|
+
retargetedLinks += 1;
|
|
777
|
+
}
|
|
778
|
+
await this.deleteMemoryCore(source.id);
|
|
779
|
+
results.push({
|
|
780
|
+
source_path: source.relative_path,
|
|
781
|
+
status: "deleted",
|
|
782
|
+
retargeted_links: retargetedLinks,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
results.push({
|
|
787
|
+
source_path: source.relative_path,
|
|
788
|
+
status: "failed",
|
|
789
|
+
retargeted_links: retargetedLinks,
|
|
790
|
+
error: error instanceof Error ? error.message : String(error),
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const deleted = results.filter((result) => result.status === "deleted").length;
|
|
795
|
+
const failed = results.length - deleted;
|
|
796
|
+
return {
|
|
797
|
+
generic_path: generic.relative_path,
|
|
798
|
+
results,
|
|
799
|
+
text: [
|
|
800
|
+
`decoction cleanup for ${generic.relative_path}: ${deleted} deleted, ${failed} failed`,
|
|
801
|
+
...results.map((result) => `- ${result.source_path}: status=${result.status}; retargeted_links=${result.retargeted_links}` +
|
|
802
|
+
(result.error ? `; error=${result.error}` : "")),
|
|
803
|
+
].join("\n"),
|
|
525
804
|
};
|
|
526
805
|
}
|
|
527
806
|
/**
|
|
528
807
|
* READ-ONLY sleep preview: which settled WORK memories WOULD consolidate.
|
|
529
|
-
* Performs zero writes
|
|
530
|
-
*
|
|
808
|
+
* Performs zero writes; an agent must draft a proposal and receive explicit
|
|
809
|
+
* user approval before `sleep_apply` changes the corpus.
|
|
531
810
|
*/
|
|
532
811
|
async sleepCandidates(input = {}, now = Date.now()) {
|
|
533
812
|
await this.init();
|
|
@@ -588,12 +867,15 @@ export class AugmentService {
|
|
|
588
867
|
* git: the route commits the whole pass as one revertible commit.
|
|
589
868
|
*/
|
|
590
869
|
async sleepApply(input) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
870
|
+
await this.init();
|
|
871
|
+
return this.serializeMutation(async () => {
|
|
872
|
+
try {
|
|
873
|
+
return await this.sleepApplyCore(input);
|
|
874
|
+
}
|
|
875
|
+
finally {
|
|
876
|
+
this.invalidateCorpusSnapshot();
|
|
877
|
+
}
|
|
878
|
+
});
|
|
597
879
|
}
|
|
598
880
|
async sleepApplyCore(input) {
|
|
599
881
|
await this.init();
|
|
@@ -620,13 +902,13 @@ export class AugmentService {
|
|
|
620
902
|
if (!project) {
|
|
621
903
|
throw new Error(`Sleep proposal project not found in corpus: ${projectName}`);
|
|
622
904
|
}
|
|
623
|
-
// 4. Write the consolidated record
|
|
624
|
-
//
|
|
905
|
+
// 4. Write the consolidated record inside this compound mutation's critical
|
|
906
|
+
// section. skip_similarity_fold pins the
|
|
625
907
|
// write to the approved target's deterministic path: the user approved
|
|
626
908
|
// THIS identity (validateSleepSources above already guaranteed it's
|
|
627
909
|
// free), so upsertCore must not divert it into some other lexically-
|
|
628
910
|
// similar same-kind record it happens to subsume.
|
|
629
|
-
const upserted = await this.
|
|
911
|
+
const upserted = await this.upsertCore({
|
|
630
912
|
project_id: project.id,
|
|
631
913
|
kind,
|
|
632
914
|
name,
|
|
@@ -655,8 +937,207 @@ export class AugmentService {
|
|
|
655
937
|
}
|
|
656
938
|
async sleepReject(filename) {
|
|
657
939
|
await this.init();
|
|
658
|
-
|
|
659
|
-
|
|
940
|
+
return this.serializeMutation(async () => {
|
|
941
|
+
const existing = await readProposal(this.config.stateDir, "sleep", filename);
|
|
942
|
+
if (existing.frontmatter.status !== "proposed") {
|
|
943
|
+
throw new Error(`Sleep proposal ${filename} is already ${existing.frontmatter.status}, not proposed`);
|
|
944
|
+
}
|
|
945
|
+
const proposal = await setProposalStatus(this.config.stateDir, "sleep", filename, "rejected");
|
|
946
|
+
return { proposal, text: renderProposalsText([proposal], "sleep") };
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Draft one speculative memory or link outside the corpus. The daemon never
|
|
951
|
+
* synthesizes a dream: an agent supplies the complete candidate and this
|
|
952
|
+
* method only validates its live references and writes a review artifact.
|
|
953
|
+
*/
|
|
954
|
+
async dreamPropose(input) {
|
|
955
|
+
await this.init();
|
|
956
|
+
const rawInput = input;
|
|
957
|
+
const hasLinkForm = rawInput.link !== undefined;
|
|
958
|
+
const hasMemoryForm = ["kind", "name", "content", "derived_from"].some((key) => rawInput[key] !== undefined);
|
|
959
|
+
if (hasLinkForm && hasMemoryForm) {
|
|
960
|
+
throw new Error("A dream proposal requires exactly one form: dream-memory or dream-link");
|
|
961
|
+
}
|
|
962
|
+
if (hasLinkForm) {
|
|
963
|
+
if (!rawInput.link || typeof rawInput.link !== "object") {
|
|
964
|
+
throw new Error("A dream-link proposal requires link endpoints");
|
|
965
|
+
}
|
|
966
|
+
const inputLink = rawInput.link;
|
|
967
|
+
const link = {
|
|
968
|
+
from: String(inputLink.from ?? ""),
|
|
969
|
+
to: String(inputLink.to ?? ""),
|
|
970
|
+
type: normalizeLinkType(String(inputLink.type ?? "")),
|
|
971
|
+
};
|
|
972
|
+
await this.validateDreamLink(input.project_name, link);
|
|
973
|
+
const proposal = await writeProposal(this.config.stateDir, {
|
|
974
|
+
type: "dream-link",
|
|
975
|
+
project: input.project_name,
|
|
976
|
+
derived_from: [],
|
|
977
|
+
link,
|
|
978
|
+
...(input.rationale !== undefined ? { rationale: input.rationale } : {}),
|
|
979
|
+
}, "");
|
|
980
|
+
return { proposal, text: renderDreamProposalsText([proposal]) };
|
|
981
|
+
}
|
|
982
|
+
const memoryInput = input;
|
|
983
|
+
if (typeof memoryInput.name !== "string" || !memoryInput.name.trim()) {
|
|
984
|
+
throw new Error("A dream-memory proposal requires a target name");
|
|
985
|
+
}
|
|
986
|
+
if (typeof memoryInput.content !== "string") {
|
|
987
|
+
throw new Error("A dream-memory proposal requires content");
|
|
988
|
+
}
|
|
989
|
+
if (!Array.isArray(memoryInput.derived_from)) {
|
|
990
|
+
throw new Error("A dream-memory proposal requires derived_from inspiration paths");
|
|
991
|
+
}
|
|
992
|
+
if (typeof memoryInput.kind !== "string") {
|
|
993
|
+
throw new Error("A dream-memory proposal requires a target kind");
|
|
994
|
+
}
|
|
995
|
+
const kind = normalizeKind(memoryInput.kind);
|
|
996
|
+
await this.validateDreamMemory(memoryInput.project_name, kind, memoryInput.name, memoryInput.derived_from);
|
|
997
|
+
const proposal = await writeProposal(this.config.stateDir, {
|
|
998
|
+
type: "dream-memory",
|
|
999
|
+
project: memoryInput.project_name,
|
|
1000
|
+
kind,
|
|
1001
|
+
name: memoryInput.name,
|
|
1002
|
+
derived_from: memoryInput.derived_from,
|
|
1003
|
+
...(memoryInput.rationale !== undefined ? { rationale: memoryInput.rationale } : {}),
|
|
1004
|
+
}, memoryInput.content);
|
|
1005
|
+
return { proposal, text: renderDreamProposalsText([proposal]) };
|
|
1006
|
+
}
|
|
1007
|
+
/** List the whole dreams/ proposal family, including both dream-memory and
|
|
1008
|
+
* dream-link records. `dream-memory` is intentionally only the directory
|
|
1009
|
+
* selector used by the shared proposal store. */
|
|
1010
|
+
async dreamProposals(status) {
|
|
1011
|
+
await this.init();
|
|
1012
|
+
const proposals = await listProposals(this.config.stateDir, "dream-memory", status);
|
|
1013
|
+
return { proposals, text: renderDreamProposalsText(proposals) };
|
|
1014
|
+
}
|
|
1015
|
+
/** Apply the explicitly approved proposal after re-reading and re-validating
|
|
1016
|
+
* it against the live corpus. The HTTP route owns the single git commit. */
|
|
1017
|
+
async dreamApply(input) {
|
|
1018
|
+
await this.init();
|
|
1019
|
+
return this.serializeMutation(async () => {
|
|
1020
|
+
try {
|
|
1021
|
+
return await this.dreamApplyCore(input);
|
|
1022
|
+
}
|
|
1023
|
+
finally {
|
|
1024
|
+
this.invalidateCorpusSnapshot();
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
async dreamApplyCore(input) {
|
|
1029
|
+
await this.init();
|
|
1030
|
+
const proposal = await readProposal(this.config.stateDir, "dream-memory", input.filename);
|
|
1031
|
+
const fm = proposal.frontmatter;
|
|
1032
|
+
if (fm.status !== "proposed") {
|
|
1033
|
+
throw new Error(`Dream proposal ${input.filename} is already ${fm.status}, not proposed`);
|
|
1034
|
+
}
|
|
1035
|
+
if (fm.type === "dream-memory") {
|
|
1036
|
+
if (!fm.kind) {
|
|
1037
|
+
throw new Error(`Dream proposal ${input.filename} is missing a target kind`);
|
|
1038
|
+
}
|
|
1039
|
+
if (!fm.name) {
|
|
1040
|
+
throw new Error(`Dream proposal ${input.filename} is missing a target name`);
|
|
1041
|
+
}
|
|
1042
|
+
const kind = normalizeKind(fm.kind);
|
|
1043
|
+
await this.validateDreamMemory(fm.project, kind, fm.name, fm.derived_from);
|
|
1044
|
+
const project = await this.db.getProjectByName(fm.project);
|
|
1045
|
+
if (!project) {
|
|
1046
|
+
throw new Error(`Dream proposal project not found in corpus: ${fm.project}`);
|
|
1047
|
+
}
|
|
1048
|
+
const upserted = await this.upsertCore({
|
|
1049
|
+
project_id: project.id,
|
|
1050
|
+
kind,
|
|
1051
|
+
name: fm.name,
|
|
1052
|
+
content: proposal.body,
|
|
1053
|
+
tags: ["dream"],
|
|
1054
|
+
derived_from: fm.derived_from,
|
|
1055
|
+
skip_similarity_fold: true,
|
|
1056
|
+
});
|
|
1057
|
+
const expectedTargetPath = memoryRelativePath(fm.project, kind, fm.name);
|
|
1058
|
+
if (upserted.memory.relative_path !== expectedTargetPath) {
|
|
1059
|
+
throw new Error(`Dream apply diverted from the approved target ${expectedTargetPath} to ${upserted.memory.relative_path}`);
|
|
1060
|
+
}
|
|
1061
|
+
const appliedProposal = await setProposalStatus(this.config.stateDir, "dream-memory", input.filename, "applied");
|
|
1062
|
+
return {
|
|
1063
|
+
type: "dream-memory",
|
|
1064
|
+
proposal: appliedProposal,
|
|
1065
|
+
memory: upserted.memory,
|
|
1066
|
+
text: `applied dream memory ${kind}/${fm.name} (${fm.project})`,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
if (fm.type === "dream-link") {
|
|
1070
|
+
if (!fm.link) {
|
|
1071
|
+
throw new Error(`Dream proposal ${input.filename} is missing link endpoints`);
|
|
1072
|
+
}
|
|
1073
|
+
const validated = await this.validateDreamLink(fm.project, fm.link);
|
|
1074
|
+
const linked = await this.linkCore(validated.from.relative_path, validated.to.relative_path, validated.type);
|
|
1075
|
+
const appliedProposal = await setProposalStatus(this.config.stateDir, "dream-memory", input.filename, "applied");
|
|
1076
|
+
return {
|
|
1077
|
+
type: "dream-link",
|
|
1078
|
+
proposal: appliedProposal,
|
|
1079
|
+
link: linked.link,
|
|
1080
|
+
text: `applied dream link ${validated.from.relative_path} -> ${validated.to.relative_path} (${validated.type})`,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
throw new Error(`Unsupported dream proposal type in ${input.filename}: ${String(fm.type)}`);
|
|
1084
|
+
}
|
|
1085
|
+
async dreamReject(filename) {
|
|
1086
|
+
await this.init();
|
|
1087
|
+
return this.serializeMutation(async () => {
|
|
1088
|
+
const existing = await readProposal(this.config.stateDir, "dream-memory", filename);
|
|
1089
|
+
if (existing.frontmatter.status !== "proposed") {
|
|
1090
|
+
throw new Error(`Dream proposal ${filename} is already ${existing.frontmatter.status}, not proposed`);
|
|
1091
|
+
}
|
|
1092
|
+
const proposal = await setProposalStatus(this.config.stateDir, "dream-memory", filename, "rejected");
|
|
1093
|
+
return { proposal, text: renderDreamProposalsText([proposal]) };
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
async validateDreamMemory(projectName, kind, name, derivedFrom) {
|
|
1097
|
+
if (!projectName) {
|
|
1098
|
+
throw new Error("A dream-memory proposal requires project_name");
|
|
1099
|
+
}
|
|
1100
|
+
if (isGenericProjectName(projectName)) {
|
|
1101
|
+
throw new Error("The generic pseudo-project cannot be a dream active project");
|
|
1102
|
+
}
|
|
1103
|
+
if (derivedFrom.length < 1) {
|
|
1104
|
+
throw new Error("A dream-memory proposal needs at least 1 inspiration source");
|
|
1105
|
+
}
|
|
1106
|
+
if (!(await this.db.getProjectByName(projectName))) {
|
|
1107
|
+
throw new Error(`Dream proposal project not found in corpus: ${projectName}`);
|
|
1108
|
+
}
|
|
1109
|
+
for (const sourcePath of derivedFrom) {
|
|
1110
|
+
const source = await this.db.getMemoryByPath(normalizeRelativePath(sourcePath));
|
|
1111
|
+
if (!source) {
|
|
1112
|
+
throw new Error(`Dream source not found in corpus: ${sourcePath}`);
|
|
1113
|
+
}
|
|
1114
|
+
// There is deliberately no project check here. Normal global search may
|
|
1115
|
+
// inspire dreams from the current, generic, or any foreign project.
|
|
1116
|
+
}
|
|
1117
|
+
const targetPath = memoryRelativePath(projectName, kind, name);
|
|
1118
|
+
if ((await this.db.getMemoryByPath(targetPath)) || (await this.memoryFileExists(targetPath))) {
|
|
1119
|
+
throw new Error(`Dream target already exists in corpus: ${targetPath}`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
async validateDreamLink(projectName, link) {
|
|
1123
|
+
if (!projectName) {
|
|
1124
|
+
throw new Error("A dream-link proposal requires project_name");
|
|
1125
|
+
}
|
|
1126
|
+
if (isGenericProjectName(projectName)) {
|
|
1127
|
+
throw new Error("The generic pseudo-project cannot be a dream active project");
|
|
1128
|
+
}
|
|
1129
|
+
const from = await this.db.getMemoryByPath(normalizeRelativePath(link.from));
|
|
1130
|
+
if (!from) {
|
|
1131
|
+
throw new Error(`Dream link source not found in corpus: ${link.from}`);
|
|
1132
|
+
}
|
|
1133
|
+
if (from.project_name !== projectName) {
|
|
1134
|
+
throw new Error(`Dream link source ${link.from} belongs to ${from.project_name}, not ${projectName}`);
|
|
1135
|
+
}
|
|
1136
|
+
const to = await this.db.getMemoryByPath(normalizeRelativePath(link.to));
|
|
1137
|
+
if (!to) {
|
|
1138
|
+
throw new Error(`Dream link target not found in corpus: ${link.to}`);
|
|
1139
|
+
}
|
|
1140
|
+
return { from, to, type: normalizeLinkType(link.type) };
|
|
660
1141
|
}
|
|
661
1142
|
/**
|
|
662
1143
|
* Shared sleep validation, run at propose time and re-run against the live
|
|
@@ -685,7 +1166,7 @@ export class AugmentService {
|
|
|
685
1166
|
}
|
|
686
1167
|
}
|
|
687
1168
|
const targetPath = memoryRelativePath(projectName, kind, name);
|
|
688
|
-
if (await this.db.getMemoryByPath(targetPath)) {
|
|
1169
|
+
if ((await this.db.getMemoryByPath(targetPath)) || (await this.memoryFileExists(targetPath))) {
|
|
689
1170
|
throw new Error(`Sleep target already exists in corpus: ${targetPath}`);
|
|
690
1171
|
}
|
|
691
1172
|
}
|
|
@@ -766,6 +1247,52 @@ function mergeFrontmatterLink(links, link) {
|
|
|
766
1247
|
}
|
|
767
1248
|
return [...links, link];
|
|
768
1249
|
}
|
|
1250
|
+
function renderDecoctionDiscoveryText(clusters, eligible, minCosine) {
|
|
1251
|
+
if (clusters.length === 0) {
|
|
1252
|
+
return `No decoction candidates found among ${eligible} eligible memories at complete-link cosine ${minCosine.toFixed(2)}. Discovery is READ-ONLY.`;
|
|
1253
|
+
}
|
|
1254
|
+
const lines = [
|
|
1255
|
+
`Decoction discovery is READ-ONLY: ${clusters.length} candidate cluster(s) among ${eligible} eligible memories.`,
|
|
1256
|
+
];
|
|
1257
|
+
clusters.forEach((cluster, index) => {
|
|
1258
|
+
lines.push(`\n${index + 1}. ${cluster.distinct_projects} projects, ${cluster.sources.length} sources, minimum pairwise cosine ${cluster.min_pairwise_cosine.toFixed(3)}`);
|
|
1259
|
+
for (const source of cluster.sources) {
|
|
1260
|
+
lines.push(` - ${source.relative_path} [${source.kind}; project ${source.project_name}; id ${source.id}]`);
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
return lines.join("\n");
|
|
1264
|
+
}
|
|
1265
|
+
function renderDecoctionWriteText(memory, suggestions, warnings) {
|
|
1266
|
+
const lines = [`wrote generic memory ${memory.id} at ${memory.relative_path}`];
|
|
1267
|
+
if (suggestions.length > 0) {
|
|
1268
|
+
lines.push("Suggested removals (not yet deleted):");
|
|
1269
|
+
for (const suggestion of suggestions) {
|
|
1270
|
+
lines.push(`- ${suggestion.path}: ${suggestion.reason}`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
else {
|
|
1274
|
+
lines.push("No source removals suggested.");
|
|
1275
|
+
}
|
|
1276
|
+
if (warnings.length > 0) {
|
|
1277
|
+
lines.push("Warnings (write allowed):", ...warnings.map((warning) => `- ${warning}`));
|
|
1278
|
+
}
|
|
1279
|
+
return lines.join("\n");
|
|
1280
|
+
}
|
|
1281
|
+
function credentialWarnings(content, location) {
|
|
1282
|
+
const patterns = [
|
|
1283
|
+
{ name: "AWS access-key-shaped credential", pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/ },
|
|
1284
|
+
{ name: "GitHub-token-shaped credential", pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/ },
|
|
1285
|
+
{ name: "API-key-shaped credential", pattern: /\bsk-[A-Za-z0-9]{20,}\b/ },
|
|
1286
|
+
{ name: "private-key material", pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
|
|
1287
|
+
];
|
|
1288
|
+
return patterns
|
|
1289
|
+
.filter(({ pattern }) => pattern.test(content))
|
|
1290
|
+
.map(({ name }) => `Potential ${name} in ${location}; review before cleanup.`);
|
|
1291
|
+
}
|
|
1292
|
+
function renderDreamProposalsText(proposals) {
|
|
1293
|
+
const rendered = renderProposalsText(proposals, "dream-memory");
|
|
1294
|
+
return rendered.replace(/^No dream-memory proposals\./, "No dream proposals.").replace(/^dream-memory proposals/, "dream proposals");
|
|
1295
|
+
}
|
|
769
1296
|
function searchDocument(memory) {
|
|
770
1297
|
return [
|
|
771
1298
|
memory.project_name,
|