@psiclawops/hypermem 0.5.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/dist/background-indexer.d.ts +132 -0
- package/dist/background-indexer.d.ts.map +1 -0
- package/dist/background-indexer.js +1044 -0
- 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 +89 -0
- package/dist/compaction-fence.d.ts.map +1 -0
- package/dist/compaction-fence.js +153 -0
- package/dist/compositor.d.ts +226 -0
- package/dist/compositor.d.ts.map +1 -0
- package/dist/compositor.js +2558 -0
- 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 +62 -0
- package/dist/cross-agent.d.ts.map +1 -0
- package/dist/cross-agent.js +259 -0
- package/dist/db.d.ts +131 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +402 -0
- package/dist/desired-state-store.d.ts +100 -0
- package/dist/desired-state-store.d.ts.map +1 -0
- package/dist/desired-state-store.js +222 -0
- package/dist/doc-chunk-store.d.ts +140 -0
- package/dist/doc-chunk-store.d.ts.map +1 -0
- package/dist/doc-chunk-store.js +391 -0
- package/dist/doc-chunker.d.ts +99 -0
- package/dist/doc-chunker.d.ts.map +1 -0
- package/dist/doc-chunker.js +324 -0
- 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 +49 -0
- package/dist/episode-store.d.ts.map +1 -0
- package/dist/episode-store.js +135 -0
- package/dist/fact-store.d.ts +75 -0
- package/dist/fact-store.d.ts.map +1 -0
- package/dist/fact-store.js +236 -0
- package/dist/fleet-store.d.ts +144 -0
- package/dist/fleet-store.d.ts.map +1 -0
- package/dist/fleet-store.js +276 -0
- 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 +64 -0
- package/dist/hybrid-retrieval.d.ts.map +1 -0
- package/dist/hybrid-retrieval.js +344 -0
- 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 +650 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1072 -0
- 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 +110 -0
- package/dist/knowledge-graph.d.ts.map +1 -0
- package/dist/knowledge-graph.js +305 -0
- 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 +72 -0
- package/dist/knowledge-store.d.ts.map +1 -0
- package/dist/knowledge-store.js +247 -0
- package/dist/library-schema.d.ts +22 -0
- package/dist/library-schema.d.ts.map +1 -0
- package/dist/library-schema.js +1038 -0
- package/dist/message-store.d.ts +89 -0
- package/dist/message-store.d.ts.map +1 -0
- package/dist/message-store.js +323 -0
- 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 +54 -0
- package/dist/preference-store.d.ts.map +1 -0
- package/dist/preference-store.js +109 -0
- package/dist/preservation-gate.d.ts +82 -0
- package/dist/preservation-gate.d.ts.map +1 -0
- package/dist/preservation-gate.js +150 -0
- 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 +50 -0
- package/dist/provider-translator.d.ts.map +1 -0
- package/dist/provider-translator.js +403 -0
- package/dist/rate-limiter.d.ts +76 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +179 -0
- 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 +15 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +229 -0
- package/dist/secret-scanner.d.ts +51 -0
- package/dist/secret-scanner.d.ts.map +1 -0
- package/dist/secret-scanner.js +248 -0
- package/dist/seed.d.ts +108 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +177 -0
- 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 +73 -0
- package/dist/system-store.d.ts.map +1 -0
- package/dist/system-store.js +182 -0
- 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 +45 -0
- package/dist/topic-store.d.ts.map +1 -0
- package/dist/topic-store.js +136 -0
- 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 +533 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/vector-store.d.ts +170 -0
- package/dist/vector-store.d.ts.map +1 -0
- package/dist/vector-store.js +677 -0
- 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 +112 -0
- package/dist/work-store.d.ts.map +1 -0
- package/dist/work-store.js +273 -0
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Temporal Store
|
|
3
|
+
*
|
|
4
|
+
* Time-range retrieval over indexed facts. Uses the temporal_index table
|
|
5
|
+
* in library.db to answer LoCoMo-style temporal questions:
|
|
6
|
+
* "What happened before X?"
|
|
7
|
+
* "What changed between January and March?"
|
|
8
|
+
* "What was the most recent thing about Y?"
|
|
9
|
+
*
|
|
10
|
+
* occurred_at is initially populated from created_at (ingest time as proxy,
|
|
11
|
+
* confidence=0.5). Future: date extraction from fact text (confidence=0.9).
|
|
12
|
+
*
|
|
13
|
+
* Query path: SQL time-range filter on temporal_index JOIN facts.
|
|
14
|
+
* No vector search involved — purely temporal ordering.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Temporal signal keywords. If any of these appear in a query string,
|
|
18
|
+
* the temporal retrieval path fires alongside vector/FTS retrieval.
|
|
19
|
+
*/
|
|
20
|
+
const TEMPORAL_SIGNALS = [
|
|
21
|
+
'before', 'after', 'when', 'during', 'between', 'since', 'until',
|
|
22
|
+
'last', 'first', 'recent', 'earlier', 'later', 'previously', 'at the time',
|
|
23
|
+
'used to', 'now', 'still', 'anymore', 'changed', 'updated', 'latest',
|
|
24
|
+
'yesterday', 'today', 'week', 'month', 'year', 'january', 'february',
|
|
25
|
+
'march', 'april', 'may', 'june', 'july', 'august', 'september',
|
|
26
|
+
'october', 'november', 'december', 'q1', 'q2', 'q3', 'q4',
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Returns true if the query string contains temporal signals.
|
|
30
|
+
*/
|
|
31
|
+
export function hasTemporalSignals(query) {
|
|
32
|
+
const lower = query.toLowerCase();
|
|
33
|
+
return TEMPORAL_SIGNALS.some(signal => lower.includes(signal));
|
|
34
|
+
}
|
|
35
|
+
export class TemporalStore {
|
|
36
|
+
db;
|
|
37
|
+
constructor(db) {
|
|
38
|
+
this.db = db;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Index a newly created or updated fact into temporal_index.
|
|
42
|
+
* Uses created_at as occurred_at proxy (confidence=0.5).
|
|
43
|
+
* Safe to call multiple times — uses INSERT OR REPLACE.
|
|
44
|
+
*/
|
|
45
|
+
indexFact(factId, agentId, createdAt, opts) {
|
|
46
|
+
const ingestMs = new Date(createdAt).getTime();
|
|
47
|
+
const occurredMs = opts?.occurredAt ?? ingestMs;
|
|
48
|
+
this.db.prepare(`
|
|
49
|
+
INSERT OR REPLACE INTO temporal_index
|
|
50
|
+
(fact_id, agent_id, occurred_at, ingest_at, time_ref, confidence)
|
|
51
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
52
|
+
`).run(factId, agentId, occurredMs, ingestMs, opts?.timeRef ?? null, opts?.confidence ?? 0.5);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Time-range query. Returns facts in temporal order.
|
|
56
|
+
* Joins temporal_index with facts to get content.
|
|
57
|
+
*/
|
|
58
|
+
timeRangeQuery(opts = {}) {
|
|
59
|
+
const limit = opts.limit ?? 20;
|
|
60
|
+
const order = opts.order ?? 'DESC';
|
|
61
|
+
const minConf = opts.minConfidence ?? 0;
|
|
62
|
+
const params = [];
|
|
63
|
+
const conditions = [
|
|
64
|
+
'f.superseded_by IS NULL',
|
|
65
|
+
'f.decay_score < 0.8',
|
|
66
|
+
`t.confidence >= ${minConf}`,
|
|
67
|
+
];
|
|
68
|
+
if (opts.agentId) {
|
|
69
|
+
conditions.push('t.agent_id = ?');
|
|
70
|
+
params.push(opts.agentId);
|
|
71
|
+
}
|
|
72
|
+
if (opts.domain) {
|
|
73
|
+
conditions.push('f.domain = ?');
|
|
74
|
+
params.push(opts.domain);
|
|
75
|
+
}
|
|
76
|
+
if (opts.fromMs !== undefined) {
|
|
77
|
+
conditions.push('t.occurred_at >= ?');
|
|
78
|
+
params.push(opts.fromMs);
|
|
79
|
+
}
|
|
80
|
+
if (opts.toMs !== undefined) {
|
|
81
|
+
conditions.push('t.occurred_at <= ?');
|
|
82
|
+
params.push(opts.toMs);
|
|
83
|
+
}
|
|
84
|
+
const where = conditions.length > 0
|
|
85
|
+
? `WHERE ${conditions.join(' AND ')}`
|
|
86
|
+
: '';
|
|
87
|
+
const sql = `
|
|
88
|
+
SELECT
|
|
89
|
+
t.fact_id,
|
|
90
|
+
t.agent_id,
|
|
91
|
+
f.content,
|
|
92
|
+
f.domain,
|
|
93
|
+
t.occurred_at,
|
|
94
|
+
t.ingest_at,
|
|
95
|
+
t.time_ref,
|
|
96
|
+
t.confidence
|
|
97
|
+
FROM temporal_index t
|
|
98
|
+
JOIN facts f ON f.id = t.fact_id
|
|
99
|
+
${where}
|
|
100
|
+
ORDER BY t.occurred_at ${order}
|
|
101
|
+
LIMIT ?
|
|
102
|
+
`;
|
|
103
|
+
params.push(limit);
|
|
104
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
105
|
+
return rows.map(r => ({
|
|
106
|
+
factId: r.fact_id,
|
|
107
|
+
agentId: r.agent_id,
|
|
108
|
+
content: r.content,
|
|
109
|
+
domain: r.domain || null,
|
|
110
|
+
occurredAt: r.occurred_at,
|
|
111
|
+
ingestAt: r.ingest_at,
|
|
112
|
+
timeRef: r.time_ref || null,
|
|
113
|
+
confidence: r.confidence,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get the most recent N facts for an agent (no time bounds).
|
|
118
|
+
* Useful for "what was the last thing about X" style queries.
|
|
119
|
+
*/
|
|
120
|
+
mostRecent(agentId, limit = 10) {
|
|
121
|
+
return this.timeRangeQuery({ agentId, limit, order: 'DESC' });
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get fact count in the temporal index for an agent.
|
|
125
|
+
*/
|
|
126
|
+
getIndexedCount(agentId) {
|
|
127
|
+
const row = this.db.prepare('SELECT COUNT(*) AS count FROM temporal_index WHERE agent_id = ?').get(agentId);
|
|
128
|
+
return row.count;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Backfill any facts not yet in the temporal index.
|
|
132
|
+
* Safe to run multiple times. Uses INSERT OR IGNORE.
|
|
133
|
+
*/
|
|
134
|
+
backfill() {
|
|
135
|
+
const result = this.db.prepare(`
|
|
136
|
+
INSERT OR IGNORE INTO temporal_index (fact_id, agent_id, occurred_at, ingest_at, confidence)
|
|
137
|
+
SELECT
|
|
138
|
+
id,
|
|
139
|
+
agent_id,
|
|
140
|
+
CAST((julianday(created_at) - 2440587.5) * 86400000 AS INTEGER),
|
|
141
|
+
CAST((julianday(created_at) - 2440587.5) * 86400000 AS INTEGER),
|
|
142
|
+
0.5
|
|
143
|
+
FROM facts
|
|
144
|
+
WHERE superseded_by IS NULL
|
|
145
|
+
`).run();
|
|
146
|
+
return result.changes;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=temporal-store.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topic Detector (P3.1)
|
|
3
|
+
*
|
|
4
|
+
* Heuristic-based topic shift detection. No model calls.
|
|
5
|
+
* Uses explicit markers, entity shift, conversation gap, and continuation signals.
|
|
6
|
+
*/
|
|
7
|
+
import type { NeutralMessage } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Strip inbound protocol metadata from message text before topic detection.
|
|
10
|
+
*
|
|
11
|
+
* Removes:
|
|
12
|
+
* - "Sender (untrusted metadata):" header blocks through next blank line
|
|
13
|
+
* - Pure timestamp lines: ISO 8601, bracket format, System YYYY-MM-DD
|
|
14
|
+
* - JSON code blocks containing "\"schema\": \"openclaw" or "inbound_meta"
|
|
15
|
+
* - Any ```json...``` block whose content contains those markers
|
|
16
|
+
*
|
|
17
|
+
* Called as the FIRST step in detectTopicShift() so that pattern matching
|
|
18
|
+
* operates on clean user content, not on injected transport headers.
|
|
19
|
+
*/
|
|
20
|
+
export declare function stripMessageMetadata(text: string): string;
|
|
21
|
+
export interface TopicSignal {
|
|
22
|
+
topicId: string | null;
|
|
23
|
+
isNewTopic: boolean;
|
|
24
|
+
confidence: number;
|
|
25
|
+
topicName: string | null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Detect whether the incoming message starts a new conversation topic.
|
|
29
|
+
*
|
|
30
|
+
* @param incomingMessage The new user message to evaluate
|
|
31
|
+
* @param recentMessages Last N messages for context (any role)
|
|
32
|
+
* @param currentTopicId The active topic id, or null if none
|
|
33
|
+
*/
|
|
34
|
+
export declare function detectTopicShift(incomingMessage: NeutralMessage, recentMessages: NeutralMessage[], currentTopicId: string | null): TopicSignal;
|
|
35
|
+
//# sourceMappingURL=topic-detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topic-detector.d.ts","sourceRoot":"","sources":["../src/topic-detector.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIjD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA6EzD;AAID,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AA6GD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,eAAe,EAAE,cAAc,EAC/B,cAAc,EAAE,cAAc,EAAE,EAChC,cAAc,EAAE,MAAM,GAAG,IAAI,GAC5B,WAAW,CAkEb"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topic Detector (P3.1)
|
|
3
|
+
*
|
|
4
|
+
* Heuristic-based topic shift detection. No model calls.
|
|
5
|
+
* Uses explicit markers, entity shift, conversation gap, and continuation signals.
|
|
6
|
+
*/
|
|
7
|
+
// ─── Metadata Stripping ───────────────────────────────────────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Strip inbound protocol metadata from message text before topic detection.
|
|
10
|
+
*
|
|
11
|
+
* Removes:
|
|
12
|
+
* - "Sender (untrusted metadata):" header blocks through next blank line
|
|
13
|
+
* - Pure timestamp lines: ISO 8601, bracket format, System YYYY-MM-DD
|
|
14
|
+
* - JSON code blocks containing "\"schema\": \"openclaw" or "inbound_meta"
|
|
15
|
+
* - Any ```json...``` block whose content contains those markers
|
|
16
|
+
*
|
|
17
|
+
* Called as the FIRST step in detectTopicShift() so that pattern matching
|
|
18
|
+
* operates on clean user content, not on injected transport headers.
|
|
19
|
+
*/
|
|
20
|
+
export function stripMessageMetadata(text) {
|
|
21
|
+
if (!text)
|
|
22
|
+
return text;
|
|
23
|
+
const lines = text.split('\n');
|
|
24
|
+
const result = [];
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (i < lines.length) {
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
// ── ```json blocks containing openclaw markers ──────────────────────────
|
|
29
|
+
if (/^```json\s*$/i.test(line)) {
|
|
30
|
+
// Peek ahead to collect the block
|
|
31
|
+
const blockLines = [line];
|
|
32
|
+
let j = i + 1;
|
|
33
|
+
let closed = false;
|
|
34
|
+
while (j < lines.length) {
|
|
35
|
+
blockLines.push(lines[j]);
|
|
36
|
+
if (lines[j].trim() === '```') {
|
|
37
|
+
closed = true;
|
|
38
|
+
j++;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
j++;
|
|
42
|
+
}
|
|
43
|
+
const blockContent = blockLines.join('\n');
|
|
44
|
+
if (blockContent.includes('"schema": "openclaw') ||
|
|
45
|
+
blockContent.includes('inbound_meta')) {
|
|
46
|
+
// Skip the whole block
|
|
47
|
+
i = j;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Not an openclaw block — keep as-is
|
|
51
|
+
result.push(...blockLines.slice(0, closed ? blockLines.length : blockLines.length));
|
|
52
|
+
i = j;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// ── Sender (untrusted metadata): block ──────────────────────────────────
|
|
56
|
+
if (/^Sender \(untrusted metadata\):/i.test(line)) {
|
|
57
|
+
// Skip through next blank line or closing ```
|
|
58
|
+
i++;
|
|
59
|
+
while (i < lines.length) {
|
|
60
|
+
const l = lines[i];
|
|
61
|
+
if (l.trim() === '' || l.trim() === '```') {
|
|
62
|
+
i++; // consume the blank/closing line too
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// ── Pure timestamp lines ─────────────────────────────────────────────────
|
|
70
|
+
// ISO 8601: 2026-04-05T02:43:00Z / 2026-04-05T02:43:00.000Z
|
|
71
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/.test(line.trim())) {
|
|
72
|
+
i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// Bracket format: [Mon 2026-04-05 02:43 MST]
|
|
76
|
+
if (/^\[\w{3}\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+\w+\]$/.test(line.trim())) {
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// System YYYY-MM-DD
|
|
81
|
+
if (/^System\s+\d{4}-\d{2}-\d{2}$/.test(line.trim())) {
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
result.push(line);
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
return result.join('\n').trim();
|
|
89
|
+
}
|
|
90
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
91
|
+
const GAP_MS = 30 * 60 * 1000; // 30 minutes
|
|
92
|
+
// Explicit topic-shift markers
|
|
93
|
+
const SHIFT_PATTERNS = [
|
|
94
|
+
/^(?:let'?s?|can we)\s+(?:talk|discuss|chat)\s+about\s+(.+)/i,
|
|
95
|
+
/^switching\s+to\s+(.+)/i,
|
|
96
|
+
/^new\s+topic\s*[:—-]\s*(.+)/i,
|
|
97
|
+
/^regarding\s+(.+)/i,
|
|
98
|
+
/^on\s+(?:the\s+)?(?:topic\s+of\s+)?(.+)/i,
|
|
99
|
+
/^(?:i\s+want\s+to\s+)?(?:talk|ask)\s+about\s+(.+)/i,
|
|
100
|
+
/^(?:change\s+of\s+)?subject\s*[:—-]\s*(.+)/i,
|
|
101
|
+
];
|
|
102
|
+
// Continuation markers — if any match, this is almost certainly same topic
|
|
103
|
+
const CONTINUATION_PATTERNS = [
|
|
104
|
+
/^\s*(?:also|and|additionally|furthermore|moreover|plus)\b/i,
|
|
105
|
+
/^\s*(?:what\s+about|how\s+about)\s+/i,
|
|
106
|
+
/^\s*(?:following\s+up|follow[- ]up|just\s+to\s+follow\s+up)\b/i,
|
|
107
|
+
/^\s*(?:actually|wait|oh)\s*[,—-]/i,
|
|
108
|
+
/^\s*(?:one\s+more\s+thing|another\s+thing|by\s+the\s+way)\b/i,
|
|
109
|
+
];
|
|
110
|
+
// Stop words for noun-phrase extraction
|
|
111
|
+
const STOP_WORDS = new Set([
|
|
112
|
+
'a', 'an', 'the', 'this', 'that', 'these', 'those', 'i', 'you', 'we', 'they',
|
|
113
|
+
'it', 'he', 'she', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
114
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should',
|
|
115
|
+
'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'on', 'at', 'for', 'with',
|
|
116
|
+
'by', 'from', 'up', 'about', 'into', 'through', 'and', 'or', 'but', 'not',
|
|
117
|
+
'so', 'yet', 'both', 'either', 'neither', 'nor', 'as', 'just', 'than',
|
|
118
|
+
'then', 'because', 'while', 'although', 'if', 'when', 'where', 'how', 'what',
|
|
119
|
+
'which', 'who', 'whom', 'whose',
|
|
120
|
+
]);
|
|
121
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Extract a short heuristic topic name from message text (max 40 chars).
|
|
124
|
+
* Returns the first meaningful noun phrase or the first substantive clause.
|
|
125
|
+
*/
|
|
126
|
+
function extractTopicName(text) {
|
|
127
|
+
// First, check if any shift pattern captured a phrase
|
|
128
|
+
for (const pattern of SHIFT_PATTERNS) {
|
|
129
|
+
const m = text.match(pattern);
|
|
130
|
+
if (m?.[1]) {
|
|
131
|
+
const phrase = m[1].replace(/[?!.]+$/, '').trim();
|
|
132
|
+
return phrase.slice(0, 40);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Fall back: split into words, skip stop words, take first 5 content words
|
|
136
|
+
const words = text
|
|
137
|
+
.replace(/[^\w\s'-]/g, ' ')
|
|
138
|
+
.split(/\s+/)
|
|
139
|
+
.filter(w => w.length > 1 && !STOP_WORDS.has(w.toLowerCase()));
|
|
140
|
+
if (words.length === 0) {
|
|
141
|
+
// Last resort: first 40 chars
|
|
142
|
+
return text.slice(0, 40).trim();
|
|
143
|
+
}
|
|
144
|
+
return words.slice(0, 5).join(' ').slice(0, 40);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Naive named-entity heuristic: extract capitalized words (length ≥ 3) and
|
|
148
|
+
* quoted strings as "entities". Returns a Set<string> (lowercased).
|
|
149
|
+
*/
|
|
150
|
+
function extractEntities(text) {
|
|
151
|
+
const entities = new Set();
|
|
152
|
+
// Quoted strings
|
|
153
|
+
const quoted = text.match(/["']([^"']{2,30})["']/g);
|
|
154
|
+
if (quoted) {
|
|
155
|
+
for (const q of quoted)
|
|
156
|
+
entities.add(q.replace(/["']/g, '').toLowerCase());
|
|
157
|
+
}
|
|
158
|
+
// Capitalized words (simple NER proxy — excludes sentence-start)
|
|
159
|
+
const words = text.split(/\s+/);
|
|
160
|
+
for (let i = 1; i < words.length; i++) {
|
|
161
|
+
const w = words[i].replace(/[^a-zA-Z-]/g, '');
|
|
162
|
+
if (w.length >= 3 && /^[A-Z]/.test(w)) {
|
|
163
|
+
entities.add(w.toLowerCase());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return entities;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Jaccard similarity between two entity sets (0 = no overlap, 1 = identical).
|
|
170
|
+
*/
|
|
171
|
+
function jaccardSimilarity(a, b) {
|
|
172
|
+
if (a.size === 0 && b.size === 0)
|
|
173
|
+
return 1;
|
|
174
|
+
if (a.size === 0 || b.size === 0)
|
|
175
|
+
return 0;
|
|
176
|
+
let intersect = 0;
|
|
177
|
+
for (const x of a) {
|
|
178
|
+
if (b.has(x))
|
|
179
|
+
intersect++;
|
|
180
|
+
}
|
|
181
|
+
const union = a.size + b.size - intersect;
|
|
182
|
+
return intersect / union;
|
|
183
|
+
}
|
|
184
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
185
|
+
/**
|
|
186
|
+
* Detect whether the incoming message starts a new conversation topic.
|
|
187
|
+
*
|
|
188
|
+
* @param incomingMessage The new user message to evaluate
|
|
189
|
+
* @param recentMessages Last N messages for context (any role)
|
|
190
|
+
* @param currentTopicId The active topic id, or null if none
|
|
191
|
+
*/
|
|
192
|
+
export function detectTopicShift(incomingMessage, recentMessages, currentTopicId) {
|
|
193
|
+
// Strip inbound protocol metadata FIRST — pattern matching must operate on
|
|
194
|
+
// clean user content, not on transport headers (timestamps, sender blocks, etc.)
|
|
195
|
+
const text = stripMessageMetadata(incomingMessage.textContent ?? '');
|
|
196
|
+
// Guard: if the message is entirely metadata with no real content, never
|
|
197
|
+
// create a new topic — there is nothing to name or classify.
|
|
198
|
+
if (!text) {
|
|
199
|
+
return { topicId: currentTopicId, isNewTopic: false, confidence: 0.9, topicName: null };
|
|
200
|
+
}
|
|
201
|
+
// ── 1. Continuation check (highest priority) ──────────────────────────────
|
|
202
|
+
for (const pat of CONTINUATION_PATTERNS) {
|
|
203
|
+
if (pat.test(text)) {
|
|
204
|
+
return { topicId: currentTopicId, isNewTopic: false, confidence: 0.85, topicName: null };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ── 2. Explicit shift marker ───────────────────────────────────────────────
|
|
208
|
+
for (const pat of SHIFT_PATTERNS) {
|
|
209
|
+
if (pat.test(text)) {
|
|
210
|
+
const name = extractTopicName(text);
|
|
211
|
+
return { topicId: null, isNewTopic: true, confidence: 0.95, topicName: name };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ── 3. Conversation gap (empty history or >30 min since last message) ──────
|
|
215
|
+
if (recentMessages.length === 0) {
|
|
216
|
+
const name = extractTopicName(text);
|
|
217
|
+
return { topicId: null, isNewTopic: true, confidence: 0.8, topicName: name };
|
|
218
|
+
}
|
|
219
|
+
// Find last message with a timestamp in metadata
|
|
220
|
+
const lastMsg = recentMessages[recentMessages.length - 1];
|
|
221
|
+
const lastTs = lastMsg.metadata?.createdAt
|
|
222
|
+
? new Date(lastMsg.metadata.createdAt).getTime()
|
|
223
|
+
: null;
|
|
224
|
+
if (lastTs !== null && !isNaN(lastTs)) {
|
|
225
|
+
const now = Date.now();
|
|
226
|
+
if (now - lastTs > GAP_MS) {
|
|
227
|
+
const name = extractTopicName(text);
|
|
228
|
+
return { topicId: null, isNewTopic: true, confidence: 0.75, topicName: name };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// ── 4. Entity shift (compare incoming to last 3 messages) ─────────────────
|
|
232
|
+
const incomingEntities = extractEntities(text);
|
|
233
|
+
const contextWindow = recentMessages.slice(-3);
|
|
234
|
+
const contextText = contextWindow
|
|
235
|
+
.map(m => m.textContent ?? '')
|
|
236
|
+
.join(' ');
|
|
237
|
+
if (incomingEntities.size > 0 && contextText.length > 0) {
|
|
238
|
+
const contextEntities = extractEntities(contextText);
|
|
239
|
+
const similarity = jaccardSimilarity(incomingEntities, contextEntities);
|
|
240
|
+
// Low similarity + no continuation marker → likely a new topic
|
|
241
|
+
if (similarity < 0.1) {
|
|
242
|
+
const name = extractTopicName(text);
|
|
243
|
+
return { topicId: null, isNewTopic: true, confidence: 0.6, topicName: name };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// ── 5. Default: continue current topic ────────────────────────────────────
|
|
247
|
+
return { topicId: currentTopicId, isNewTopic: false, confidence: 0.7, topicName: null };
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=topic-detector.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Topic Store
|
|
3
|
+
*
|
|
4
|
+
* Cross-session topic tracking. Topics are conversation threads that
|
|
5
|
+
* can span multiple sessions and channels.
|
|
6
|
+
* Lives in the central library DB.
|
|
7
|
+
*/
|
|
8
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
9
|
+
import type { Topic, TopicStatus } from './types.js';
|
|
10
|
+
export declare class TopicStore {
|
|
11
|
+
private readonly db;
|
|
12
|
+
constructor(db: DatabaseSync);
|
|
13
|
+
/**
|
|
14
|
+
* Create a new topic.
|
|
15
|
+
*/
|
|
16
|
+
create(agentId: string, name: string, description?: string, visibility?: string): Topic;
|
|
17
|
+
/**
|
|
18
|
+
* Touch a topic — update activity tracking.
|
|
19
|
+
*/
|
|
20
|
+
touch(topicId: number, sessionKey: string, messagesDelta?: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* Get active topics for an agent.
|
|
23
|
+
*/
|
|
24
|
+
getActive(agentId: string, limit?: number): Topic[];
|
|
25
|
+
/**
|
|
26
|
+
* Get all topics for an agent.
|
|
27
|
+
*/
|
|
28
|
+
getAll(agentId: string, opts?: {
|
|
29
|
+
status?: TopicStatus;
|
|
30
|
+
limit?: number;
|
|
31
|
+
}): Topic[];
|
|
32
|
+
/**
|
|
33
|
+
* Find topics matching a query.
|
|
34
|
+
*/
|
|
35
|
+
search(agentId: string, query: string, limit?: number): Topic[];
|
|
36
|
+
/**
|
|
37
|
+
* Mark dormant topics (no activity for dormantAfterHours).
|
|
38
|
+
*/
|
|
39
|
+
markDormant(agentId: string, dormantAfterHours?: number): number;
|
|
40
|
+
/**
|
|
41
|
+
* Close dormant topics.
|
|
42
|
+
*/
|
|
43
|
+
closeDormant(agentId: string, closedAfterDays?: number): number;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=topic-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topic-store.d.ts","sourceRoot":"","sources":["../src/topic-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAqBrD,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,YAAY;IAE7C;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK;IAwBvF;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,GAAE,MAAU,GAAG,IAAI;IAY3E;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,KAAK,EAAE;IAWvD;;OAEG;IACH,MAAM,CACJ,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC9C,KAAK,EAAE;IAoBV;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,KAAK,EAAE;IAanE;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,iBAAiB,GAAE,MAAW,GAAG,MAAM;IAYpE;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,GAAE,MAAU,GAAG,MAAM;CAWnE"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hypermem Topic Store
|
|
3
|
+
*
|
|
4
|
+
* Cross-session topic tracking. Topics are conversation threads that
|
|
5
|
+
* can span multiple sessions and channels.
|
|
6
|
+
* Lives in the central library DB.
|
|
7
|
+
*/
|
|
8
|
+
function nowIso() {
|
|
9
|
+
return new Date().toISOString();
|
|
10
|
+
}
|
|
11
|
+
function parseTopicRow(row) {
|
|
12
|
+
return {
|
|
13
|
+
id: row.id,
|
|
14
|
+
agentId: row.agent_id,
|
|
15
|
+
name: row.name,
|
|
16
|
+
description: row.description || null,
|
|
17
|
+
status: row.status,
|
|
18
|
+
visibility: row.visibility || 'org',
|
|
19
|
+
lastSessionKey: row.last_session_key || null,
|
|
20
|
+
messageCount: row.message_count,
|
|
21
|
+
createdAt: row.created_at,
|
|
22
|
+
updatedAt: row.updated_at,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export class TopicStore {
|
|
26
|
+
db;
|
|
27
|
+
constructor(db) {
|
|
28
|
+
this.db = db;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a new topic.
|
|
32
|
+
*/
|
|
33
|
+
create(agentId, name, description, visibility) {
|
|
34
|
+
const now = nowIso();
|
|
35
|
+
const result = this.db.prepare(`
|
|
36
|
+
INSERT INTO topics (agent_id, name, description, status, visibility, message_count, created_at, updated_at)
|
|
37
|
+
VALUES (?, ?, ?, 'active', ?, 0, ?, ?)
|
|
38
|
+
`).run(agentId, name, description || null, visibility || 'org', now, now);
|
|
39
|
+
const id = Number(result.lastInsertRowid);
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
agentId,
|
|
43
|
+
name,
|
|
44
|
+
description: description || null,
|
|
45
|
+
status: 'active',
|
|
46
|
+
visibility: visibility || 'org',
|
|
47
|
+
lastSessionKey: null,
|
|
48
|
+
messageCount: 0,
|
|
49
|
+
createdAt: now,
|
|
50
|
+
updatedAt: now,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Touch a topic — update activity tracking.
|
|
55
|
+
*/
|
|
56
|
+
touch(topicId, sessionKey, messagesDelta = 1) {
|
|
57
|
+
const now = nowIso();
|
|
58
|
+
this.db.prepare(`
|
|
59
|
+
UPDATE topics
|
|
60
|
+
SET last_session_key = ?,
|
|
61
|
+
message_count = message_count + ?,
|
|
62
|
+
status = 'active',
|
|
63
|
+
updated_at = ?
|
|
64
|
+
WHERE id = ?
|
|
65
|
+
`).run(sessionKey, messagesDelta, now, topicId);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get active topics for an agent.
|
|
69
|
+
*/
|
|
70
|
+
getActive(agentId, limit = 20) {
|
|
71
|
+
const rows = this.db.prepare(`
|
|
72
|
+
SELECT * FROM topics
|
|
73
|
+
WHERE agent_id = ? AND status = 'active'
|
|
74
|
+
ORDER BY updated_at DESC
|
|
75
|
+
LIMIT ?
|
|
76
|
+
`).all(agentId, limit);
|
|
77
|
+
return rows.map(parseTopicRow);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get all topics for an agent.
|
|
81
|
+
*/
|
|
82
|
+
getAll(agentId, opts) {
|
|
83
|
+
let sql = 'SELECT * FROM topics WHERE agent_id = ?';
|
|
84
|
+
const params = [agentId];
|
|
85
|
+
if (opts?.status) {
|
|
86
|
+
sql += ' AND status = ?';
|
|
87
|
+
params.push(opts.status);
|
|
88
|
+
}
|
|
89
|
+
sql += ' ORDER BY updated_at DESC';
|
|
90
|
+
if (opts?.limit) {
|
|
91
|
+
sql += ' LIMIT ?';
|
|
92
|
+
params.push(opts.limit);
|
|
93
|
+
}
|
|
94
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
95
|
+
return rows.map(parseTopicRow);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Find topics matching a query.
|
|
99
|
+
*/
|
|
100
|
+
search(agentId, query, limit = 10) {
|
|
101
|
+
const rows = this.db.prepare(`
|
|
102
|
+
SELECT * FROM topics
|
|
103
|
+
WHERE agent_id = ?
|
|
104
|
+
AND (name LIKE ? OR description LIKE ?)
|
|
105
|
+
AND status != 'closed'
|
|
106
|
+
ORDER BY updated_at DESC
|
|
107
|
+
LIMIT ?
|
|
108
|
+
`).all(agentId, `%${query}%`, `%${query}%`, limit);
|
|
109
|
+
return rows.map(parseTopicRow);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Mark dormant topics (no activity for dormantAfterHours).
|
|
113
|
+
*/
|
|
114
|
+
markDormant(agentId, dormantAfterHours = 24) {
|
|
115
|
+
const cutoff = new Date(Date.now() - dormantAfterHours * 60 * 60 * 1000).toISOString();
|
|
116
|
+
const result = this.db.prepare(`
|
|
117
|
+
UPDATE topics
|
|
118
|
+
SET status = 'dormant', updated_at = ?
|
|
119
|
+
WHERE agent_id = ? AND status = 'active' AND updated_at < ?
|
|
120
|
+
`).run(nowIso(), agentId, cutoff);
|
|
121
|
+
return result.changes;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Close dormant topics.
|
|
125
|
+
*/
|
|
126
|
+
closeDormant(agentId, closedAfterDays = 7) {
|
|
127
|
+
const cutoff = new Date(Date.now() - closedAfterDays * 24 * 60 * 60 * 1000).toISOString();
|
|
128
|
+
const result = this.db.prepare(`
|
|
129
|
+
UPDATE topics
|
|
130
|
+
SET status = 'closed', updated_at = ?
|
|
131
|
+
WHERE agent_id = ? AND status = 'dormant' AND updated_at < ?
|
|
132
|
+
`).run(nowIso(), agentId, cutoff);
|
|
133
|
+
return result.changes;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=topic-store.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topic Synthesizer
|
|
3
|
+
*
|
|
4
|
+
* Synthesizes compiled knowledge pages (wiki-style) from stale topics.
|
|
5
|
+
* Heuristic-only: no LLM calls. Uses content-type classifier + keystone scoring.
|
|
6
|
+
*
|
|
7
|
+
* Architecture: Karpathy LLM Wiki Pattern adapted for hypermem.
|
|
8
|
+
* Raw sources (messages.db) → Wiki (knowledge table) → Compositor (compose-time)
|
|
9
|
+
*/
|
|
10
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
11
|
+
declare const SYNTHESIS_STALE_MINUTES = 30;
|
|
12
|
+
declare const SYNTHESIS_MIN_MESSAGES = 5;
|
|
13
|
+
declare const SYNTHESIS_REGROWTH_THRESHOLD = 5;
|
|
14
|
+
declare const SYNTHESIS_MAX_SUMMARY_CHARS = 800;
|
|
15
|
+
declare const SYNTHESIS_MAX_DECISIONS = 10;
|
|
16
|
+
declare const SYNTHESIS_MAX_QUESTIONS = 5;
|
|
17
|
+
declare const LINT_FREQUENCY = 10;
|
|
18
|
+
declare const LINT_STALE_DAYS = 7;
|
|
19
|
+
export { SYNTHESIS_STALE_MINUTES, SYNTHESIS_MIN_MESSAGES, SYNTHESIS_REGROWTH_THRESHOLD, SYNTHESIS_MAX_SUMMARY_CHARS, SYNTHESIS_MAX_DECISIONS, SYNTHESIS_MAX_QUESTIONS, LINT_FREQUENCY, LINT_STALE_DAYS, };
|
|
20
|
+
export interface SynthesisConfig {
|
|
21
|
+
SYNTHESIS_STALE_MINUTES: number;
|
|
22
|
+
SYNTHESIS_MIN_MESSAGES: number;
|
|
23
|
+
SYNTHESIS_REGROWTH_THRESHOLD: number;
|
|
24
|
+
SYNTHESIS_MAX_SUMMARY_CHARS: number;
|
|
25
|
+
SYNTHESIS_MAX_DECISIONS: number;
|
|
26
|
+
SYNTHESIS_MAX_QUESTIONS: number;
|
|
27
|
+
LINT_FREQUENCY: number;
|
|
28
|
+
LINT_STALE_DAYS: number;
|
|
29
|
+
}
|
|
30
|
+
export interface SynthesisResult {
|
|
31
|
+
topicsSynthesized: number;
|
|
32
|
+
topicsSkipped: number;
|
|
33
|
+
knowledgeEntriesWritten: number;
|
|
34
|
+
}
|
|
35
|
+
export declare class TopicSynthesizer {
|
|
36
|
+
private readonly libraryDb;
|
|
37
|
+
private readonly getMessageDb;
|
|
38
|
+
private readonly config?;
|
|
39
|
+
private readonly effectiveConfig;
|
|
40
|
+
constructor(libraryDb: DatabaseSync, getMessageDb: (agentId: string) => DatabaseSync | null, config?: Partial<SynthesisConfig> | undefined);
|
|
41
|
+
/**
|
|
42
|
+
* Run one synthesis pass for an agent.
|
|
43
|
+
* Finds stale topics, synthesizes wiki pages, writes to knowledge table.
|
|
44
|
+
*/
|
|
45
|
+
tick(agentId: string): SynthesisResult;
|
|
46
|
+
/**
|
|
47
|
+
* Synthesize a wiki page for a topic from its messages.
|
|
48
|
+
*/
|
|
49
|
+
private synthesizeTopic;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=topic-synthesizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topic-synthesizer.d.ts","sourceRoot":"","sources":["../src/topic-synthesizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,QAAA,MAAM,uBAAuB,KAAK,CAAC;AACnC,QAAA,MAAM,sBAAsB,IAAI,CAAC;AACjC,QAAA,MAAM,4BAA4B,IAAI,CAAC;AACvC,QAAA,MAAM,2BAA2B,MAAM,CAAC;AACxC,QAAA,MAAM,uBAAuB,KAAK,CAAC;AACnC,QAAA,MAAM,uBAAuB,IAAI,CAAC;AAClC,QAAA,MAAM,cAAc,KAAK,CAAC;AAC1B,QAAA,MAAM,eAAe,IAAI,CAAC;AAG1B,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,4BAA4B,EAC5B,2BAA2B,EAC3B,uBAAuB,EACvB,uBAAuB,EACvB,cAAc,EACd,eAAe,GAChB,CAAC;AAIF,MAAM,WAAW,eAAe;IAC9B,uBAAuB,EAAE,MAAM,CAAC;IAChC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,4BAA4B,EAAE,MAAM,CAAC;IACrC,2BAA2B,EAAE,MAAM,CAAC;IACpC,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,uBAAuB,EAAE,MAAM,CAAC;CACjC;AA6HD,qBAAa,gBAAgB;IAIzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IAL1B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;gBAG/B,SAAS,EAAE,YAAY,EACvB,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,YAAY,GAAG,IAAI,EACtD,MAAM,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,YAAA;IAcpD;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe;IAkFtC;;OAEG;IACH,OAAO,CAAC,eAAe;CA+GxB"}
|