opcode-pg-memory 2.2.8 → 2.3.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/cli.js +232 -214
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -21006
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.js +319 -302
- package/dist/mcp-server.js.map +1 -0
- package/dist/src/cache/semantic-cache.js +399 -0
- package/dist/src/cache/semantic-cache.js.map +1 -0
- package/dist/src/cli.js +404 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +89 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/db/init-db.js +545 -0
- package/dist/src/db/init-db.js.map +1 -0
- package/dist/src/hooks/message-part-updated.js +203 -0
- package/dist/src/hooks/message-part-updated.js.map +1 -0
- package/dist/src/hooks/message-updated.js +347 -0
- package/dist/src/hooks/message-updated.js.map +1 -0
- package/dist/src/hooks/session-compacting.js +179 -0
- package/dist/src/hooks/session-compacting.js.map +1 -0
- package/dist/src/hooks/session-completed.js +337 -0
- package/dist/src/hooks/session-completed.js.map +1 -0
- package/dist/src/hooks/session-created.js +206 -0
- package/dist/src/hooks/session-created.js.map +1 -0
- package/dist/src/hooks/tool-execute.js +267 -0
- package/dist/src/hooks/tool-execute.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +642 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/hindsight-reflect-omo.js +318 -0
- package/dist/src/mcp/hindsight-reflect-omo.js.map +1 -0
- package/dist/src/mcp/hindsight-reflect.js +838 -0
- package/dist/src/mcp/hindsight-reflect.js.map +1 -0
- package/dist/src/mcp/recall-memory-omo.js +263 -0
- package/dist/src/mcp/recall-memory-omo.js.map +1 -0
- package/dist/src/mcp/recall-memory.d.ts +6 -0
- package/dist/src/mcp/recall-memory.d.ts.map +1 -1
- package/dist/src/mcp/recall-memory.js +900 -0
- package/dist/src/mcp/recall-memory.js.map +1 -0
- package/dist/src/omo/adapter.js +583 -0
- package/dist/src/omo/adapter.js.map +1 -0
- package/dist/src/omo/types.js +44 -0
- package/dist/src/omo/types.js.map +1 -0
- package/dist/src/services/db-polling.d.ts +33 -0
- package/dist/src/services/db-polling.d.ts.map +1 -0
- package/dist/src/services/db-polling.js +104 -0
- package/dist/src/services/db-polling.js.map +1 -0
- package/dist/src/services/event-synchronizer.d.ts +15 -0
- package/dist/src/services/event-synchronizer.d.ts.map +1 -0
- package/dist/src/services/event-synchronizer.js +119 -0
- package/dist/src/services/event-synchronizer.js.map +1 -0
- package/dist/src/services/keyword.js +29 -0
- package/dist/src/services/keyword.js.map +1 -0
- package/dist/src/services/logger.js +42 -0
- package/dist/src/services/logger.js.map +1 -0
- package/dist/src/services/opencode-schema-adapter.d.ts +100 -0
- package/dist/src/services/opencode-schema-adapter.d.ts.map +1 -0
- package/dist/src/services/opencode-schema-adapter.js +192 -0
- package/dist/src/services/opencode-schema-adapter.js.map +1 -0
- package/dist/src/services/privacy.js +23 -0
- package/dist/src/services/privacy.js.map +1 -0
- package/dist/src/topic/segment-manager.js +447 -0
- package/dist/src/topic/segment-manager.js.map +1 -0
- package/dist/src/types.d.ts +20 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +8 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/embedding.js +180 -0
- package/dist/src/utils/embedding.js.map +1 -0
- package/dist/src/utils/token-budget.js +152 -0
- package/dist/src/utils/token-budget.js.map +1 -0
- package/package.json +5 -4
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenCodeSchemaAdapter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* OpenCode SQLite 数据模型适配器
|
|
6
|
+
*
|
|
7
|
+
* 基于真实 OpenCode 数据库结构编写。
|
|
8
|
+
* 关键结构(经现场确认,非假设):
|
|
9
|
+
* - session: 扁平列 (id, title, time_created, agent, model)
|
|
10
|
+
* - message: data TEXT (JSON) → role, tokens, modelID, agent, time
|
|
11
|
+
* - part: data TEXT (JSON) → type(text/tool), tool, callID, state, text
|
|
12
|
+
* - event: data TEXT (JSON), type, aggregate_id, seq
|
|
13
|
+
* - session_message: 桥接表
|
|
14
|
+
*/
|
|
15
|
+
const logger_1 = require("./logger");
|
|
16
|
+
const path_1 = require("path");
|
|
17
|
+
const os_1 = require("os");
|
|
18
|
+
const logger = (0, logger_1.createLogger)('schema-adapter');
|
|
19
|
+
class OpenCodeSchemaAdapter {
|
|
20
|
+
db = null;
|
|
21
|
+
path;
|
|
22
|
+
drizzleVersion = 0;
|
|
23
|
+
constructor(customPath) {
|
|
24
|
+
this.path = customPath || (0, path_1.join)((0, os_1.homedir)(), '.local', 'share', 'opencode', 'opencode.db');
|
|
25
|
+
}
|
|
26
|
+
connect() {
|
|
27
|
+
if (this.db)
|
|
28
|
+
return true;
|
|
29
|
+
try {
|
|
30
|
+
const { Database } = require('bun:sqlite');
|
|
31
|
+
this.db = new Database(this.path);
|
|
32
|
+
this.detectDrizzleVersion();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
logger.warn('Failed to connect to OpenCode SQLite', { path: this.path, error: err.message });
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
detectDrizzleVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const rows = this.db?.query('SELECT count(*) as cnt FROM __drizzle_migrations').all() || [];
|
|
43
|
+
this.drizzleVersion = rows.length > 0 ? rows[0].cnt || 0 : 0;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
this.drizzleVersion = 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
isConnected() { return this.db !== null; }
|
|
50
|
+
getDrizzleVersion() { return this.drizzleVersion; }
|
|
51
|
+
// ── 会话 ─────────────────────────────────────────
|
|
52
|
+
getRecentSessions() {
|
|
53
|
+
if (!this.db)
|
|
54
|
+
return [];
|
|
55
|
+
try {
|
|
56
|
+
return this.db.query(`
|
|
57
|
+
SELECT id, title, time_created, agent, model
|
|
58
|
+
FROM session ORDER BY time_created ASC
|
|
59
|
+
`).all();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logger.warn('Failed to query sessions', { error: err.message });
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
getSessionById(id) {
|
|
67
|
+
if (!this.db)
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
return this.db.query('SELECT id, title, time_created, agent FROM session WHERE id = ?').get(id) || null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ── 消息 (message.data JSON) ─────────────────────
|
|
77
|
+
/** 从 message.data JSON 中解析元数据 */
|
|
78
|
+
parseMessageMeta(raw) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(raw);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** 获取指定会话的全部消息(含 JSON 解析) */
|
|
87
|
+
getMessagesBySession(sessionId) {
|
|
88
|
+
if (!this.db)
|
|
89
|
+
return [];
|
|
90
|
+
try {
|
|
91
|
+
const rows = this.db.query(`
|
|
92
|
+
SELECT id, data FROM message WHERE session_id = ? ORDER BY time_created ASC
|
|
93
|
+
`).all(sessionId);
|
|
94
|
+
return rows.map(r => ({ id: r.id, meta: this.parseMessageMeta(r.data) }));
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
logger.warn('Failed to query messages', { sessionId, error: err.message });
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** 获取增量消息(time_created > sinceTime) */
|
|
102
|
+
getRecentMessages(sinceTime) {
|
|
103
|
+
if (!this.db)
|
|
104
|
+
return [];
|
|
105
|
+
try {
|
|
106
|
+
let sql = 'SELECT id, session_id, data, time_created FROM message';
|
|
107
|
+
const params = [];
|
|
108
|
+
if (sinceTime) {
|
|
109
|
+
sql += ' WHERE time_created > ?';
|
|
110
|
+
params.push(sinceTime);
|
|
111
|
+
}
|
|
112
|
+
sql += ' ORDER BY time_created ASC';
|
|
113
|
+
const rows = this.db.query(sql).all(...params);
|
|
114
|
+
return rows.map(r => ({ id: r.id, session_id: r.session_id, meta: this.parseMessageMeta(r.data) }));
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
logger.warn('Failed to query recent messages', { error: err.message });
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ── 消息部件 (part.data JSON) ────────────────────
|
|
122
|
+
/** 从 part.data JSON 中解析出部件信息 */
|
|
123
|
+
parsePart(raw) {
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(raw);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** 获取指定消息的全部部件 */
|
|
132
|
+
getPartsByMessage(messageId) {
|
|
133
|
+
if (!this.db)
|
|
134
|
+
return [];
|
|
135
|
+
try {
|
|
136
|
+
const rows = this.db.query('SELECT data FROM part WHERE message_id = ? ORDER BY time_created ASC').all(messageId);
|
|
137
|
+
return rows.map(r => this.parsePart(r.data)).filter(Boolean);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logger.warn('Failed to query parts', { messageId, error: err.message });
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** 获取指定消息中的工具调用部件 */
|
|
145
|
+
getToolCallsByMessage(messageId) {
|
|
146
|
+
return this.getPartsByMessage(messageId).filter(p => p.type === 'tool' && p.tool);
|
|
147
|
+
}
|
|
148
|
+
// ── 高级查询 ────────────────────────────────────
|
|
149
|
+
/** 获取指定会话的全部工具调用(两段式:message → part) */
|
|
150
|
+
getToolCallsBySession(sessionId) {
|
|
151
|
+
const results = [];
|
|
152
|
+
const msgs = this.db?.query('SELECT id FROM message WHERE session_id = ? ORDER BY time_created ASC').all(sessionId) || [];
|
|
153
|
+
for (const msg of msgs) {
|
|
154
|
+
const tools = this.getToolCallsByMessage(msg.id);
|
|
155
|
+
for (const t of tools) {
|
|
156
|
+
results.push({
|
|
157
|
+
messageId: msg.id,
|
|
158
|
+
tool: t.tool || 'unknown',
|
|
159
|
+
callID: t.callID,
|
|
160
|
+
input: t.state?.input,
|
|
161
|
+
output: t.state?.output,
|
|
162
|
+
status: t.state?.status,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
/** 检查数据库是否可访问 */
|
|
169
|
+
healthCheck() {
|
|
170
|
+
try {
|
|
171
|
+
if (!this.db)
|
|
172
|
+
return { ok: false, drizzleVersion: 0, sessionCount: 0, messageCount: 0 };
|
|
173
|
+
const sc = this.db.query('SELECT count(*) as c FROM session').get()?.c || 0;
|
|
174
|
+
const mc = this.db.query('SELECT count(*) as c FROM message').get()?.c || 0;
|
|
175
|
+
return { ok: true, drizzleVersion: this.drizzleVersion, sessionCount: sc, messageCount: mc };
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return { ok: false, drizzleVersion: 0, sessionCount: 0, messageCount: 0 };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
close() {
|
|
182
|
+
if (this.db) {
|
|
183
|
+
try {
|
|
184
|
+
this.db.close();
|
|
185
|
+
}
|
|
186
|
+
catch { }
|
|
187
|
+
this.db = null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.OpenCodeSchemaAdapter = OpenCodeSchemaAdapter;
|
|
192
|
+
//# sourceMappingURL=opencode-schema-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-schema-adapter.js","sourceRoot":"","sources":["../../../src/services/opencode-schema-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.containsPrivateTag = containsPrivateTag;
|
|
4
|
+
exports.stripPrivateContent = stripPrivateContent;
|
|
5
|
+
exports.isFullyPrivate = isFullyPrivate;
|
|
6
|
+
const PRIVATE_TAG_RE = /<private>[\s\S]*?<\/private>/gi;
|
|
7
|
+
function containsPrivateTag(content) {
|
|
8
|
+
if (!content)
|
|
9
|
+
return false;
|
|
10
|
+
return PRIVATE_TAG_RE.test(content);
|
|
11
|
+
}
|
|
12
|
+
function stripPrivateContent(content) {
|
|
13
|
+
if (!content)
|
|
14
|
+
return content;
|
|
15
|
+
return content.replace(PRIVATE_TAG_RE, '[REDACTED]');
|
|
16
|
+
}
|
|
17
|
+
function isFullyPrivate(content) {
|
|
18
|
+
if (!content)
|
|
19
|
+
return false;
|
|
20
|
+
const stripped = stripPrivateContent(content).trim();
|
|
21
|
+
return stripped === '[REDACTED]' || stripped === '';
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=privacy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privacy.js","sourceRoot":"","sources":["../../../src/services/privacy.ts"],"names":[],"mappings":";;AAEA,gDAGC;AAED,kDAGC;AAED,wCAIC;AAhBD,MAAM,cAAc,GAAG,gCAAgC,CAAC;AAExD,SAAgB,kBAAkB,CAAC,OAAe;IAChD,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,OAAO,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACtC,CAAC;AAED,SAAgB,mBAAmB,CAAC,OAAe;IACjD,IAAI,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC;IAC7B,OAAO,OAAO,CAAC,OAAO,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;AACvD,CAAC;AAED,SAAgB,cAAc,CAAC,OAAe;IAC5C,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACrD,OAAO,QAAQ,KAAK,YAAY,IAAI,QAAQ,KAAK,EAAE,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TopicManager = void 0;
|
|
4
|
+
const embedding_1 = require("../utils/embedding");
|
|
5
|
+
// ==================== Defaults ====================
|
|
6
|
+
const DEFAULT_CONFIG = {
|
|
7
|
+
windowSize: 3,
|
|
8
|
+
mutationThreshold: 0.3,
|
|
9
|
+
};
|
|
10
|
+
/** Minimum text length to consider for topic classification. Shorter messages are skipped. */
|
|
11
|
+
const MIN_TEXT_LENGTH = 10;
|
|
12
|
+
// ==================== Math Utilities ====================
|
|
13
|
+
/**
|
|
14
|
+
* Compute cosine similarity between two embedding vectors.
|
|
15
|
+
* cos(a,b) = (a·b) / (|a| * |b|)
|
|
16
|
+
* Returns 0 if either vector is empty or has zero magnitude.
|
|
17
|
+
*/
|
|
18
|
+
function cosineSimilarity(a, b) {
|
|
19
|
+
if (a.length === 0 || b.length === 0)
|
|
20
|
+
return 0;
|
|
21
|
+
let dotProduct = 0;
|
|
22
|
+
let normA = 0;
|
|
23
|
+
let normB = 0;
|
|
24
|
+
const minLen = Math.min(a.length, b.length);
|
|
25
|
+
for (let i = 0; i < minLen; i++) {
|
|
26
|
+
dotProduct += a[i] * b[i];
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i < a.length; i++) {
|
|
29
|
+
normA += a[i] * a[i];
|
|
30
|
+
}
|
|
31
|
+
for (let i = 0; i < b.length; i++) {
|
|
32
|
+
normB += b[i] * b[i];
|
|
33
|
+
}
|
|
34
|
+
const magnitudeA = Math.sqrt(normA);
|
|
35
|
+
const magnitudeB = Math.sqrt(normB);
|
|
36
|
+
if (magnitudeA === 0 || magnitudeB === 0)
|
|
37
|
+
return 0;
|
|
38
|
+
return dotProduct / (magnitudeA * magnitudeB);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a vector to unit length (L2 norm = 1).
|
|
42
|
+
* Returns a copy; does not mutate the input.
|
|
43
|
+
*/
|
|
44
|
+
function normalizeVector(v) {
|
|
45
|
+
const norm = Math.sqrt(v.reduce((sum, x) => sum + x * x, 0));
|
|
46
|
+
if (norm === 0)
|
|
47
|
+
return [...v];
|
|
48
|
+
return v.map(x => x / norm);
|
|
49
|
+
}
|
|
50
|
+
// ==================== TopicManager ====================
|
|
51
|
+
/**
|
|
52
|
+
* Intra-session topic boundary detector using sliding window embedding similarity.
|
|
53
|
+
*
|
|
54
|
+
* How it works:
|
|
55
|
+
* 1. Each incoming event is embedded (via the shared EmbeddingService).
|
|
56
|
+
* 2. The embedding is added to a fixed-size window buffer.
|
|
57
|
+
* 3. Average cosine similarity between window items and the current segment's
|
|
58
|
+
* centroid embedding is computed.
|
|
59
|
+
* 4. If similarity drops below `mutationThreshold`, the current segment is closed
|
|
60
|
+
* and a new one begins. Otherwise, the segment centroid is updated via
|
|
61
|
+
* moving average (0.7 current + 0.3 new).
|
|
62
|
+
*
|
|
63
|
+
* This prevents entity contamination across different topics within the same
|
|
64
|
+
* OpenCode session.
|
|
65
|
+
*/
|
|
66
|
+
class TopicManager {
|
|
67
|
+
/** Internal UUID from the sessions table (session_map_id). */
|
|
68
|
+
sessionMapId;
|
|
69
|
+
/** External OpenCode session ID (as seen in event.session.id). */
|
|
70
|
+
opencodeSessionId;
|
|
71
|
+
/** All topic segments for this session, ordered by segmentIndex. */
|
|
72
|
+
segments;
|
|
73
|
+
/** The currently open (unclosed) segment, if any. */
|
|
74
|
+
currentSegment;
|
|
75
|
+
/**
|
|
76
|
+
* Fixed-size sliding window of recent event embeddings.
|
|
77
|
+
* Used to detect topic drift by comparing against the segment centroid.
|
|
78
|
+
*/
|
|
79
|
+
windowBuffer;
|
|
80
|
+
/** Maximum number of items in the window buffer. */
|
|
81
|
+
windowSize;
|
|
82
|
+
/** Cosine similarity threshold for topic boundary detection. */
|
|
83
|
+
mutationThreshold;
|
|
84
|
+
/** PostgreSQL connection pool. */
|
|
85
|
+
pool;
|
|
86
|
+
constructor(pool, sessionMapId, opencodeSessionId, config) {
|
|
87
|
+
this.pool = pool;
|
|
88
|
+
this.sessionMapId = sessionMapId;
|
|
89
|
+
this.opencodeSessionId = opencodeSessionId;
|
|
90
|
+
this.segments = [];
|
|
91
|
+
this.currentSegment = null;
|
|
92
|
+
this.windowBuffer = [];
|
|
93
|
+
this.windowSize = config?.windowSize ?? DEFAULT_CONFIG.windowSize;
|
|
94
|
+
this.mutationThreshold = config?.mutationThreshold ?? DEFAULT_CONFIG.mutationThreshold;
|
|
95
|
+
}
|
|
96
|
+
// ==================== Public Core API ====================
|
|
97
|
+
/**
|
|
98
|
+
* Classify an incoming event: decide whether it belongs to the current
|
|
99
|
+
* topic segment or triggers a new one.
|
|
100
|
+
*
|
|
101
|
+
* This is the primary entry point for the event handler.
|
|
102
|
+
* Call this for every tool.execute.after and message.updated event.
|
|
103
|
+
*
|
|
104
|
+
* @returns The TopicSegmentInfo this event belongs to.
|
|
105
|
+
*/
|
|
106
|
+
async classifyEvent(event) {
|
|
107
|
+
const text = this.extractTextFromEvent(event);
|
|
108
|
+
// ── Short text skip ──────────────────────────────────────────
|
|
109
|
+
// Messages shorter than MIN_TEXT_LENGTH are not informative
|
|
110
|
+
// enough to warrant topic boundary detection.
|
|
111
|
+
if (!text || text.length < MIN_TEXT_LENGTH) {
|
|
112
|
+
if (this.currentSegment)
|
|
113
|
+
return this.currentSegment;
|
|
114
|
+
// Create an initial segment even for short messages so we always have one.
|
|
115
|
+
return this.createNewSegment(this.sessionMapId, this.segments.length, event.messageId || 'auto');
|
|
116
|
+
}
|
|
117
|
+
// ── Generate embedding ────────────────────────────────────────
|
|
118
|
+
let embedding = [];
|
|
119
|
+
const embService = (0, embedding_1.getEmbeddingService)();
|
|
120
|
+
if (embService) {
|
|
121
|
+
try {
|
|
122
|
+
embedding = await embService.generateEmbedding(text);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.warn('[TopicManager] Embedding generation failed, using zero vector fallback:', err);
|
|
126
|
+
embedding = [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
console.warn('[TopicManager] EmbeddingService unavailable — topic detection disabled');
|
|
131
|
+
}
|
|
132
|
+
// ── Create first segment if none exists ───────────────────────
|
|
133
|
+
if (!this.currentSegment) {
|
|
134
|
+
const seg = await this.createNewSegment(this.sessionMapId, this.segments.length, event.messageId || 'auto');
|
|
135
|
+
this.currentSegment = seg;
|
|
136
|
+
this.currentSegment.embedding = embedding;
|
|
137
|
+
if (embedding.length > 0) {
|
|
138
|
+
this.windowBuffer.push({ messageId: event.messageId || 'auto', embedding });
|
|
139
|
+
}
|
|
140
|
+
return seg;
|
|
141
|
+
}
|
|
142
|
+
// ── No embedding available → stay in current segment ──────────
|
|
143
|
+
if (embedding.length === 0) {
|
|
144
|
+
return this.currentSegment;
|
|
145
|
+
}
|
|
146
|
+
// ── Sliding window management ─────────────────────────────────
|
|
147
|
+
this.windowBuffer.push({ messageId: event.messageId || 'auto', embedding });
|
|
148
|
+
while (this.windowBuffer.length > this.windowSize) {
|
|
149
|
+
this.windowBuffer.shift();
|
|
150
|
+
}
|
|
151
|
+
// ── Topic boundary detection ──────────────────────────────────
|
|
152
|
+
const centroid = this.currentSegment.embedding;
|
|
153
|
+
if (centroid && centroid.length > 0 && this.windowBuffer.length >= 2) {
|
|
154
|
+
const similarities = this.windowBuffer.map(item => cosineSimilarity(item.embedding, centroid));
|
|
155
|
+
const avgSimilarity = similarities.reduce((sum, s) => sum + s, 0) / similarities.length;
|
|
156
|
+
if (avgSimilarity < this.mutationThreshold) {
|
|
157
|
+
// Topic shift detected: close old segment, start new one.
|
|
158
|
+
const boundaryMessageId = this.windowBuffer[0]?.messageId || event.messageId || 'auto';
|
|
159
|
+
await this.closeCurrentSegment(boundaryMessageId);
|
|
160
|
+
const newSeg = await this.createNewSegment(this.sessionMapId, this.segments.length, event.messageId || 'auto');
|
|
161
|
+
this.currentSegment = newSeg;
|
|
162
|
+
this.currentSegment.embedding = embedding;
|
|
163
|
+
// Reset window: only the current event belongs to the new segment.
|
|
164
|
+
this.windowBuffer = [{ messageId: event.messageId || 'auto', embedding }];
|
|
165
|
+
console.log(`[TopicManager] Topic shift detected (similarity=${avgSimilarity.toFixed(3)} < ${this.mutationThreshold}), ` +
|
|
166
|
+
`new segment #${newSeg.segmentIndex} created`);
|
|
167
|
+
return newSeg;
|
|
168
|
+
}
|
|
169
|
+
// ── No shift: update segment centroid via moving average ────
|
|
170
|
+
this.currentSegment.embedding = this.updateSegmentEmbedding(centroid, embedding);
|
|
171
|
+
}
|
|
172
|
+
else if (!centroid || centroid.length === 0) {
|
|
173
|
+
// First meaningful embedding for this segment.
|
|
174
|
+
this.currentSegment.embedding = embedding;
|
|
175
|
+
}
|
|
176
|
+
return this.currentSegment;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Create a new topic segment in the database and track it in memory.
|
|
180
|
+
*/
|
|
181
|
+
async createNewSegment(sessionMapId, index, startMessageId) {
|
|
182
|
+
const result = await this.pool.query(`INSERT INTO topic_segments (session_map_id, segment_index, start_message_external_id)
|
|
183
|
+
VALUES ($1, $2, $3)
|
|
184
|
+
RETURNING id, session_map_id, segment_index, start_message_external_id, created_at`, [sessionMapId, index, startMessageId]);
|
|
185
|
+
const row = result.rows[0];
|
|
186
|
+
const segment = {
|
|
187
|
+
id: row.id,
|
|
188
|
+
sessionMapId: row.session_map_id,
|
|
189
|
+
segmentIndex: row.segment_index,
|
|
190
|
+
startMessageExternalId: row.start_message_external_id,
|
|
191
|
+
};
|
|
192
|
+
this.segments.push(segment);
|
|
193
|
+
return segment;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Close the current topic segment: generate a summary, compute its
|
|
197
|
+
* embedding, and persist both to the database.
|
|
198
|
+
*
|
|
199
|
+
* After calling this, `currentSegment` is set to null.
|
|
200
|
+
*/
|
|
201
|
+
async closeCurrentSegment(endMessageId) {
|
|
202
|
+
if (!this.currentSegment)
|
|
203
|
+
return;
|
|
204
|
+
const seg = this.currentSegment;
|
|
205
|
+
// ── Generate summary ────────────────────────────────────────
|
|
206
|
+
let summary = '';
|
|
207
|
+
try {
|
|
208
|
+
summary = await this.retrieveSegmentSummary(seg.id);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
console.warn('[TopicManager] Summary generation failed:', err);
|
|
212
|
+
summary = `Segment #${seg.segmentIndex}`;
|
|
213
|
+
}
|
|
214
|
+
// ── Generate embedding for the summary ──────────────────────
|
|
215
|
+
let summaryEmbedding = [];
|
|
216
|
+
const embService = (0, embedding_1.getEmbeddingService)();
|
|
217
|
+
if (embService && summary) {
|
|
218
|
+
try {
|
|
219
|
+
summaryEmbedding = await embService.generateEmbedding(summary);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
console.warn('[TopicManager] Summary embedding generation failed:', err);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ── Persist closure to database ─────────────────────────────
|
|
226
|
+
await this.pool.query(`UPDATE topic_segments
|
|
227
|
+
SET summary = $1,
|
|
228
|
+
embedding = $2,
|
|
229
|
+
end_message_external_id = $3,
|
|
230
|
+
closed_at = NOW()
|
|
231
|
+
WHERE id = $4`, [
|
|
232
|
+
summary || null,
|
|
233
|
+
summaryEmbedding.length > 0 ? summaryEmbedding : null,
|
|
234
|
+
endMessageId,
|
|
235
|
+
seg.id,
|
|
236
|
+
]);
|
|
237
|
+
// ── Update in-memory state ──────────────────────────────────
|
|
238
|
+
seg.summary = summary;
|
|
239
|
+
seg.embedding = summaryEmbedding.length > 0 ? summaryEmbedding : undefined;
|
|
240
|
+
seg.endMessageExternalId = endMessageId;
|
|
241
|
+
seg.closedAt = new Date();
|
|
242
|
+
this.currentSegment = null;
|
|
243
|
+
console.log(`[TopicManager] Closed segment #${seg.segmentIndex}: ${summary}`);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Close all currently open segments (should be called when the
|
|
247
|
+
* OpenCode session ends or is compacted).
|
|
248
|
+
*
|
|
249
|
+
* Updates the session's last_active_at timestamp.
|
|
250
|
+
*/
|
|
251
|
+
async closeAllPendingSegments() {
|
|
252
|
+
if (this.currentSegment) {
|
|
253
|
+
await this.closeCurrentSegment('session-end');
|
|
254
|
+
}
|
|
255
|
+
// Update session timestamp (updated_at serves as last_active_at proxy).
|
|
256
|
+
try {
|
|
257
|
+
await this.pool.query(`UPDATE sessions SET updated_at = NOW() WHERE id = $1`, [this.sessionMapId]);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
console.warn('[TopicManager] Failed to update session timestamp:', err);
|
|
261
|
+
}
|
|
262
|
+
this.windowBuffer = [];
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Generate a 1-sentence summary for a segment by inspecting its
|
|
266
|
+
* observations and producing a heuristic or LLM-generated description.
|
|
267
|
+
*
|
|
268
|
+
* Uses a heuristic by default (tool name aggregation). The LLM path
|
|
269
|
+
* can be wired in later for higher-quality summaries.
|
|
270
|
+
*/
|
|
271
|
+
async retrieveSegmentSummary(segmentId) {
|
|
272
|
+
// Retrieve observations that fall within this segment's time range.
|
|
273
|
+
const result = await this.pool.query(`SELECT o.tool_name, o.tool_input_summary, o.tool_output_summary
|
|
274
|
+
FROM observations o
|
|
275
|
+
JOIN topic_segments ts ON ts.session_map_id = o.session_id
|
|
276
|
+
WHERE ts.id = $1
|
|
277
|
+
AND o.created_at >= ts.created_at
|
|
278
|
+
AND (ts.closed_at IS NULL OR o.created_at <= ts.closed_at)
|
|
279
|
+
ORDER BY o.created_at ASC
|
|
280
|
+
LIMIT 50`, [segmentId]);
|
|
281
|
+
if (result.rows.length === 0) {
|
|
282
|
+
// Fallback: get recent observations for the session.
|
|
283
|
+
const fallbackResult = await this.pool.query(`SELECT tool_name, tool_input_summary
|
|
284
|
+
FROM observations
|
|
285
|
+
WHERE session_id = $1
|
|
286
|
+
ORDER BY created_at DESC
|
|
287
|
+
LIMIT 20`, [this.sessionMapId]);
|
|
288
|
+
if (fallbackResult.rows.length === 0) {
|
|
289
|
+
return 'Empty segment';
|
|
290
|
+
}
|
|
291
|
+
const toolNames = [
|
|
292
|
+
...new Set(fallbackResult.rows
|
|
293
|
+
.map((r) => r.tool_name)
|
|
294
|
+
.filter(Boolean)),
|
|
295
|
+
];
|
|
296
|
+
return `Topic segment covering ${fallbackResult.rows.length} observations` +
|
|
297
|
+
(toolNames.length > 0 ? ` using tools: ${toolNames.slice(0, 5).join(', ')}` : '');
|
|
298
|
+
}
|
|
299
|
+
const toolNames = [
|
|
300
|
+
...new Set(result.rows
|
|
301
|
+
.map((r) => r.tool_name)
|
|
302
|
+
.filter(Boolean)),
|
|
303
|
+
];
|
|
304
|
+
// Heuristic 1-sentence summary.
|
|
305
|
+
let summary = `Topic segment with ${result.rows.length} observations`;
|
|
306
|
+
if (toolNames.length > 0) {
|
|
307
|
+
summary += ` covering: ${toolNames.slice(0, 5).join(', ')}`;
|
|
308
|
+
}
|
|
309
|
+
// Add a hint about the first tool's input for context.
|
|
310
|
+
if (result.rows[0]?.tool_input_summary) {
|
|
311
|
+
const firstInput = String(result.rows[0].tool_input_summary).substring(0, 80);
|
|
312
|
+
summary += ` — starts with ${firstInput}`;
|
|
313
|
+
}
|
|
314
|
+
return summary;
|
|
315
|
+
}
|
|
316
|
+
// ==================== Static Factory ====================
|
|
317
|
+
/**
|
|
318
|
+
* Factory method: resolve the internal session_map_id from the
|
|
319
|
+
* OpenCode session ID, load any existing segments, and return a
|
|
320
|
+
* fully initialized TopicManager.
|
|
321
|
+
*
|
|
322
|
+
* Typical usage:
|
|
323
|
+
* const tm = await TopicManager.forSession(pool, event.session.id);
|
|
324
|
+
*/
|
|
325
|
+
static async forSession(pool, opencodeSessionId, config) {
|
|
326
|
+
// Resolve internal session UUID.
|
|
327
|
+
const sessionResult = await pool.query(`SELECT id FROM sessions WHERE external_id = $1`, [opencodeSessionId]);
|
|
328
|
+
if (sessionResult.rows.length === 0) {
|
|
329
|
+
throw new Error(`[TopicManager] Session not found for OpenCode session ID: ${opencodeSessionId}. ` +
|
|
330
|
+
`Ensure the session has been recorded before creating a TopicManager.`);
|
|
331
|
+
}
|
|
332
|
+
const sessionMapId = sessionResult.rows[0].id;
|
|
333
|
+
const manager = new TopicManager(pool, sessionMapId, opencodeSessionId, config);
|
|
334
|
+
// Load existing segments from DB.
|
|
335
|
+
const segmentsResult = await pool.query(`SELECT id, session_map_id, segment_index,
|
|
336
|
+
start_message_external_id, end_message_external_id,
|
|
337
|
+
summary, embedding, closed_at, created_at
|
|
338
|
+
FROM topic_segments
|
|
339
|
+
WHERE session_map_id = $1
|
|
340
|
+
ORDER BY segment_index ASC`, [sessionMapId]);
|
|
341
|
+
manager.segments = segmentsResult.rows.map((row) => ({
|
|
342
|
+
id: row.id,
|
|
343
|
+
sessionMapId: row.session_map_id,
|
|
344
|
+
segmentIndex: row.segment_index,
|
|
345
|
+
startMessageExternalId: row.start_message_external_id,
|
|
346
|
+
endMessageExternalId: row.end_message_external_id ?? undefined,
|
|
347
|
+
summary: row.summary ?? undefined,
|
|
348
|
+
embedding: row.embedding ?? undefined,
|
|
349
|
+
closedAt: row.closed_at ?? undefined,
|
|
350
|
+
}));
|
|
351
|
+
// Restore currentSegment (the last unclosed segment, if any).
|
|
352
|
+
const openSegment = manager.segments.find(s => !s.closedAt);
|
|
353
|
+
if (openSegment) {
|
|
354
|
+
manager.currentSegment = openSegment;
|
|
355
|
+
}
|
|
356
|
+
return manager;
|
|
357
|
+
}
|
|
358
|
+
// ==================== Accessors ====================
|
|
359
|
+
/** Returns the internal session UUID (session_map_id). */
|
|
360
|
+
getSessionMapId() {
|
|
361
|
+
return this.sessionMapId;
|
|
362
|
+
}
|
|
363
|
+
/** Returns the OpenCode external session ID. */
|
|
364
|
+
getOpenCodeSessionId() {
|
|
365
|
+
return this.opencodeSessionId;
|
|
366
|
+
}
|
|
367
|
+
/** Returns the current segment ID, or null if no segment is open. */
|
|
368
|
+
getCurrentSegmentId() {
|
|
369
|
+
return this.currentSegment?.id ?? null;
|
|
370
|
+
}
|
|
371
|
+
/** Returns the current segment, or null. */
|
|
372
|
+
getCurrentSegment() {
|
|
373
|
+
return this.currentSegment;
|
|
374
|
+
}
|
|
375
|
+
/** Returns all segments for this session (including closed ones). */
|
|
376
|
+
getSegments() {
|
|
377
|
+
return [...this.segments];
|
|
378
|
+
}
|
|
379
|
+
// ==================== Private Helpers ====================
|
|
380
|
+
/**
|
|
381
|
+
* Extract a representative text string from an event for embedding.
|
|
382
|
+
*
|
|
383
|
+
* - For tool.execute.after events: combines tool name with a snippet
|
|
384
|
+
* of the result output.
|
|
385
|
+
* - For message events: uses the message content directly.
|
|
386
|
+
* - For all others: falls back to the event type string.
|
|
387
|
+
*/
|
|
388
|
+
extractTextFromEvent(event) {
|
|
389
|
+
// Tool result events (tool.execute.after).
|
|
390
|
+
if (event.tool?.name && event.result) {
|
|
391
|
+
const toolName = event.tool.name;
|
|
392
|
+
let resultStr;
|
|
393
|
+
if (!event.result.success) {
|
|
394
|
+
resultStr = event.result.error || 'error';
|
|
395
|
+
}
|
|
396
|
+
else if (event.result.data === undefined || event.result.data === null) {
|
|
397
|
+
resultStr = 'success';
|
|
398
|
+
}
|
|
399
|
+
else if (typeof event.result.data === 'string') {
|
|
400
|
+
resultStr = event.result.data.substring(0, 300);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
try {
|
|
404
|
+
resultStr = JSON.stringify(event.result.data).substring(0, 300);
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
resultStr = 'complex data';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return `${toolName}: ${resultStr}`;
|
|
411
|
+
}
|
|
412
|
+
// Message events (message.updated).
|
|
413
|
+
if (event.message?.content) {
|
|
414
|
+
return event.message.content;
|
|
415
|
+
}
|
|
416
|
+
// Direct content field (some message variants).
|
|
417
|
+
if (typeof event.content === 'string') {
|
|
418
|
+
return event.content;
|
|
419
|
+
}
|
|
420
|
+
// Fallback: event type provides minimal context.
|
|
421
|
+
return event.type || 'unknown';
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Update a segment's centroid embedding using a moving average.
|
|
425
|
+
*
|
|
426
|
+
* Formula: 0.7 * current + 0.3 * new, then normalized to unit length.
|
|
427
|
+
*
|
|
428
|
+
* This ensures the centroid slowly tracks topic drift within a segment
|
|
429
|
+
* while remaining anchored to the dominant theme.
|
|
430
|
+
*
|
|
431
|
+
* Gracefully handles mismatched vector lengths and null/empty inputs.
|
|
432
|
+
*/
|
|
433
|
+
updateSegmentEmbedding(current, newEmb) {
|
|
434
|
+
if (!current || current.length === 0)
|
|
435
|
+
return [...newEmb];
|
|
436
|
+
if (!newEmb || newEmb.length === 0)
|
|
437
|
+
return [...current];
|
|
438
|
+
const length = Math.min(current.length, newEmb.length);
|
|
439
|
+
const result = new Array(length);
|
|
440
|
+
for (let i = 0; i < length; i++) {
|
|
441
|
+
result[i] = 0.7 * current[i] + 0.3 * newEmb[i];
|
|
442
|
+
}
|
|
443
|
+
return normalizeVector(result);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
exports.TopicManager = TopicManager;
|
|
447
|
+
//# sourceMappingURL=segment-manager.js.map
|