@psiclawops/hypermem 0.1.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/ARCHITECTURE.md +4 -3
- package/README.md +457 -174
- package/dist/background-indexer.d.ts +19 -4
- package/dist/background-indexer.d.ts.map +1 -1
- package/dist/background-indexer.js +329 -17
- package/dist/cache.d.ts +110 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +495 -0
- package/dist/compaction-fence.d.ts +1 -1
- package/dist/compaction-fence.js +1 -1
- package/dist/compositor.d.ts +114 -27
- package/dist/compositor.d.ts.map +1 -1
- package/dist/compositor.js +1678 -229
- package/dist/content-type-classifier.d.ts +41 -0
- package/dist/content-type-classifier.d.ts.map +1 -0
- package/dist/content-type-classifier.js +181 -0
- package/dist/cross-agent.d.ts +5 -0
- package/dist/cross-agent.d.ts.map +1 -1
- package/dist/cross-agent.js +5 -0
- package/dist/db.d.ts +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +6 -2
- package/dist/desired-state-store.d.ts +1 -1
- package/dist/desired-state-store.d.ts.map +1 -1
- package/dist/desired-state-store.js +15 -5
- package/dist/doc-chunk-store.d.ts +26 -1
- package/dist/doc-chunk-store.d.ts.map +1 -1
- package/dist/doc-chunk-store.js +114 -1
- package/dist/doc-chunker.d.ts +1 -1
- package/dist/doc-chunker.js +1 -1
- package/dist/dreaming-promoter.d.ts +86 -0
- package/dist/dreaming-promoter.d.ts.map +1 -0
- package/dist/dreaming-promoter.js +381 -0
- package/dist/episode-store.d.ts +2 -1
- package/dist/episode-store.d.ts.map +1 -1
- package/dist/episode-store.js +4 -4
- package/dist/fact-store.d.ts +19 -1
- package/dist/fact-store.d.ts.map +1 -1
- package/dist/fact-store.js +64 -3
- package/dist/fleet-store.d.ts +1 -1
- package/dist/fleet-store.js +1 -1
- package/dist/fos-mod.d.ts +178 -0
- package/dist/fos-mod.d.ts.map +1 -0
- package/dist/fos-mod.js +416 -0
- package/dist/hybrid-retrieval.d.ts +5 -1
- package/dist/hybrid-retrieval.d.ts.map +1 -1
- package/dist/hybrid-retrieval.js +7 -3
- package/dist/image-eviction.d.ts +49 -0
- package/dist/image-eviction.d.ts.map +1 -0
- package/dist/image-eviction.js +251 -0
- package/dist/index.d.ts +50 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -43
- package/dist/keystone-scorer.d.ts +51 -0
- package/dist/keystone-scorer.d.ts.map +1 -0
- package/dist/keystone-scorer.js +52 -0
- package/dist/knowledge-graph.d.ts +1 -1
- package/dist/knowledge-graph.js +1 -1
- package/dist/knowledge-lint.d.ts +29 -0
- package/dist/knowledge-lint.d.ts.map +1 -0
- package/dist/knowledge-lint.js +116 -0
- package/dist/knowledge-store.d.ts +1 -1
- package/dist/knowledge-store.d.ts.map +1 -1
- package/dist/knowledge-store.js +8 -2
- package/dist/library-schema.d.ts +3 -3
- package/dist/library-schema.d.ts.map +1 -1
- package/dist/library-schema.js +324 -3
- package/dist/message-store.d.ts +15 -2
- package/dist/message-store.d.ts.map +1 -1
- package/dist/message-store.js +51 -1
- package/dist/metrics-dashboard.d.ts +114 -0
- package/dist/metrics-dashboard.d.ts.map +1 -0
- package/dist/metrics-dashboard.js +260 -0
- package/dist/obsidian-exporter.d.ts +57 -0
- package/dist/obsidian-exporter.d.ts.map +1 -0
- package/dist/obsidian-exporter.js +274 -0
- package/dist/obsidian-watcher.d.ts +147 -0
- package/dist/obsidian-watcher.d.ts.map +1 -0
- package/dist/obsidian-watcher.js +403 -0
- package/dist/open-domain.d.ts +46 -0
- package/dist/open-domain.d.ts.map +1 -0
- package/dist/open-domain.js +125 -0
- package/dist/preference-store.d.ts +1 -1
- package/dist/preference-store.js +1 -1
- package/dist/preservation-gate.d.ts +1 -1
- package/dist/preservation-gate.js +1 -1
- package/dist/proactive-pass.d.ts +63 -0
- package/dist/proactive-pass.d.ts.map +1 -0
- package/dist/proactive-pass.js +239 -0
- package/dist/profiles.d.ts +44 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +227 -0
- package/dist/provider-translator.d.ts +13 -3
- package/dist/provider-translator.d.ts.map +1 -1
- package/dist/provider-translator.js +63 -9
- package/dist/rate-limiter.d.ts +1 -1
- package/dist/rate-limiter.js +1 -1
- package/dist/repair-tool-pairs.d.ts +38 -0
- package/dist/repair-tool-pairs.d.ts.map +1 -0
- package/dist/repair-tool-pairs.js +138 -0
- package/dist/retrieval-policy.d.ts +51 -0
- package/dist/retrieval-policy.d.ts.map +1 -0
- package/dist/retrieval-policy.js +77 -0
- package/dist/schema.d.ts +2 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +28 -2
- package/dist/secret-scanner.d.ts +1 -1
- package/dist/secret-scanner.js +1 -1
- package/dist/seed.d.ts +2 -2
- package/dist/seed.js +2 -2
- package/dist/session-flusher.d.ts +53 -0
- package/dist/session-flusher.d.ts.map +1 -0
- package/dist/session-flusher.js +69 -0
- package/dist/session-topic-map.d.ts +41 -0
- package/dist/session-topic-map.d.ts.map +1 -0
- package/dist/session-topic-map.js +77 -0
- package/dist/spawn-context.d.ts +54 -0
- package/dist/spawn-context.d.ts.map +1 -0
- package/dist/spawn-context.js +159 -0
- package/dist/system-store.d.ts +1 -1
- package/dist/system-store.js +1 -1
- package/dist/temporal-store.d.ts +80 -0
- package/dist/temporal-store.d.ts.map +1 -0
- package/dist/temporal-store.js +149 -0
- package/dist/topic-detector.d.ts +35 -0
- package/dist/topic-detector.d.ts.map +1 -0
- package/dist/topic-detector.js +249 -0
- package/dist/topic-store.d.ts +1 -1
- package/dist/topic-store.js +1 -1
- package/dist/topic-synthesizer.d.ts +51 -0
- package/dist/topic-synthesizer.d.ts.map +1 -0
- package/dist/topic-synthesizer.js +315 -0
- package/dist/trigger-registry.d.ts +63 -0
- package/dist/trigger-registry.d.ts.map +1 -0
- package/dist/trigger-registry.js +163 -0
- package/dist/types.d.ts +214 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/vector-store.d.ts +43 -5
- package/dist/vector-store.d.ts.map +1 -1
- package/dist/vector-store.js +189 -10
- package/dist/version.d.ts +34 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +34 -0
- package/dist/wiki-page-emitter.d.ts +65 -0
- package/dist/wiki-page-emitter.d.ts.map +1 -0
- package/dist/wiki-page-emitter.js +258 -0
- package/dist/work-store.d.ts +1 -1
- package/dist/work-store.js +1 -1
- package/package.json +15 -5
- package/dist/redis.d.ts +0 -188
- package/dist/redis.d.ts.map +0 -1
- package/dist/redis.js +0 -534
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* open-domain.ts — Open-domain query detection and FTS5 retrieval
|
|
3
|
+
*
|
|
4
|
+
* LoCoMo benchmark open-domain questions are broad, exploratory, and have no
|
|
5
|
+
* topical anchor. They span the full conversation history and require content
|
|
6
|
+
* that may have been filtered out by the quality gate (isQualityFact). The
|
|
7
|
+
* fix: detect open-domain queries and run a separate FTS5 search against raw
|
|
8
|
+
* messages_fts, bypassing the quality filter entirely.
|
|
9
|
+
*
|
|
10
|
+
* Detection heuristics (conservative — false positives add noise):
|
|
11
|
+
* - Short query with no named entities (no TitleCase tokens)
|
|
12
|
+
* - Broad interrogative patterns (what did, how did, tell me about, etc.)
|
|
13
|
+
* - No temporal signals (those go to the temporal retrieval path)
|
|
14
|
+
* - No specific identifiers (URLs, IDs, ticket numbers, version strings)
|
|
15
|
+
*
|
|
16
|
+
* Retrieval: MessageStore.searchMessages() against messages_fts — covers all
|
|
17
|
+
* raw message history regardless of quality gate.
|
|
18
|
+
*/
|
|
19
|
+
// ── Open-domain signal patterns ───────────────────────────────────────────
|
|
20
|
+
const BROAD_INTERROGATIVE = /\b(what did|what does|what has|what was|what were|what is|how did|how does|how has|tell me about|describe|explain|summarize|overview|recap|what do you know about|what have|who is|who was|who did)\b/i;
|
|
21
|
+
const SPECIFIC_ANCHOR = /\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})+|v\d+\.\d+|#\d{2,}|https?:\/\/|[A-Z]{2,}-\d+)\b/;
|
|
22
|
+
const TEMPORAL_SIGNALS = /\b(before|after|when|last\s+\w+|yesterday|today|recently|between|since|until|ago|this\s+week|this\s+month|in\s+(january|february|march|april|may|june|july|august|september|october|november|december))\b/i;
|
|
23
|
+
/**
|
|
24
|
+
* Returns true if the query looks like an open-domain question:
|
|
25
|
+
* broad, exploratory, no specific anchors, no temporal signals.
|
|
26
|
+
*/
|
|
27
|
+
export function isOpenDomainQuery(query) {
|
|
28
|
+
if (!query || query.trim().length < 8)
|
|
29
|
+
return false;
|
|
30
|
+
// Has temporal signals → temporal path handles it
|
|
31
|
+
if (TEMPORAL_SIGNALS.test(query))
|
|
32
|
+
return false;
|
|
33
|
+
// Has specific named entity / version / ticket anchor → not open-domain
|
|
34
|
+
if (SPECIFIC_ANCHOR.test(query))
|
|
35
|
+
return false;
|
|
36
|
+
// Must match a broad interrogative pattern
|
|
37
|
+
if (!BROAD_INTERROGATIVE.test(query))
|
|
38
|
+
return false;
|
|
39
|
+
// Sanity: query should not be too long (long queries are usually specific)
|
|
40
|
+
const wordCount = query.trim().split(/\s+/).length;
|
|
41
|
+
if (wordCount > 20)
|
|
42
|
+
return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// ── FTS5 query builder ────────────────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Build a FTS5 MATCH query from a broad question.
|
|
48
|
+
* Strips stop words, question words, and punctuation.
|
|
49
|
+
* Returns up to 6 prefix-matched terms joined with OR.
|
|
50
|
+
*/
|
|
51
|
+
export function buildOpenDomainFtsQuery(query) {
|
|
52
|
+
const STOP_WORDS = new Set([
|
|
53
|
+
'what', 'did', 'does', 'has', 'was', 'were', 'is', 'are', 'how',
|
|
54
|
+
'tell', 'me', 'about', 'describe', 'explain', 'summarize', 'overview',
|
|
55
|
+
'recap', 'who', 'do', 'you', 'know', 'have', 'the', 'a', 'an', 'of',
|
|
56
|
+
'in', 'on', 'at', 'to', 'for', 'and', 'or', 'but', 'with', 'from',
|
|
57
|
+
]);
|
|
58
|
+
const terms = query
|
|
59
|
+
.toLowerCase()
|
|
60
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
61
|
+
.split(/\s+/)
|
|
62
|
+
.filter(w => w.length >= 3 && !STOP_WORDS.has(w))
|
|
63
|
+
.slice(0, 6)
|
|
64
|
+
.map(w => `${w}*`);
|
|
65
|
+
if (terms.length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
return terms.join(' OR ');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Search raw message history via FTS5 for open-domain queries.
|
|
71
|
+
* Returns up to `limit` matching messages, deduplicated against existing context.
|
|
72
|
+
*
|
|
73
|
+
* @param db — agent messages DB (contains messages_fts)
|
|
74
|
+
* @param query — the user's query
|
|
75
|
+
* @param existingContent — already-assembled context (for dedup)
|
|
76
|
+
* @param limit — max results (default 10)
|
|
77
|
+
*/
|
|
78
|
+
export function searchOpenDomain(db, query, existingContent, limit = 10) {
|
|
79
|
+
const ftsQuery = buildOpenDomainFtsQuery(query);
|
|
80
|
+
if (!ftsQuery)
|
|
81
|
+
return [];
|
|
82
|
+
try {
|
|
83
|
+
const rows = db.prepare(`
|
|
84
|
+
WITH fts_matches AS (
|
|
85
|
+
SELECT rowid, rank
|
|
86
|
+
FROM messages_fts
|
|
87
|
+
WHERE messages_fts MATCH ?
|
|
88
|
+
ORDER BY rank
|
|
89
|
+
LIMIT ?
|
|
90
|
+
)
|
|
91
|
+
SELECT
|
|
92
|
+
m.role,
|
|
93
|
+
m.text_content AS content,
|
|
94
|
+
m.created_at AS createdAt
|
|
95
|
+
FROM messages m
|
|
96
|
+
JOIN fts_matches ON m.id = fts_matches.rowid
|
|
97
|
+
WHERE m.text_content IS NOT NULL
|
|
98
|
+
AND m.text_content != ''
|
|
99
|
+
AND m.is_heartbeat = 0
|
|
100
|
+
ORDER BY fts_matches.rank
|
|
101
|
+
`).all(ftsQuery, limit * 2);
|
|
102
|
+
// Deduplicate against existing context and filter short content
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
const results = [];
|
|
105
|
+
for (const row of rows) {
|
|
106
|
+
if (!row.content || row.content.trim().length < 20)
|
|
107
|
+
continue;
|
|
108
|
+
const fingerprint = row.content.slice(0, 80);
|
|
109
|
+
if (seen.has(fingerprint))
|
|
110
|
+
continue;
|
|
111
|
+
if (existingContent.includes(fingerprint))
|
|
112
|
+
continue;
|
|
113
|
+
seen.add(fingerprint);
|
|
114
|
+
results.push(row);
|
|
115
|
+
if (results.length >= limit)
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// FTS query may fail on special characters — degrade silently
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=open-domain.js.map
|
package/dist/preference-store.js
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Proactive Passes
|
|
3
|
+
*
|
|
4
|
+
* Background maintenance passes that run between indexer ticks to keep
|
|
5
|
+
* message storage lean. Two passes:
|
|
6
|
+
*
|
|
7
|
+
* 1. Noise Sweep — deletes low/zero-signal messages outside the recent
|
|
8
|
+
* window (heartbeats, acks, empty strings, control tokens).
|
|
9
|
+
*
|
|
10
|
+
* 2. Tool Decay — truncates oversized tool_results outside the recent
|
|
11
|
+
* window in-place, preserving JSON structure but collapsing large
|
|
12
|
+
* content blobs into a byte-count placeholder.
|
|
13
|
+
*
|
|
14
|
+
* Both passes are:
|
|
15
|
+
* - Synchronous (DatabaseSync, no async)
|
|
16
|
+
* - Wrapped in transactions (atomic)
|
|
17
|
+
* - Best-effort: catch all errors, log, and return a zero-change result
|
|
18
|
+
*
|
|
19
|
+
* Ported and adapted from ClawText proactive-pass.ts.
|
|
20
|
+
* hypermem schema differences vs ClawText:
|
|
21
|
+
* - No content_type column — we classify on the fly via classifyContentType()
|
|
22
|
+
* - No external payload store — we truncate content inline in tool_results JSON
|
|
23
|
+
* - No ClawText-specific dependencies (payload-store, tool-tracker, etc.)
|
|
24
|
+
*/
|
|
25
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
26
|
+
export interface NoiseSweepResult {
|
|
27
|
+
messagesDeleted: number;
|
|
28
|
+
passType: 'noise_sweep';
|
|
29
|
+
}
|
|
30
|
+
export interface ToolDecayResult {
|
|
31
|
+
messagesUpdated: number;
|
|
32
|
+
bytesFreed: number;
|
|
33
|
+
passType: 'tool_decay';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Delete noise and heartbeat messages outside the recent window.
|
|
37
|
+
*
|
|
38
|
+
* "Outside the recent window" means message_index < maxIndex - recentWindowSize.
|
|
39
|
+
* Messages inside the window are never deleted, even if they are noise —
|
|
40
|
+
* the model may still reference them in the current turn.
|
|
41
|
+
*
|
|
42
|
+
* Deletions are wrapped in a single transaction. The FTS5 trigger handles
|
|
43
|
+
* index cleanup automatically (msg_fts_ad fires on DELETE).
|
|
44
|
+
*/
|
|
45
|
+
export declare function runNoiseSweep(db: DatabaseSync, conversationId: number, recentWindowSize?: number): NoiseSweepResult;
|
|
46
|
+
/**
|
|
47
|
+
* Truncate oversized tool_results outside the recent window.
|
|
48
|
+
*
|
|
49
|
+
* Strategy:
|
|
50
|
+
* 1. Find messages whose tool_results JSON string is > 2000 chars total,
|
|
51
|
+
* outside the recent window.
|
|
52
|
+
* 2. Parse the JSON array.
|
|
53
|
+
* 3. For each result entry where the `content` field exceeds 500 chars,
|
|
54
|
+
* replace `content` with `[tool result truncated — N bytes]`.
|
|
55
|
+
* 4. Re-serialize and write back.
|
|
56
|
+
*
|
|
57
|
+
* The JSON structure is preserved (array of result objects). Only the
|
|
58
|
+
* oversized `content` values are collapsed.
|
|
59
|
+
*
|
|
60
|
+
* Mutations are committed in a single transaction.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runToolDecay(db: DatabaseSync, conversationId: number, recentWindowSize?: number): ToolDecayResult;
|
|
63
|
+
//# sourceMappingURL=proactive-pass.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proactive-pass.d.ts","sourceRoot":"","sources":["../src/proactive-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AA6CD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,GAC5B,gBAAgB,CA2ElB;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC1B,EAAE,EAAE,YAAY,EAChB,cAAc,EAAE,MAAM,EACtB,gBAAgB,GAAE,MAAW,GAC5B,eAAe,CAiGjB"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Proactive Passes
|
|
3
|
+
*
|
|
4
|
+
* Background maintenance passes that run between indexer ticks to keep
|
|
5
|
+
* message storage lean. Two passes:
|
|
6
|
+
*
|
|
7
|
+
* 1. Noise Sweep — deletes low/zero-signal messages outside the recent
|
|
8
|
+
* window (heartbeats, acks, empty strings, control tokens).
|
|
9
|
+
*
|
|
10
|
+
* 2. Tool Decay — truncates oversized tool_results outside the recent
|
|
11
|
+
* window in-place, preserving JSON structure but collapsing large
|
|
12
|
+
* content blobs into a byte-count placeholder.
|
|
13
|
+
*
|
|
14
|
+
* Both passes are:
|
|
15
|
+
* - Synchronous (DatabaseSync, no async)
|
|
16
|
+
* - Wrapped in transactions (atomic)
|
|
17
|
+
* - Best-effort: catch all errors, log, and return a zero-change result
|
|
18
|
+
*
|
|
19
|
+
* Ported and adapted from ClawText proactive-pass.ts.
|
|
20
|
+
* hypermem schema differences vs ClawText:
|
|
21
|
+
* - No content_type column — we classify on the fly via classifyContentType()
|
|
22
|
+
* - No external payload store — we truncate content inline in tool_results JSON
|
|
23
|
+
* - No ClawText-specific dependencies (payload-store, tool-tracker, etc.)
|
|
24
|
+
*/
|
|
25
|
+
import { classifyContentType } from './content-type-classifier.js';
|
|
26
|
+
// ─── Internal helpers ────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the safe window to a finite positive integer.
|
|
29
|
+
* Mirrors the ClawText resolveSafeWindow() guard.
|
|
30
|
+
*/
|
|
31
|
+
function resolveSafeWindow(recentWindowSize) {
|
|
32
|
+
if (Number.isFinite(recentWindowSize) && recentWindowSize > 0) {
|
|
33
|
+
return Math.floor(recentWindowSize);
|
|
34
|
+
}
|
|
35
|
+
return 20;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the maximum message_index for a conversation.
|
|
39
|
+
* Returns -1 if no messages exist.
|
|
40
|
+
*/
|
|
41
|
+
function getMaxMessageIndex(db, conversationId) {
|
|
42
|
+
const row = db
|
|
43
|
+
.prepare('SELECT COALESCE(MAX(message_index), -1) AS max_index FROM messages WHERE conversation_id = ?')
|
|
44
|
+
.get(conversationId);
|
|
45
|
+
return typeof row.max_index === 'number' ? row.max_index : -1;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decide if a message is noise based on content + is_heartbeat flag.
|
|
49
|
+
*
|
|
50
|
+
* A message is noise when:
|
|
51
|
+
* - is_heartbeat = 1 (explicit heartbeat marker), OR
|
|
52
|
+
* - text_content is NULL or empty (≤3 chars after trimming), OR
|
|
53
|
+
* - classifyContentType() returns 'noise' or 'ack'
|
|
54
|
+
*
|
|
55
|
+
* We call the classifier rather than duplicating its patterns here.
|
|
56
|
+
*/
|
|
57
|
+
function isNoiseMessage(textContent, isHeartbeat) {
|
|
58
|
+
if (isHeartbeat === 1)
|
|
59
|
+
return true;
|
|
60
|
+
if (textContent === null || textContent.trim().length <= 3)
|
|
61
|
+
return true;
|
|
62
|
+
const { type } = classifyContentType(textContent);
|
|
63
|
+
return type === 'noise' || type === 'ack';
|
|
64
|
+
}
|
|
65
|
+
// ─── Noise Sweep ─────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Delete noise and heartbeat messages outside the recent window.
|
|
68
|
+
*
|
|
69
|
+
* "Outside the recent window" means message_index < maxIndex - recentWindowSize.
|
|
70
|
+
* Messages inside the window are never deleted, even if they are noise —
|
|
71
|
+
* the model may still reference them in the current turn.
|
|
72
|
+
*
|
|
73
|
+
* Deletions are wrapped in a single transaction. The FTS5 trigger handles
|
|
74
|
+
* index cleanup automatically (msg_fts_ad fires on DELETE).
|
|
75
|
+
*/
|
|
76
|
+
export function runNoiseSweep(db, conversationId, recentWindowSize = 20) {
|
|
77
|
+
const ZERO = { messagesDeleted: 0, passType: 'noise_sweep' };
|
|
78
|
+
try {
|
|
79
|
+
const safeWindow = resolveSafeWindow(recentWindowSize);
|
|
80
|
+
const maxIndex = getMaxMessageIndex(db, conversationId);
|
|
81
|
+
if (maxIndex < 0)
|
|
82
|
+
return ZERO;
|
|
83
|
+
// Messages with message_index strictly below this value are eligible.
|
|
84
|
+
const cutoff = maxIndex - safeWindow;
|
|
85
|
+
if (cutoff <= 0)
|
|
86
|
+
return ZERO; // Not enough history yet
|
|
87
|
+
// Fetch all candidate messages outside the recent window.
|
|
88
|
+
// Exclude messages whose content lives entirely in tool_results — those
|
|
89
|
+
// are tool result rows handled by runToolDecay(), not noise sweep.
|
|
90
|
+
// We deliberately avoid a content-based WHERE clause for the classifier
|
|
91
|
+
// because SQLite can't use the index for JS classification logic;
|
|
92
|
+
// it's cheaper to fetch a small batch and classify in JS.
|
|
93
|
+
const candidates = db
|
|
94
|
+
.prepare(`
|
|
95
|
+
SELECT id, text_content, is_heartbeat
|
|
96
|
+
FROM messages
|
|
97
|
+
WHERE conversation_id = ?
|
|
98
|
+
AND message_index < ?
|
|
99
|
+
AND (tool_results IS NULL OR tool_results = '')
|
|
100
|
+
`)
|
|
101
|
+
.all(conversationId, cutoff);
|
|
102
|
+
if (candidates.length === 0)
|
|
103
|
+
return ZERO;
|
|
104
|
+
// Filter to noise messages
|
|
105
|
+
const toDelete = candidates.filter(row => isNoiseMessage(row.text_content, row.is_heartbeat));
|
|
106
|
+
if (toDelete.length === 0)
|
|
107
|
+
return ZERO;
|
|
108
|
+
const ids = toDelete.map(r => r.id);
|
|
109
|
+
// Delete in a transaction; use chunked IN clauses to avoid
|
|
110
|
+
// SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (default 999).
|
|
111
|
+
let totalDeleted = 0;
|
|
112
|
+
const CHUNK = 500;
|
|
113
|
+
db.prepare('BEGIN').run();
|
|
114
|
+
try {
|
|
115
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
116
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
117
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
118
|
+
const result = db
|
|
119
|
+
.prepare(`DELETE FROM messages WHERE id IN (${placeholders})`)
|
|
120
|
+
.run(...chunk);
|
|
121
|
+
totalDeleted += typeof result.changes === 'number' ? result.changes : chunk.length;
|
|
122
|
+
}
|
|
123
|
+
db.prepare('COMMIT').run();
|
|
124
|
+
}
|
|
125
|
+
catch (innerErr) {
|
|
126
|
+
db.prepare('ROLLBACK').run();
|
|
127
|
+
throw innerErr;
|
|
128
|
+
}
|
|
129
|
+
return { messagesDeleted: totalDeleted, passType: 'noise_sweep' };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
console.warn(`[proactive-pass] Noise sweep failed for conversation ${conversationId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
133
|
+
return ZERO;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ─── Tool Decay ──────────────────────────────────────────────────
|
|
137
|
+
/**
|
|
138
|
+
* Truncate oversized tool_results outside the recent window.
|
|
139
|
+
*
|
|
140
|
+
* Strategy:
|
|
141
|
+
* 1. Find messages whose tool_results JSON string is > 2000 chars total,
|
|
142
|
+
* outside the recent window.
|
|
143
|
+
* 2. Parse the JSON array.
|
|
144
|
+
* 3. For each result entry where the `content` field exceeds 500 chars,
|
|
145
|
+
* replace `content` with `[tool result truncated — N bytes]`.
|
|
146
|
+
* 4. Re-serialize and write back.
|
|
147
|
+
*
|
|
148
|
+
* The JSON structure is preserved (array of result objects). Only the
|
|
149
|
+
* oversized `content` values are collapsed.
|
|
150
|
+
*
|
|
151
|
+
* Mutations are committed in a single transaction.
|
|
152
|
+
*/
|
|
153
|
+
export function runToolDecay(db, conversationId, recentWindowSize = 40) {
|
|
154
|
+
const ZERO = { messagesUpdated: 0, bytesFreed: 0, passType: 'tool_decay' };
|
|
155
|
+
try {
|
|
156
|
+
const safeWindow = resolveSafeWindow(recentWindowSize);
|
|
157
|
+
const maxIndex = getMaxMessageIndex(db, conversationId);
|
|
158
|
+
if (maxIndex < 0)
|
|
159
|
+
return ZERO;
|
|
160
|
+
const cutoff = maxIndex - safeWindow;
|
|
161
|
+
if (cutoff <= 0)
|
|
162
|
+
return ZERO;
|
|
163
|
+
// Fetch messages with large tool_results outside the recent window.
|
|
164
|
+
const candidates = db
|
|
165
|
+
.prepare(`
|
|
166
|
+
SELECT id, tool_results
|
|
167
|
+
FROM messages
|
|
168
|
+
WHERE conversation_id = ?
|
|
169
|
+
AND message_index < ?
|
|
170
|
+
AND tool_results IS NOT NULL
|
|
171
|
+
AND length(tool_results) > 2000
|
|
172
|
+
`)
|
|
173
|
+
.all(conversationId, cutoff);
|
|
174
|
+
if (candidates.length === 0)
|
|
175
|
+
return ZERO;
|
|
176
|
+
// Build the update list by processing each candidate.
|
|
177
|
+
const updates = [];
|
|
178
|
+
for (const row of candidates) {
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(row.tool_results);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Corrupt JSON — skip this row
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!Array.isArray(parsed))
|
|
188
|
+
continue;
|
|
189
|
+
let changed = false;
|
|
190
|
+
const newResults = parsed.map(entry => {
|
|
191
|
+
const content = entry.content;
|
|
192
|
+
if (typeof content === 'string' && content.length > 500) {
|
|
193
|
+
const originalBytes = Buffer.byteLength(content, 'utf8');
|
|
194
|
+
changed = true;
|
|
195
|
+
return {
|
|
196
|
+
...entry,
|
|
197
|
+
content: `[tool result truncated — ${originalBytes} bytes]`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return entry;
|
|
201
|
+
});
|
|
202
|
+
if (!changed)
|
|
203
|
+
continue;
|
|
204
|
+
const newJson = JSON.stringify(newResults);
|
|
205
|
+
const savedBytes = row.tool_results.length - newJson.length;
|
|
206
|
+
if (savedBytes > 0) {
|
|
207
|
+
updates.push({ id: row.id, newJson, savedBytes });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (updates.length === 0)
|
|
211
|
+
return ZERO;
|
|
212
|
+
let totalUpdated = 0;
|
|
213
|
+
let totalBytesFreed = 0;
|
|
214
|
+
db.prepare('BEGIN').run();
|
|
215
|
+
try {
|
|
216
|
+
const stmt = db.prepare('UPDATE messages SET tool_results = ? WHERE id = ?');
|
|
217
|
+
for (const { id, newJson, savedBytes } of updates) {
|
|
218
|
+
stmt.run(newJson, id);
|
|
219
|
+
totalUpdated++;
|
|
220
|
+
totalBytesFreed += savedBytes;
|
|
221
|
+
}
|
|
222
|
+
db.prepare('COMMIT').run();
|
|
223
|
+
}
|
|
224
|
+
catch (innerErr) {
|
|
225
|
+
db.prepare('ROLLBACK').run();
|
|
226
|
+
throw innerErr;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
messagesUpdated: totalUpdated,
|
|
230
|
+
bytesFreed: totalBytesFreed,
|
|
231
|
+
passType: 'tool_decay',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
console.warn(`[proactive-pass] Tool decay failed for conversation ${conversationId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
236
|
+
return ZERO;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=proactive-pass.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem configuration profiles
|
|
3
|
+
*
|
|
4
|
+
* Pre-built configs for common deployment patterns. Pass to createHyperMem()
|
|
5
|
+
* directly or use as a base for custom configs via mergeProfile().
|
|
6
|
+
*
|
|
7
|
+
* Profiles:
|
|
8
|
+
* minimal — 64k context, single agent, low resource usage
|
|
9
|
+
* standard — 128k context, fleet default, balanced
|
|
10
|
+
* rich — 200k+ context, multi-agent, full feature set
|
|
11
|
+
*/
|
|
12
|
+
import type { HyperMemConfig } from './types.js';
|
|
13
|
+
export declare const lightProfile: HyperMemConfig;
|
|
14
|
+
export declare const standardProfile: HyperMemConfig;
|
|
15
|
+
export declare const fullProfile: HyperMemConfig;
|
|
16
|
+
export type ProfileName = 'light' | 'standard' | 'full';
|
|
17
|
+
export declare const minimalProfile: HyperMemConfig;
|
|
18
|
+
export declare const extendedProfile: HyperMemConfig;
|
|
19
|
+
export declare const richProfile: HyperMemConfig;
|
|
20
|
+
export declare const PROFILES: Record<ProfileName, HyperMemConfig>;
|
|
21
|
+
/**
|
|
22
|
+
* Load a named profile.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const config = getProfile('light');
|
|
26
|
+
* const hm = createHyperMem(config);
|
|
27
|
+
*/
|
|
28
|
+
export declare function getProfile(name: ProfileName | 'extended'): HyperMemConfig;
|
|
29
|
+
/**
|
|
30
|
+
* Merge a partial config on top of a named profile.
|
|
31
|
+
* Deep-merges compositor and indexer; top-level fields are replaced.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const config = mergeProfile('light', {
|
|
35
|
+
* cache: { keyPrefix: 'myapp:' },
|
|
36
|
+
* compositor: { outputProfile: 'standard' }, // upgrade tier
|
|
37
|
+
* });
|
|
38
|
+
*/
|
|
39
|
+
export declare function mergeProfile(name: ProfileName | 'extended', overrides: DeepPartial<HyperMemConfig>): HyperMemConfig;
|
|
40
|
+
type DeepPartial<T> = {
|
|
41
|
+
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
42
|
+
};
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAyE,MAAM,YAAY,CAAC;AAiExH,eAAO,MAAM,YAAY,EAAE,cAa1B,CAAC;AA6CF,eAAO,MAAM,eAAe,EAAE,cAO7B,CAAC;AA8CF,eAAO,MAAM,WAAW,EAAE,cAWzB,CAAC;AAMF,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAGxD,eAAO,MAAM,cAAc,gBAAe,CAAC;AAC3C,eAAO,MAAM,eAAe,gBAAc,CAAC;AAC3C,eAAO,MAAM,WAAW,gBAAc,CAAC;AAEvC,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,WAAW,EAAE,cAAc,CAIxD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,cAAc,CAIzE;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,WAAW,GAAG,UAAU,EAC9B,SAAS,EAAE,WAAW,CAAC,cAAc,CAAC,GACrC,cAAc,CAUhB;AAMD,KAAK,WAAW,CAAC,CAAC,IAAI;KACnB,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAChE,CAAC"}
|