@memtensor/memos-local-openclaw-plugin 1.0.8-beta.2 → 1.0.8-beta.3
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/index.ts +7 -1
- package/package.json +1 -1
- package/src/hub/server.ts +5 -4
- package/src/ingest/providers/anthropic.ts +9 -6
- package/src/ingest/providers/bedrock.ts +9 -6
- package/src/ingest/providers/gemini.ts +9 -6
- package/src/ingest/providers/index.ts +122 -21
- package/src/ingest/providers/openai.ts +141 -6
- package/src/ingest/task-processor.ts +61 -41
- package/src/ingest/worker.ts +32 -11
- package/src/recall/engine.ts +1 -0
- package/src/sharing/types.ts +1 -0
- package/src/storage/sqlite.ts +39 -5
- package/src/types.ts +3 -0
- package/src/viewer/html.ts +54 -28
- package/src/viewer/server.ts +63 -1
|
@@ -51,6 +51,9 @@ export class TaskProcessor {
|
|
|
51
51
|
* Determines if a new task boundary was crossed and handles transition.
|
|
52
52
|
*/
|
|
53
53
|
async onChunksIngested(sessionKey: string, latestTimestamp: number, owner?: string): Promise<void> {
|
|
54
|
+
if (sessionKey.startsWith("temp:") || sessionKey.startsWith("internal:") || sessionKey.startsWith("system:")) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
54
57
|
const resolvedOwner = owner ?? "agent:main";
|
|
55
58
|
this.ctx.log.debug(`TaskProcessor.onChunksIngested called session=${sessionKey} ts=${latestTimestamp} owner=${resolvedOwner} processing=${this.processing}`);
|
|
56
59
|
this.pendingEvents.push({ sessionKey, latestTimestamp, owner: resolvedOwner });
|
|
@@ -79,13 +82,19 @@ export class TaskProcessor {
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
private static extractAgentPrefix(sessionKey: string): string {
|
|
86
|
+
const parts = sessionKey.split(":");
|
|
87
|
+
return parts.length >= 3 ? parts.slice(0, 3).join(":") : sessionKey;
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
private async detectAndProcess(sessionKey: string, latestTimestamp: number, owner: string): Promise<void> {
|
|
83
91
|
this.ctx.log.debug(`TaskProcessor.detectAndProcess session=${sessionKey} owner=${owner}`);
|
|
84
92
|
|
|
93
|
+
const currentAgentPrefix = TaskProcessor.extractAgentPrefix(sessionKey);
|
|
85
94
|
const allActive = this.store.getAllActiveTasks(owner);
|
|
86
95
|
for (const t of allActive) {
|
|
87
|
-
if (t.sessionKey !== sessionKey) {
|
|
88
|
-
this.ctx.log.info(`Session changed: finalizing task=${t.id} from session=${t.sessionKey} (owner=${owner})`);
|
|
96
|
+
if (t.sessionKey !== sessionKey && TaskProcessor.extractAgentPrefix(t.sessionKey) === currentAgentPrefix) {
|
|
97
|
+
this.ctx.log.info(`Session changed within agent: finalizing task=${t.id} from session=${t.sessionKey} (owner=${owner})`);
|
|
89
98
|
await this.finalizeTask(t);
|
|
90
99
|
}
|
|
91
100
|
}
|
|
@@ -179,26 +188,36 @@ export class TaskProcessor {
|
|
|
179
188
|
continue;
|
|
180
189
|
}
|
|
181
190
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
191
|
+
// Structured topic classification
|
|
192
|
+
const taskState = this.buildTopicJudgeState(currentTaskChunks, userChunk);
|
|
184
193
|
const newMsg = userChunk.content.slice(0, 500);
|
|
185
|
-
this.ctx.log.info(`Topic
|
|
186
|
-
const
|
|
187
|
-
this.ctx.log.info(`Topic
|
|
194
|
+
this.ctx.log.info(`Topic classify: "${newMsg.slice(0, 60)}" vs ${existingUserCount} user turns`);
|
|
195
|
+
const result = await this.summarizer.classifyTopic(taskState, newMsg);
|
|
196
|
+
this.ctx.log.info(`Topic classify: decision=${result?.decision ?? "null"} confidence=${result?.confidence ?? "?"} type=${result?.boundaryType ?? "?"} reason=${result?.reason ?? ""}`);
|
|
188
197
|
|
|
189
|
-
if (
|
|
198
|
+
if (!result || result.decision === "SAME") {
|
|
190
199
|
this.assignChunksToTask(turn, currentTask.id);
|
|
191
200
|
currentTaskChunks = currentTaskChunks.concat(turn);
|
|
192
201
|
continue;
|
|
193
202
|
}
|
|
194
203
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
// Low-confidence NEW: second-pass arbitration
|
|
205
|
+
if (result.confidence < 0.65) {
|
|
206
|
+
this.ctx.log.info(`Low confidence NEW (${result.confidence}), running second-pass arbitration...`);
|
|
207
|
+
const secondResult = await this.summarizer.arbitrateTopicSplit(taskState, newMsg);
|
|
208
|
+
this.ctx.log.info(`Second-pass result: ${secondResult ?? "null(fallback->SAME)"}`);
|
|
209
|
+
if (!secondResult || secondResult !== "NEW") {
|
|
210
|
+
this.assignChunksToTask(turn, currentTask.id);
|
|
211
|
+
currentTaskChunks = currentTaskChunks.concat(turn);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
200
214
|
}
|
|
201
215
|
|
|
216
|
+
this.ctx.log.info(`Task boundary at turn ${i}: classifier judged NEW (confidence=${result.confidence}). Msg: "${newMsg.slice(0, 80)}..."`);
|
|
217
|
+
await this.finalizeTask(currentTask);
|
|
218
|
+
currentTask = await this.createNewTaskReturn(sessionKey, userChunk.createdAt, owner);
|
|
219
|
+
currentTaskChunks = [];
|
|
220
|
+
|
|
202
221
|
this.assignChunksToTask(turn, currentTask.id);
|
|
203
222
|
currentTaskChunks = currentTaskChunks.concat(turn);
|
|
204
223
|
}
|
|
@@ -226,38 +245,39 @@ export class TaskProcessor {
|
|
|
226
245
|
}
|
|
227
246
|
|
|
228
247
|
/**
|
|
229
|
-
* Build
|
|
230
|
-
* Includes
|
|
231
|
-
*
|
|
232
|
-
* and where the conversation currently is.
|
|
233
|
-
*
|
|
234
|
-
* For user messages, include full content (up to 500 chars) since
|
|
235
|
-
* they carry the topic signal. For assistant messages, use summary
|
|
236
|
-
* or truncated content since they mostly elaborate.
|
|
248
|
+
* Build compact task state for the LLM topic classifier.
|
|
249
|
+
* Includes: topic (first user msg), last 3 turn summaries,
|
|
250
|
+
* and optional assistant snippet for short/ambiguous messages.
|
|
237
251
|
*/
|
|
238
|
-
private
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
private buildTopicJudgeState(chunks: Chunk[], newUserChunk: Chunk): string {
|
|
253
|
+
const conv = chunks.filter((c) => c.role === "user" || c.role === "assistant");
|
|
254
|
+
if (conv.length === 0) return "";
|
|
255
|
+
|
|
256
|
+
const firstUser = conv.find((c) => c.role === "user");
|
|
257
|
+
const topic = firstUser?.summary || firstUser?.content.slice(0, 80) || "";
|
|
258
|
+
|
|
259
|
+
const turns: Array<{ u: string; a: string }> = [];
|
|
260
|
+
for (let j = 0; j < conv.length; j++) {
|
|
261
|
+
if (conv[j].role === "user") {
|
|
262
|
+
const u = conv[j].summary || conv[j].content.slice(0, 60);
|
|
263
|
+
const nextA = conv[j + 1]?.role === "assistant" ? conv[j + 1] : null;
|
|
264
|
+
const a = nextA ? (nextA.summary || nextA.content.slice(0, 60)) : "";
|
|
265
|
+
turns.push({ u, a });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const recent = turns.slice(-3);
|
|
270
|
+
const turnLines = recent.map((t, i) => `${i + 1}. U:${t.u} A:${t.a}`);
|
|
248
271
|
|
|
249
|
-
|
|
250
|
-
|
|
272
|
+
let snippet = "";
|
|
273
|
+
if (newUserChunk.content.length < 30 || /^[那这它其还哪啥]/.test(newUserChunk.content.trim())) {
|
|
274
|
+
const lastA = [...conv].reverse().find((c) => c.role === "assistant");
|
|
275
|
+
if (lastA) snippet = lastA.content.slice(0, 200);
|
|
251
276
|
}
|
|
252
277
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
return
|
|
256
|
-
"--- Task opening ---",
|
|
257
|
-
...opening,
|
|
258
|
-
"--- Recent exchanges ---",
|
|
259
|
-
...recent,
|
|
260
|
-
].join("\n");
|
|
278
|
+
const parts = [`topic:${topic}`, ...turnLines];
|
|
279
|
+
if (snippet) parts.push(`lastA:${snippet}`);
|
|
280
|
+
return parts.join("\n");
|
|
261
281
|
}
|
|
262
282
|
|
|
263
283
|
private async createNewTaskReturn(sessionKey: string, timestamp: number, owner: string = "agent:main"): Promise<Task> {
|
package/src/ingest/worker.ts
CHANGED
|
@@ -25,8 +25,14 @@ export class IngestWorker {
|
|
|
25
25
|
|
|
26
26
|
getTaskProcessor(): TaskProcessor { return this.taskProcessor; }
|
|
27
27
|
|
|
28
|
+
private static isEphemeralSession(sessionKey: string): boolean {
|
|
29
|
+
return sessionKey.startsWith("temp:") || sessionKey.startsWith("internal:") || sessionKey.startsWith("system:");
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
enqueue(messages: ConversationMessage[]): void {
|
|
29
|
-
|
|
33
|
+
const filtered = messages.filter((m) => !IngestWorker.isEphemeralSession(m.sessionKey));
|
|
34
|
+
if (filtered.length === 0) return;
|
|
35
|
+
this.queue.push(...filtered);
|
|
30
36
|
if (!this.processing) {
|
|
31
37
|
this.processQueue().catch((err) => {
|
|
32
38
|
this.ctx.log.error(`Ingest worker error: ${err}`);
|
|
@@ -150,14 +156,23 @@ export class IngestWorker {
|
|
|
150
156
|
let mergeHistory = "[]";
|
|
151
157
|
|
|
152
158
|
// Fast path: exact content_hash match within same owner (agent dimension)
|
|
159
|
+
// Strategy: retire the OLD chunk, keep the NEW one active (latest wins)
|
|
153
160
|
const chunkOwner = msg.owner ?? "agent:main";
|
|
154
161
|
const existingByHash = this.store.findActiveChunkByHash(content, chunkOwner);
|
|
155
162
|
if (existingByHash) {
|
|
156
|
-
this.ctx.log.debug(`Exact-dup (owner=${chunkOwner}): hash match →
|
|
163
|
+
this.ctx.log.debug(`Exact-dup (owner=${chunkOwner}): hash match → retiring old=${existingByHash}, keeping new=${chunkId}`);
|
|
157
164
|
this.store.recordMergeHit(existingByHash, "DUPLICATE", "exact content hash match");
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
const oldChunk = this.store.getChunk(existingByHash);
|
|
166
|
+
this.store.markDedupStatus(existingByHash, "duplicate", chunkId, "exact content hash match");
|
|
167
|
+
this.store.deleteEmbedding(existingByHash);
|
|
168
|
+
mergedFromOld = existingByHash;
|
|
160
169
|
dedupReason = "exact content hash match";
|
|
170
|
+
if (oldChunk) {
|
|
171
|
+
const oldHistory = JSON.parse(oldChunk.mergeHistory || "[]");
|
|
172
|
+
oldHistory.push({ action: "duplicate_superseded", at: Date.now(), reason: "exact content hash match", sourceChunkId: existingByHash });
|
|
173
|
+
mergeHistory = JSON.stringify(oldHistory);
|
|
174
|
+
mergeCount = (oldChunk.mergeCount || 0) + 1;
|
|
175
|
+
}
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
// Smart dedup: find Top-5 similar chunks, then ask LLM to judge
|
|
@@ -173,8 +188,9 @@ export class IngestWorker {
|
|
|
173
188
|
index: i + 1,
|
|
174
189
|
summary: chunk?.summary ?? "",
|
|
175
190
|
chunkId: s.chunkId,
|
|
191
|
+
role: chunk?.role,
|
|
176
192
|
};
|
|
177
|
-
}).filter(c => c.summary);
|
|
193
|
+
}).filter(c => c.summary && c.role === msg.role);
|
|
178
194
|
|
|
179
195
|
if (candidates.length > 0) {
|
|
180
196
|
const dedupResult = await this.summarizer.judgeDedup(summary, candidates);
|
|
@@ -183,10 +199,18 @@ export class IngestWorker {
|
|
|
183
199
|
const targetChunkId = candidates[dedupResult.targetIndex - 1]?.chunkId;
|
|
184
200
|
if (targetChunkId) {
|
|
185
201
|
this.store.recordMergeHit(targetChunkId, "DUPLICATE", dedupResult.reason);
|
|
186
|
-
|
|
187
|
-
|
|
202
|
+
const oldChunk = this.store.getChunk(targetChunkId);
|
|
203
|
+
this.store.markDedupStatus(targetChunkId, "duplicate", chunkId, dedupResult.reason);
|
|
204
|
+
this.store.deleteEmbedding(targetChunkId);
|
|
205
|
+
mergedFromOld = targetChunkId;
|
|
188
206
|
dedupReason = dedupResult.reason;
|
|
189
|
-
|
|
207
|
+
if (oldChunk) {
|
|
208
|
+
const oldHistory = JSON.parse(oldChunk.mergeHistory || "[]");
|
|
209
|
+
oldHistory.push({ action: "duplicate_superseded", at: Date.now(), reason: dedupResult.reason, sourceChunkId: targetChunkId });
|
|
210
|
+
mergeHistory = JSON.stringify(oldHistory);
|
|
211
|
+
mergeCount = (oldChunk.mergeCount || 0) + 1;
|
|
212
|
+
}
|
|
213
|
+
this.ctx.log.debug(`Smart dedup: DUPLICATE → retiring old=${targetChunkId}, keeping new=${chunkId} active, reason: ${dedupResult.reason}`);
|
|
190
214
|
}
|
|
191
215
|
}
|
|
192
216
|
|
|
@@ -266,9 +290,6 @@ export class IngestWorker {
|
|
|
266
290
|
}
|
|
267
291
|
this.ctx.log.debug(`Stored chunk=${chunkId} kind=${kind} role=${msg.role} dedup=${dedupStatus} len=${content.length} hasVec=${!!embedding && dedupStatus === "active"}`);
|
|
268
292
|
|
|
269
|
-
if (dedupStatus === "duplicate") {
|
|
270
|
-
return { action: "duplicate", summary, targetChunkId: dedupTarget ?? undefined, reason: dedupReason ?? undefined };
|
|
271
|
-
}
|
|
272
293
|
if (mergedFromOld) {
|
|
273
294
|
return { action: "merged", chunkId, summary, targetChunkId: mergedFromOld, reason: dedupReason ?? undefined };
|
|
274
295
|
}
|
package/src/recall/engine.ts
CHANGED
|
@@ -234,6 +234,7 @@ export class RecallEngine {
|
|
|
234
234
|
score: Math.round(candidate.score * 1000) / 1000,
|
|
235
235
|
taskId: chunk.taskId,
|
|
236
236
|
skillId: chunk.skillId,
|
|
237
|
+
owner: chunk.owner,
|
|
237
238
|
origin: chunk.owner === "public" ? "local-shared" : "local",
|
|
238
239
|
source: {
|
|
239
240
|
ts: chunk.createdAt,
|
package/src/sharing/types.ts
CHANGED
package/src/storage/sqlite.ts
CHANGED
|
@@ -110,10 +110,12 @@ export class SqliteStore {
|
|
|
110
110
|
this.migrateOwnerFields();
|
|
111
111
|
this.migrateSkillVisibility();
|
|
112
112
|
this.migrateSkillEmbeddingsAndFts();
|
|
113
|
+
this.migrateTaskTopicColumn();
|
|
113
114
|
this.migrateTaskEmbeddingsAndFts();
|
|
114
115
|
this.migrateFtsToTrigram();
|
|
115
116
|
this.migrateHubTables();
|
|
116
117
|
this.migrateHubFtsToTrigram();
|
|
118
|
+
this.migrateHubMemorySourceAgent();
|
|
117
119
|
this.migrateLocalSharedTasksOwner();
|
|
118
120
|
this.migrateHubUserIdentityFields();
|
|
119
121
|
this.migrateClientHubConnectionIdentityFields();
|
|
@@ -125,6 +127,16 @@ export class SqliteStore {
|
|
|
125
127
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_dedup_created ON chunks(dedup_status, created_at DESC)");
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
private migrateHubMemorySourceAgent(): void {
|
|
131
|
+
try {
|
|
132
|
+
const cols = this.db.prepare("PRAGMA table_info(hub_memories)").all() as Array<{ name: string }>;
|
|
133
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "source_agent")) {
|
|
134
|
+
this.db.exec("ALTER TABLE hub_memories ADD COLUMN source_agent TEXT NOT NULL DEFAULT ''");
|
|
135
|
+
this.log.info("Migrated: added source_agent column to hub_memories");
|
|
136
|
+
}
|
|
137
|
+
} catch { /* table may not exist yet */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
private migrateLocalSharedTasksOwner(): void {
|
|
129
141
|
try {
|
|
130
142
|
const cols = this.db.prepare("PRAGMA table_info(local_shared_tasks)").all() as Array<{ name: string }>;
|
|
@@ -557,6 +569,14 @@ export class SqliteStore {
|
|
|
557
569
|
}
|
|
558
570
|
}
|
|
559
571
|
|
|
572
|
+
private migrateTaskTopicColumn(): void {
|
|
573
|
+
const cols = this.db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
|
574
|
+
if (!cols.some((c) => c.name === "topic")) {
|
|
575
|
+
this.db.exec("ALTER TABLE tasks ADD COLUMN topic TEXT DEFAULT NULL");
|
|
576
|
+
this.log.info("Migrated: added topic column to tasks");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
560
580
|
private migrateTaskSkillMeta(): void {
|
|
561
581
|
const cols = this.db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
|
562
582
|
if (!cols.some((c) => c.name === "skill_status")) {
|
|
@@ -1042,6 +1062,7 @@ export class SqliteStore {
|
|
|
1042
1062
|
id TEXT PRIMARY KEY,
|
|
1043
1063
|
source_chunk_id TEXT NOT NULL,
|
|
1044
1064
|
source_user_id TEXT NOT NULL,
|
|
1065
|
+
source_agent TEXT NOT NULL DEFAULT '',
|
|
1045
1066
|
role TEXT NOT NULL,
|
|
1046
1067
|
content TEXT NOT NULL,
|
|
1047
1068
|
summary TEXT NOT NULL DEFAULT '',
|
|
@@ -1483,8 +1504,15 @@ export class SqliteStore {
|
|
|
1483
1504
|
|
|
1484
1505
|
deleteAll(): number {
|
|
1485
1506
|
this.db.exec("PRAGMA foreign_keys = OFF");
|
|
1507
|
+
try {
|
|
1508
|
+
this.db.exec("DROP TRIGGER IF EXISTS tasks_fts_ai");
|
|
1509
|
+
this.db.exec("DROP TRIGGER IF EXISTS tasks_fts_ad");
|
|
1510
|
+
this.db.exec("DROP TRIGGER IF EXISTS tasks_fts_au");
|
|
1511
|
+
this.db.exec("DELETE FROM tasks_fts");
|
|
1512
|
+
} catch (_) {}
|
|
1486
1513
|
const tables = [
|
|
1487
1514
|
"task_skills",
|
|
1515
|
+
"task_embeddings",
|
|
1488
1516
|
"skill_embeddings",
|
|
1489
1517
|
"skill_versions",
|
|
1490
1518
|
"skills",
|
|
@@ -1507,6 +1535,7 @@ export class SqliteStore {
|
|
|
1507
1535
|
}
|
|
1508
1536
|
}
|
|
1509
1537
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
1538
|
+
this.migrateTaskEmbeddingsAndFts();
|
|
1510
1539
|
const remaining = this.countChunks();
|
|
1511
1540
|
return remaining === 0 ? 1 : 0;
|
|
1512
1541
|
}
|
|
@@ -2574,9 +2603,10 @@ export class SqliteStore {
|
|
|
2574
2603
|
|
|
2575
2604
|
upsertHubMemory(memory: HubMemoryRecord): void {
|
|
2576
2605
|
this.db.prepare(`
|
|
2577
|
-
INSERT INTO hub_memories (id, source_chunk_id, source_user_id, role, content, summary, kind, group_id, visibility, created_at, updated_at)
|
|
2578
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2606
|
+
INSERT INTO hub_memories (id, source_chunk_id, source_user_id, source_agent, role, content, summary, kind, group_id, visibility, created_at, updated_at)
|
|
2607
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2579
2608
|
ON CONFLICT(source_user_id, source_chunk_id) DO UPDATE SET
|
|
2609
|
+
source_agent = excluded.source_agent,
|
|
2580
2610
|
role = excluded.role,
|
|
2581
2611
|
content = excluded.content,
|
|
2582
2612
|
summary = excluded.summary,
|
|
@@ -2585,7 +2615,7 @@ export class SqliteStore {
|
|
|
2585
2615
|
visibility = excluded.visibility,
|
|
2586
2616
|
created_at = excluded.created_at,
|
|
2587
2617
|
updated_at = excluded.updated_at
|
|
2588
|
-
`).run(memory.id, memory.sourceChunkId, memory.sourceUserId, memory.role, memory.content, memory.summary, memory.kind, memory.groupId, memory.visibility, memory.createdAt, memory.updatedAt);
|
|
2618
|
+
`).run(memory.id, memory.sourceChunkId, memory.sourceUserId, memory.sourceAgent, memory.role, memory.content, memory.summary, memory.kind, memory.groupId, memory.visibility, memory.createdAt, memory.updatedAt);
|
|
2589
2619
|
}
|
|
2590
2620
|
|
|
2591
2621
|
getHubMemoryBySource(sourceUserId: string, sourceChunkId: string): HubMemoryRecord | null {
|
|
@@ -2751,7 +2781,7 @@ export class SqliteStore {
|
|
|
2751
2781
|
if (!sanitized) return [];
|
|
2752
2782
|
const rows = this.db.prepare(`
|
|
2753
2783
|
SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
|
|
2754
|
-
bm25(hub_memories_fts) as rank
|
|
2784
|
+
COALESCE(hm.source_agent, '') as source_agent, bm25(hub_memories_fts) as rank
|
|
2755
2785
|
FROM hub_memories_fts f
|
|
2756
2786
|
JOIN hub_memories hm ON hm.rowid = f.rowid
|
|
2757
2787
|
LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
|
|
@@ -2767,7 +2797,7 @@ export class SqliteStore {
|
|
|
2767
2797
|
getVisibleHubSearchHitByMemoryId(memoryId: string, userId: string): HubMemorySearchRow | null {
|
|
2768
2798
|
const row = this.db.prepare(`
|
|
2769
2799
|
SELECT hm.id, hm.content, hm.summary, hm.role, hm.created_at, hm.visibility, '' as group_name, hu.username as owner_name,
|
|
2770
|
-
0 as rank
|
|
2800
|
+
COALESCE(hm.source_agent, '') as source_agent, 0 as rank
|
|
2771
2801
|
FROM hub_memories hm
|
|
2772
2802
|
LEFT JOIN hub_users hu ON hu.id = hm.source_user_id
|
|
2773
2803
|
WHERE hm.id = ?
|
|
@@ -3261,6 +3291,7 @@ export interface HubMemoryRecord {
|
|
|
3261
3291
|
id: string;
|
|
3262
3292
|
sourceChunkId: string;
|
|
3263
3293
|
sourceUserId: string;
|
|
3294
|
+
sourceAgent: string;
|
|
3264
3295
|
role: string;
|
|
3265
3296
|
content: string;
|
|
3266
3297
|
summary: string;
|
|
@@ -3275,6 +3306,7 @@ interface HubMemoryRow {
|
|
|
3275
3306
|
id: string;
|
|
3276
3307
|
source_chunk_id: string;
|
|
3277
3308
|
source_user_id: string;
|
|
3309
|
+
source_agent: string;
|
|
3278
3310
|
role: string;
|
|
3279
3311
|
content: string;
|
|
3280
3312
|
summary: string;
|
|
@@ -3290,6 +3322,7 @@ function rowToHubMemory(row: HubMemoryRow): HubMemoryRecord {
|
|
|
3290
3322
|
id: row.id,
|
|
3291
3323
|
sourceChunkId: row.source_chunk_id,
|
|
3292
3324
|
sourceUserId: row.source_user_id,
|
|
3325
|
+
sourceAgent: row.source_agent || "",
|
|
3293
3326
|
role: row.role,
|
|
3294
3327
|
content: row.content,
|
|
3295
3328
|
summary: row.summary,
|
|
@@ -3310,6 +3343,7 @@ interface HubMemorySearchRow {
|
|
|
3310
3343
|
visibility: string;
|
|
3311
3344
|
group_name: string | null;
|
|
3312
3345
|
owner_name: string | null;
|
|
3346
|
+
source_agent: string;
|
|
3313
3347
|
rank: number;
|
|
3314
3348
|
}
|
|
3315
3349
|
|
package/src/types.ts
CHANGED
|
@@ -322,6 +322,8 @@ export interface MemosLocalConfig {
|
|
|
322
322
|
skillEvolution?: SkillEvolutionConfig;
|
|
323
323
|
telemetry?: TelemetryConfig;
|
|
324
324
|
sharing?: SharingConfig;
|
|
325
|
+
/** Hours of inactivity after which an active task is automatically finalized. 0 = disabled. Default 4. */
|
|
326
|
+
taskAutoFinalizeHours?: number;
|
|
325
327
|
}
|
|
326
328
|
|
|
327
329
|
// ─── Defaults ───
|
|
@@ -357,6 +359,7 @@ export const DEFAULTS = {
|
|
|
357
359
|
skillAutoRecallLimit: 2,
|
|
358
360
|
skillPreferUpgrade: true,
|
|
359
361
|
skillRedactSensitive: true,
|
|
362
|
+
taskAutoFinalizeHours: 4,
|
|
360
363
|
} as const;
|
|
361
364
|
|
|
362
365
|
// ─── Plugin Hooks (OpenClaw integration) ───
|