clementine-agent 1.0.33 → 1.0.34
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/dist/memory/context-assembler.js +30 -17
- package/dist/memory/graph-store.d.ts +16 -0
- package/dist/memory/graph-store.js +47 -0
- package/dist/memory/store.js +25 -14
- package/package.json +1 -1
|
@@ -33,14 +33,15 @@ export async function assembleContext(options) {
|
|
|
33
33
|
priority: 0,
|
|
34
34
|
maxChars: 500,
|
|
35
35
|
minRemainingBudget: 0,
|
|
36
|
-
resolve: () => {
|
|
36
|
+
resolve: (budget) => {
|
|
37
37
|
if (!fs.existsSync(idPath))
|
|
38
38
|
return '';
|
|
39
39
|
try {
|
|
40
40
|
const content = fs.readFileSync(idPath, 'utf-8').trim();
|
|
41
41
|
if (!content)
|
|
42
42
|
return '';
|
|
43
|
-
|
|
43
|
+
const block = `## Identity\n\n${content}`;
|
|
44
|
+
return block.length > budget ? block.slice(0, budget) : block;
|
|
44
45
|
}
|
|
45
46
|
catch {
|
|
46
47
|
return '';
|
|
@@ -56,14 +57,15 @@ export async function assembleContext(options) {
|
|
|
56
57
|
priority: 1,
|
|
57
58
|
maxChars: isAutonomous ? 1000 : 2000,
|
|
58
59
|
minRemainingBudget: 0,
|
|
59
|
-
resolve: () => {
|
|
60
|
+
resolve: (budget) => {
|
|
60
61
|
if (!fs.existsSync(wmPath))
|
|
61
62
|
return '';
|
|
62
63
|
try {
|
|
63
64
|
const content = fs.readFileSync(wmPath, 'utf-8').trim();
|
|
64
65
|
if (!content)
|
|
65
66
|
return '';
|
|
66
|
-
|
|
67
|
+
const block = `## Working Memory (scratchpad)\n\n${content}`;
|
|
68
|
+
return block.length > budget ? block.slice(0, budget) : block;
|
|
67
69
|
}
|
|
68
70
|
catch {
|
|
69
71
|
return '';
|
|
@@ -79,10 +81,15 @@ export async function assembleContext(options) {
|
|
|
79
81
|
priority: 2,
|
|
80
82
|
maxChars: isAutonomous ? 1000 : 2000,
|
|
81
83
|
minRemainingBudget: 500,
|
|
82
|
-
resolve: () => skillCtx,
|
|
84
|
+
resolve: (budget) => skillCtx.length > budget ? skillCtx.slice(0, budget) : skillCtx,
|
|
83
85
|
});
|
|
84
86
|
}
|
|
85
87
|
// Slot 3: Memory search results (core recall)
|
|
88
|
+
// formatResultsForPrompt respects the effective budget and breaks on
|
|
89
|
+
// entry boundaries (not mid-string), so we don't need the outer
|
|
90
|
+
// slice-truncation to kick in here. Previously this slot was double-
|
|
91
|
+
// truncated: formatter used its own 8000 cap, then the outer loop cut
|
|
92
|
+
// further by Math.min(maxChars, remaining), chopping entries in half.
|
|
86
93
|
if (options.memoryResults && options.memoryResults.length > 0) {
|
|
87
94
|
const results = options.memoryResults;
|
|
88
95
|
slots.push({
|
|
@@ -90,10 +97,7 @@ export async function assembleContext(options) {
|
|
|
90
97
|
priority: 3,
|
|
91
98
|
maxChars: isAutonomous ? 2000 : 8000,
|
|
92
99
|
minRemainingBudget: 200,
|
|
93
|
-
resolve: () =>
|
|
94
|
-
// formatResultsForPrompt already handles truncation within its own budget
|
|
95
|
-
return formatResultsForPrompt(results, isAutonomous ? 2000 : 8000);
|
|
96
|
-
},
|
|
100
|
+
resolve: (budget) => formatResultsForPrompt(results, budget),
|
|
97
101
|
});
|
|
98
102
|
}
|
|
99
103
|
// Slot 4: Graph relationships (supplementary)
|
|
@@ -104,7 +108,7 @@ export async function assembleContext(options) {
|
|
|
104
108
|
priority: 4,
|
|
105
109
|
maxChars: 2000,
|
|
106
110
|
minRemainingBudget: 500,
|
|
107
|
-
resolve: () => graphCtx,
|
|
111
|
+
resolve: (budget) => graphCtx.length > budget ? graphCtx.slice(0, budget) : graphCtx,
|
|
108
112
|
});
|
|
109
113
|
}
|
|
110
114
|
// Sort by priority (lower number = higher priority)
|
|
@@ -121,18 +125,27 @@ export async function assembleContext(options) {
|
|
|
121
125
|
continue;
|
|
122
126
|
}
|
|
123
127
|
try {
|
|
124
|
-
|
|
128
|
+
// The slot's effective budget is the smaller of its own maxChars and
|
|
129
|
+
// what's actually remaining across all slots. Passed into resolve so
|
|
130
|
+
// the slot produces right-sized content up front, not a mid-entry
|
|
131
|
+
// truncation after the fact.
|
|
132
|
+
const effectiveBudget = Math.min(slot.maxChars, remaining);
|
|
133
|
+
const content = await slot.resolve(effectiveBudget);
|
|
125
134
|
if (!content) {
|
|
126
135
|
skipped.push(slot.name);
|
|
127
136
|
continue;
|
|
128
137
|
}
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
// Safety net: if resolve() ignored the budget and returned too much,
|
|
139
|
+
// clip at a line boundary rather than a character boundary so we don't
|
|
140
|
+
// leave a malformed half-block in the prompt.
|
|
141
|
+
let finalContent = content;
|
|
142
|
+
if (content.length > effectiveBudget) {
|
|
143
|
+
const trimmed = content.slice(0, effectiveBudget);
|
|
144
|
+
const lastNewline = trimmed.lastIndexOf('\n');
|
|
145
|
+
finalContent = (lastNewline > 0 ? trimmed.slice(0, lastNewline) : trimmed) + '\n...(truncated)';
|
|
133
146
|
}
|
|
134
|
-
parts.push(
|
|
135
|
-
remaining -=
|
|
147
|
+
parts.push(finalContent);
|
|
148
|
+
remaining -= finalContent.length;
|
|
136
149
|
included.push(slot.name);
|
|
137
150
|
}
|
|
138
151
|
catch {
|
|
@@ -61,6 +61,22 @@ export declare class GraphStore {
|
|
|
61
61
|
syncFromVault(vaultDir: string, agentsDir: string): Promise<GraphSyncStats>;
|
|
62
62
|
extractAndStoreRelationships(triplets: RelationshipTriplet[]): Promise<void>;
|
|
63
63
|
enrichWithGraphContext(entityIds: string[], _maxHops?: number): Promise<string>;
|
|
64
|
+
/**
|
|
65
|
+
* Drop Note nodes whose slug isn't in the caller-provided set of valid IDs.
|
|
66
|
+
* Wikilinks into deleted vault files leave dangling Note nodes with
|
|
67
|
+
* MENTIONS edges pointing at them — this cleans those up.
|
|
68
|
+
*
|
|
69
|
+
* Deliberately NOT auto-scheduled: blast radius is significant, and the
|
|
70
|
+
* caller (dashboard action, MCP tool, manual script) should supply the
|
|
71
|
+
* authoritative valid-IDs set. Runs DETACH DELETE so incoming edges go
|
|
72
|
+
* with the node.
|
|
73
|
+
*
|
|
74
|
+
* Returns counts of what was removed.
|
|
75
|
+
*/
|
|
76
|
+
invalidateOrphanedNotes(validIds: Set<string>): Promise<{
|
|
77
|
+
scanned: number;
|
|
78
|
+
deleted: number;
|
|
79
|
+
}>;
|
|
64
80
|
}
|
|
65
81
|
export declare function getSharedGraphStore(persistenceDir: string): Promise<GraphStore | null>;
|
|
66
82
|
//# sourceMappingURL=graph-store.d.ts.map
|
|
@@ -580,6 +580,53 @@ export class GraphStore {
|
|
|
580
580
|
return '';
|
|
581
581
|
return '\n## Relationship Context\n' + lines.join('\n');
|
|
582
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Drop Note nodes whose slug isn't in the caller-provided set of valid IDs.
|
|
585
|
+
* Wikilinks into deleted vault files leave dangling Note nodes with
|
|
586
|
+
* MENTIONS edges pointing at them — this cleans those up.
|
|
587
|
+
*
|
|
588
|
+
* Deliberately NOT auto-scheduled: blast radius is significant, and the
|
|
589
|
+
* caller (dashboard action, MCP tool, manual script) should supply the
|
|
590
|
+
* authoritative valid-IDs set. Runs DETACH DELETE so incoming edges go
|
|
591
|
+
* with the node.
|
|
592
|
+
*
|
|
593
|
+
* Returns counts of what was removed.
|
|
594
|
+
*/
|
|
595
|
+
async invalidateOrphanedNotes(validIds) {
|
|
596
|
+
if (!this.available)
|
|
597
|
+
return { scanned: 0, deleted: 0 };
|
|
598
|
+
if (validIds.size === 0) {
|
|
599
|
+
// Defense: refuse to run with an empty set — would delete every Note.
|
|
600
|
+
logger.warn('invalidateOrphanedNotes called with empty validIds — refusing to run');
|
|
601
|
+
return { scanned: 0, deleted: 0 };
|
|
602
|
+
}
|
|
603
|
+
let scanned = 0;
|
|
604
|
+
let deleted = 0;
|
|
605
|
+
try {
|
|
606
|
+
const res = await this.graph.query('MATCH (n:Note) RETURN n.id AS id');
|
|
607
|
+
const rows = (res.data ?? []);
|
|
608
|
+
scanned = rows.length;
|
|
609
|
+
for (const row of rows) {
|
|
610
|
+
const id = row.id;
|
|
611
|
+
if (!id || validIds.has(id))
|
|
612
|
+
continue;
|
|
613
|
+
try {
|
|
614
|
+
await this.graph.query('MATCH (n:Note {id: $id}) DETACH DELETE n', { params: { id } });
|
|
615
|
+
deleted++;
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
logger.debug({ err, id }, 'Orphan Note deletion failed');
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
logger.warn({ err }, 'invalidateOrphanedNotes query failed');
|
|
624
|
+
}
|
|
625
|
+
if (deleted > 0) {
|
|
626
|
+
logger.info({ scanned, deleted, validIdsSize: validIds.size }, 'Invalidated orphan Note nodes');
|
|
627
|
+
}
|
|
628
|
+
return { scanned, deleted };
|
|
629
|
+
}
|
|
583
630
|
}
|
|
584
631
|
// ── Shared Client Helper ───────────────────────────────────────────────
|
|
585
632
|
/**
|
package/dist/memory/store.js
CHANGED
|
@@ -14,6 +14,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statS
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import Database from 'better-sqlite3';
|
|
16
16
|
import { BASE_DIR } from '../config.js';
|
|
17
|
+
import { temporalDecay } from './search.js';
|
|
17
18
|
import * as embeddingsModule from './embeddings.js';
|
|
18
19
|
import { chunkFile } from './chunker.js';
|
|
19
20
|
import { mmrRerank } from './mmr.js';
|
|
@@ -733,20 +734,30 @@ export class MemoryStore {
|
|
|
733
734
|
* Get the most recently updated chunks.
|
|
734
735
|
*/
|
|
735
736
|
getRecentChunks(limit = 5, agentSlug, filters, strict = false) {
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
737
|
+
const now = Date.now();
|
|
738
|
+
const mapRow = (row) => {
|
|
739
|
+
// Score recency by exponential decay (half-life 30 days). Previously
|
|
740
|
+
// every recent row got score=0, which meant MMR's min-max normalization
|
|
741
|
+
// ranked them at the floor — a two-day-old chunk and a six-month-old
|
|
742
|
+
// chunk were indistinguishable. Decay lets recent results actually
|
|
743
|
+
// compete with FTS and vector matches during rerank.
|
|
744
|
+
const daysOld = row.updated_at ? (now - Date.parse(row.updated_at)) / 86_400_000 : 0;
|
|
745
|
+
const decayed = temporalDecay(daysOld);
|
|
746
|
+
return {
|
|
747
|
+
sourceFile: row.source_file,
|
|
748
|
+
section: row.section,
|
|
749
|
+
content: row.content,
|
|
750
|
+
score: decayed,
|
|
751
|
+
chunkType: row.chunk_type,
|
|
752
|
+
matchType: 'recency',
|
|
753
|
+
lastUpdated: row.updated_at ?? '',
|
|
754
|
+
chunkId: row.id,
|
|
755
|
+
salience: row.salience ?? 0,
|
|
756
|
+
agentSlug: row.agent_slug ?? null,
|
|
757
|
+
category: row.category,
|
|
758
|
+
topic: row.topic,
|
|
759
|
+
};
|
|
760
|
+
};
|
|
750
761
|
// Build optional WHERE clauses for category/topic
|
|
751
762
|
let filterSql = '';
|
|
752
763
|
const filterParams = [];
|