@clawmem-ai/clawmem 0.1.15 → 0.1.16
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/README.md +6 -2
- package/openclaw.plugin.json +29 -2
- package/package.json +4 -1
- package/skills/clawmem/SKILL.md +4 -2
- package/src/config.test.ts +3 -0
- package/src/config.ts +3 -0
- package/src/conversation.ts +217 -2
- package/src/memory.test.ts +33 -3
- package/src/memory.ts +206 -2
- package/src/runtime-env.ts +12 -0
- package/src/service.ts +344 -88
- package/src/state.test.ts +88 -0
- package/src/state.ts +139 -8
- package/src/types.ts +46 -2
- package/src/utils.ts +19 -0
package/src/service.ts
CHANGED
|
@@ -5,31 +5,34 @@ import { filterDirectCollaborators, listRepoAccessTeams, resolveOrgInvitationRol
|
|
|
5
5
|
import { ConversationMirror } from "./conversation.js";
|
|
6
6
|
import { GitHubIssueClient } from "./github-client.js";
|
|
7
7
|
import { KeyedAsyncQueue } from "./keyed-async-queue.js";
|
|
8
|
-
import { MemoryStore } from "./memory.js";
|
|
8
|
+
import { MemoryStore, mergeMemoryCandidates } from "./memory.js";
|
|
9
9
|
import { sanitizeRecallQueryInput } from "./recall-sanitize.js";
|
|
10
10
|
import { loadState, resolveStatePath, saveState } from "./state.js";
|
|
11
11
|
import { readTranscriptSnapshot } from "./transcript.js";
|
|
12
|
-
import type { BootstrapIdentityResponse, ClawMemPluginConfig, ClawMemResolvedRoute, PluginState, SessionMirrorState, TranscriptSnapshot } from "./types.js";
|
|
12
|
+
import type { BootstrapIdentityResponse, ClawMemPluginConfig, ClawMemResolvedRoute, PluginState, SessionDerivedState, SessionMirrorState, TranscriptSnapshot } from "./types.js";
|
|
13
13
|
import { buildAgentBootstrapRegistration, inferAgentIdFromTranscriptPath, normalizeAgentId, sessionScopeKey } from "./utils.js";
|
|
14
|
+
import { getOpenClawAgentIdFromEnv, getOpenClawHostVersionFromEnv } from "./runtime-env.js";
|
|
14
15
|
|
|
15
16
|
type TurnPayload = { sessionId?: string; sessionKey?: string; agentId?: string; messages: unknown[] };
|
|
16
17
|
type FinalizePayload = { sessionId?: string; sessionKey?: string; sessionFile?: string; agentId?: string; reason?: string; messages?: unknown[] };
|
|
17
18
|
type CollaborationPermission = "read" | "write" | "admin";
|
|
18
19
|
type CollaborationTeamRole = "member" | "maintainer";
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
+
const DERIVED_WORK_RECOVERY_DELAYS_MS = [5000, 30000, 120000] as const;
|
|
21
22
|
const MODERN_PROMPT_HOOK_MIN_HOST_VERSION = "2026.3.7";
|
|
22
23
|
type PromptHookMode = "modern" | "legacy";
|
|
23
24
|
|
|
24
25
|
class ClawMemService {
|
|
25
26
|
private readonly config: ClawMemPluginConfig;
|
|
26
|
-
private readonly
|
|
27
|
+
private readonly ioQueue = new KeyedAsyncQueue();
|
|
28
|
+
private readonly deriveQueue = new KeyedAsyncQueue();
|
|
29
|
+
private readonly repoWriteQueue = new KeyedAsyncQueue();
|
|
27
30
|
private readonly stateQueue = new KeyedAsyncQueue();
|
|
28
31
|
private readonly pending = new Set<Promise<unknown>>();
|
|
29
32
|
private readonly syncTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
30
|
-
private readonly
|
|
33
|
+
private readonly recoveryTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
31
34
|
private statePath = "";
|
|
32
|
-
private state: PluginState = { version:
|
|
35
|
+
private state: PluginState = { version: 3, sessions: {} };
|
|
33
36
|
private unsubTranscript?: () => void;
|
|
34
37
|
private loadPromise: Promise<void> | null = null;
|
|
35
38
|
private readonly configPromises = new Map<string, Promise<boolean>>();
|
|
@@ -59,9 +62,7 @@ class ClawMemService {
|
|
|
59
62
|
this.unsubTranscript = this.api.runtime.events.onSessionTranscriptUpdate((u) => {
|
|
60
63
|
void this.track(this.handleTranscript(u.sessionFile)).catch((e) => this.warn("transcript update", e));
|
|
61
64
|
});
|
|
62
|
-
|
|
63
|
-
this.scheduleRecentSessionMaintenance(agentId);
|
|
64
|
-
}
|
|
65
|
+
this.recoverDerivedWorkOnStart();
|
|
65
66
|
const configuredCount = Object.keys(this.config.agents).filter((agentId) => {
|
|
66
67
|
const route = resolveAgentRoute(this.config, agentId);
|
|
67
68
|
return isAgentConfigured(route) && hasDefaultRepo(route);
|
|
@@ -77,8 +78,8 @@ class ClawMemService {
|
|
|
77
78
|
this.unsubTranscript?.();
|
|
78
79
|
for (const t of this.syncTimers.values()) clearTimeout(t);
|
|
79
80
|
this.syncTimers.clear();
|
|
80
|
-
for (const t of this.
|
|
81
|
-
this.
|
|
81
|
+
for (const t of this.recoveryTimers.values()) clearTimeout(t);
|
|
82
|
+
this.recoveryTimers.clear();
|
|
82
83
|
await Promise.allSettled([...this.pending]);
|
|
83
84
|
},
|
|
84
85
|
});
|
|
@@ -342,12 +343,12 @@ class ClawMemService {
|
|
|
342
343
|
if ("error" in resolved) return toolText(resolved.error);
|
|
343
344
|
const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
|
|
344
345
|
const topics = Array.isArray(p.topics) ? p.topics.filter((topic): topic is string => typeof topic === "string" && topic.trim().length > 0) : undefined;
|
|
345
|
-
const result = await resolved.mem.store({
|
|
346
|
+
const result = await this.enqueueRepoWrite(this.repoWriteKey(resolved.route), () => resolved.mem.store({
|
|
346
347
|
...(title ? { title } : {}),
|
|
347
348
|
detail,
|
|
348
349
|
...(kind ? { kind } : {}),
|
|
349
350
|
...(topics && topics.length > 0 ? { topics } : {}),
|
|
350
|
-
});
|
|
351
|
+
}));
|
|
351
352
|
if (!result.created) return toolText(`Memory already exists in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
|
|
352
353
|
return toolText(`Stored memory in ${resolved.route.repo}.\n${renderMemoryBlock(result.memory)}`);
|
|
353
354
|
},
|
|
@@ -391,7 +392,10 @@ class ClawMemService {
|
|
|
391
392
|
if ("error" in resolved) return toolText(resolved.error);
|
|
392
393
|
let updated;
|
|
393
394
|
try {
|
|
394
|
-
updated = await
|
|
395
|
+
updated = await this.enqueueRepoWrite(
|
|
396
|
+
this.repoWriteKey(resolved.route),
|
|
397
|
+
() => resolved.mem.update(memoryId, { ...(title ? { title } : {}), ...(detail ? { detail } : {}), ...(kind !== undefined ? { kind } : {}), ...(topics !== undefined ? { topics } : {}) }),
|
|
398
|
+
);
|
|
395
399
|
} catch (error) {
|
|
396
400
|
return toolText(`Unable to update memory "${memoryId}": ${String(error)}`);
|
|
397
401
|
}
|
|
@@ -421,7 +425,7 @@ class ClawMemService {
|
|
|
421
425
|
const agentId = this.resolveToolAgentId(p.agentId);
|
|
422
426
|
const resolved = await this.requireToolRoute(agentId, p.repo);
|
|
423
427
|
if ("error" in resolved) return toolText(resolved.error);
|
|
424
|
-
const forgotten = await resolved.mem.forget(memoryId);
|
|
428
|
+
const forgotten = await this.enqueueRepoWrite(this.repoWriteKey(resolved.route), () => resolved.mem.forget(memoryId));
|
|
425
429
|
if (!forgotten) return toolText(`No active memory matched id "${memoryId}" in ${resolved.route.repo}.`);
|
|
426
430
|
return toolText(`Marked memory [${forgotten.memoryId}] stale in ${resolved.route.repo}: ${forgotten.detail}`);
|
|
427
431
|
},
|
|
@@ -1330,7 +1334,6 @@ class ClawMemService {
|
|
|
1330
1334
|
private async collectAutoRecallContext(event: unknown, agentId?: string): Promise<string | undefined> {
|
|
1331
1335
|
const routeAgentId = normalizeAgentId(agentId);
|
|
1332
1336
|
if (!(await this.ensureDefaultRepoConfigured(routeAgentId))) return undefined;
|
|
1333
|
-
this.scheduleRecentSessionMaintenance(routeAgentId);
|
|
1334
1337
|
const prompt = extractPromptTextForRecall(event);
|
|
1335
1338
|
if (typeof prompt !== "string" || prompt.trim().length < 5) return undefined;
|
|
1336
1339
|
try {
|
|
@@ -1357,7 +1360,7 @@ class ClawMemService {
|
|
|
1357
1360
|
const { conv } = this.getServices(agentId);
|
|
1358
1361
|
if (!conv.shouldMirror(snap.sessionId, snap.messages)) return;
|
|
1359
1362
|
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1360
|
-
await this.
|
|
1363
|
+
await this.enqueueSessionIo(sessionScopeKey(snap.sessionId, agentId), async () => {
|
|
1361
1364
|
const s = this.getOrCreate(snap.sessionId!, agentId);
|
|
1362
1365
|
s.sessionFile = sessionFile;
|
|
1363
1366
|
s.updatedAt = new Date().toISOString();
|
|
@@ -1373,7 +1376,7 @@ class ClawMemService {
|
|
|
1373
1376
|
if (prev) clearTimeout(prev);
|
|
1374
1377
|
const timer = setTimeout(() => {
|
|
1375
1378
|
this.syncTimers.delete(scopeKey);
|
|
1376
|
-
void this.track(this.
|
|
1379
|
+
void this.track(this.enqueueSessionIo(scopeKey, () => this.syncTurn(p))).catch((e) => this.warn("turn sync", e));
|
|
1377
1380
|
}, this.config.turnCommentDelayMs);
|
|
1378
1381
|
timer.unref?.();
|
|
1379
1382
|
this.syncTimers.set(scopeKey, timer);
|
|
@@ -1382,6 +1385,7 @@ class ClawMemService {
|
|
|
1382
1385
|
private async syncTurn(p: TurnPayload): Promise<void> {
|
|
1383
1386
|
if (!p.sessionId) return;
|
|
1384
1387
|
const agentId = normalizeAgentId(p.agentId);
|
|
1388
|
+
const scopeKey = sessionScopeKey(p.sessionId, agentId);
|
|
1385
1389
|
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1386
1390
|
const { conv } = this.getServices(agentId);
|
|
1387
1391
|
const s = this.getOrCreate(p.sessionId, agentId);
|
|
@@ -1392,8 +1396,9 @@ class ClawMemService {
|
|
|
1392
1396
|
await conv.syncLabels(s, snap, false);
|
|
1393
1397
|
const next = snap.messages.slice(s.lastMirroredCount);
|
|
1394
1398
|
if (next.length > 0) { const n = await conv.appendComments(s.issueNumber!, next); s.lastMirroredCount += n; s.turnCount += n; }
|
|
1399
|
+
this.markPostMirrorTasks(s);
|
|
1395
1400
|
await this.persistState();
|
|
1396
|
-
this.
|
|
1401
|
+
this.kickDerivedWork(scopeKey, agentId, "turn");
|
|
1397
1402
|
}
|
|
1398
1403
|
|
|
1399
1404
|
private enqueueFinalize(p: FinalizePayload): void {
|
|
@@ -1401,7 +1406,7 @@ class ClawMemService {
|
|
|
1401
1406
|
const scopeKey = sessionScopeKey(p.sessionId, p.agentId);
|
|
1402
1407
|
const prev = this.syncTimers.get(scopeKey);
|
|
1403
1408
|
if (prev) { clearTimeout(prev); this.syncTimers.delete(scopeKey); }
|
|
1404
|
-
void this.track(this.
|
|
1409
|
+
void this.track(this.enqueueSessionIo(scopeKey, () => this.finalize(p))).catch((e) => this.warn("finalize", e));
|
|
1405
1410
|
}
|
|
1406
1411
|
|
|
1407
1412
|
private async finalize(p: FinalizePayload): Promise<void> {
|
|
@@ -1423,16 +1428,26 @@ class ClawMemService {
|
|
|
1423
1428
|
if (next.length > 0) { const n = await conv.appendComments(s.issueNumber!, next); s.lastMirroredCount += n; s.turnCount += n; allOk = n === next.length; }
|
|
1424
1429
|
await conv.syncLabels(s, snap, true);
|
|
1425
1430
|
await conv.syncBody(s, snap, "pending", true);
|
|
1426
|
-
s.summaryStatus = "pending";
|
|
1427
1431
|
if (allOk) s.finalizedAt = new Date().toISOString();
|
|
1432
|
+
this.markPostMirrorTasks(s);
|
|
1433
|
+
this.markSummaryPending(s);
|
|
1428
1434
|
await this.persistState();
|
|
1429
|
-
this.
|
|
1435
|
+
this.kickDerivedWork(scopeKey, agentId, p.reason ?? "finalize");
|
|
1430
1436
|
}
|
|
1431
1437
|
|
|
1432
1438
|
// --- Infrastructure ---
|
|
1433
1439
|
|
|
1434
|
-
private
|
|
1435
|
-
return this.
|
|
1440
|
+
private enqueueSessionIo<T>(sessionId: string, task: () => Promise<T>): Promise<T> {
|
|
1441
|
+
return this.ioQueue.enqueue(sessionId, async () => { await this.ensureLoaded(); return task(); });
|
|
1442
|
+
}
|
|
1443
|
+
private enqueueSessionDerived<T>(sessionId: string, task: () => Promise<T>): Promise<T> {
|
|
1444
|
+
return this.deriveQueue.enqueue(sessionId, async () => { await this.ensureLoaded(); return task(); });
|
|
1445
|
+
}
|
|
1446
|
+
private enqueueRepoWrite<T>(repoKey: string, task: () => Promise<T>): Promise<T> {
|
|
1447
|
+
return this.repoWriteQueue.enqueue(repoKey, task);
|
|
1448
|
+
}
|
|
1449
|
+
private repoWriteKey(route: ClawMemResolvedRoute): string {
|
|
1450
|
+
return route.repo || route.defaultRepo || route.agentId;
|
|
1436
1451
|
}
|
|
1437
1452
|
private track<T>(promise: Promise<T>): Promise<T> {
|
|
1438
1453
|
this.pending.add(promise);
|
|
@@ -1453,12 +1468,90 @@ class ClawMemService {
|
|
|
1453
1468
|
agentId: normalizeAgentId(agentId),
|
|
1454
1469
|
lastMirroredCount: 0,
|
|
1455
1470
|
turnCount: 0,
|
|
1471
|
+
derived: {
|
|
1472
|
+
digest: { cursor: 0, status: "idle", attempt: 0 },
|
|
1473
|
+
summary: { basedOnCursor: 0, status: "idle" },
|
|
1474
|
+
memory: {
|
|
1475
|
+
extractCursor: 0,
|
|
1476
|
+
appliedCursor: 0,
|
|
1477
|
+
extractStatus: "idle",
|
|
1478
|
+
reconcileStatus: "idle",
|
|
1479
|
+
attempt: 0,
|
|
1480
|
+
pendingCandidates: [],
|
|
1481
|
+
},
|
|
1482
|
+
},
|
|
1456
1483
|
createdAt: now,
|
|
1457
1484
|
updatedAt: now,
|
|
1458
1485
|
};
|
|
1459
1486
|
this.state.sessions[scopeKey] = s;
|
|
1460
1487
|
return s;
|
|
1461
1488
|
}
|
|
1489
|
+
|
|
1490
|
+
private ensureDerived(session: SessionMirrorState): SessionDerivedState {
|
|
1491
|
+
if (!session.derived) {
|
|
1492
|
+
session.derived = {
|
|
1493
|
+
digest: { cursor: 0, status: "idle", attempt: 0 },
|
|
1494
|
+
summary: { basedOnCursor: 0, status: "idle" },
|
|
1495
|
+
memory: {
|
|
1496
|
+
extractCursor: 0,
|
|
1497
|
+
appliedCursor: session.lastMemorySyncCount ?? 0,
|
|
1498
|
+
extractStatus: "idle",
|
|
1499
|
+
reconcileStatus: "idle",
|
|
1500
|
+
attempt: 0,
|
|
1501
|
+
pendingCandidates: [],
|
|
1502
|
+
},
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
return session.derived;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
private syncLegacyTaskFields(session: SessionMirrorState): void {
|
|
1509
|
+
const derived = this.ensureDerived(session);
|
|
1510
|
+
session.summaryStatus = derived.summary.status === "complete" ? "complete" : session.finalizedAt ? "pending" : undefined;
|
|
1511
|
+
session.lastMemorySyncCount = derived.memory.appliedCursor;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
private hasMeaningfulTranscript(session: SessionMirrorState): boolean {
|
|
1515
|
+
return Math.max(session.lastMirroredCount, session.turnCount) >= 2;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
private needsDigest(session: SessionMirrorState): boolean {
|
|
1519
|
+
if (!this.hasMeaningfulTranscript(session)) return false;
|
|
1520
|
+
const derived = this.ensureDerived(session);
|
|
1521
|
+
return derived.digest.cursor < session.lastMirroredCount;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private needsFinalSummary(session: SessionMirrorState): boolean {
|
|
1525
|
+
if (!session.finalizedAt || !this.hasMeaningfulTranscript(session)) return false;
|
|
1526
|
+
const derived = this.ensureDerived(session);
|
|
1527
|
+
return derived.summary.status !== "complete" || derived.summary.basedOnCursor < session.lastMirroredCount;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
private needsMemoryExtract(session: SessionMirrorState): boolean {
|
|
1531
|
+
if (!this.hasMeaningfulTranscript(session)) return false;
|
|
1532
|
+
const derived = this.ensureDerived(session);
|
|
1533
|
+
return derived.memory.extractCursor < session.lastMirroredCount;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
private needsMemoryReconcile(session: SessionMirrorState): boolean {
|
|
1537
|
+
if (!this.hasMeaningfulTranscript(session)) return false;
|
|
1538
|
+
const derived = this.ensureDerived(session);
|
|
1539
|
+
return derived.memory.pendingCandidates.length > 0 || derived.memory.appliedCursor < derived.memory.extractCursor;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
private markPostMirrorTasks(session: SessionMirrorState): void {
|
|
1543
|
+
const derived = this.ensureDerived(session);
|
|
1544
|
+
if (this.needsDigest(session)) derived.digest.status = "pending";
|
|
1545
|
+
if (this.needsMemoryExtract(session)) derived.memory.extractStatus = "pending";
|
|
1546
|
+
if (this.needsMemoryReconcile(session)) derived.memory.reconcileStatus = "pending";
|
|
1547
|
+
this.syncLegacyTaskFields(session);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
private markSummaryPending(session: SessionMirrorState): void {
|
|
1551
|
+
const derived = this.ensureDerived(session);
|
|
1552
|
+
derived.summary.status = "pending";
|
|
1553
|
+
this.syncLegacyTaskFields(session);
|
|
1554
|
+
}
|
|
1462
1555
|
private resolveTranscriptAgentId(sessionId: string, sessionFile: string): string | null {
|
|
1463
1556
|
const fromPath = inferAgentIdFromTranscriptPath(sessionFile);
|
|
1464
1557
|
if (fromPath) return fromPath;
|
|
@@ -1583,109 +1676,277 @@ class ClawMemService {
|
|
|
1583
1676
|
},
|
|
1584
1677
|
});
|
|
1585
1678
|
}
|
|
1586
|
-
private
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1679
|
+
private clearRecoveryTimer(scopeKey: string): void {
|
|
1680
|
+
const prev = this.recoveryTimers.get(scopeKey);
|
|
1681
|
+
if (!prev) return;
|
|
1682
|
+
clearTimeout(prev);
|
|
1683
|
+
this.recoveryTimers.delete(scopeKey);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
private recoverDerivedWorkOnStart(): void {
|
|
1687
|
+
const recoverableSessions = Object.values(this.state.sessions)
|
|
1688
|
+
.filter((session) => this.sessionNeedsDerivedWork(session))
|
|
1689
|
+
.sort((a, b) => Date.parse(b.updatedAt ?? b.createdAt ?? "") - Date.parse(a.updatedAt ?? a.createdAt ?? ""));
|
|
1690
|
+
for (const session of recoverableSessions) {
|
|
1691
|
+
this.scheduleDerivedRecovery(sessionScopeKey(session.sessionId, session.agentId), normalizeAgentId(session.agentId), {
|
|
1595
1692
|
delayMs: 0,
|
|
1693
|
+
attempt: 0,
|
|
1694
|
+
reason: "startup-recovery",
|
|
1596
1695
|
});
|
|
1597
|
-
break;
|
|
1598
1696
|
}
|
|
1599
1697
|
}
|
|
1600
1698
|
|
|
1601
|
-
private
|
|
1699
|
+
private kickDerivedWork(scopeKey: string, agentId: string, reason: string): void {
|
|
1700
|
+
this.clearRecoveryTimer(scopeKey);
|
|
1701
|
+
void this.track(this.enqueueSessionDerived(scopeKey, () => this.runSessionDerivedWork(scopeKey, agentId, 0, reason)))
|
|
1702
|
+
.catch((error) => this.warn(`derived work for ${scopeKey}`, error));
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
private scheduleDerivedRecovery(
|
|
1602
1706
|
scopeKey: string,
|
|
1603
1707
|
agentId: string,
|
|
1604
1708
|
options: { delayMs?: number; attempt?: number; reason?: string } = {},
|
|
1605
1709
|
): void {
|
|
1606
|
-
|
|
1607
|
-
if (prev) clearTimeout(prev);
|
|
1710
|
+
this.clearRecoveryTimer(scopeKey);
|
|
1608
1711
|
const delayMs = Math.max(0, options.delayMs ?? 0);
|
|
1609
1712
|
const attempt = Math.max(0, options.attempt ?? 0);
|
|
1610
|
-
const reason = options.reason ?? "scheduled";
|
|
1713
|
+
const reason = options.reason ?? "scheduled-recovery";
|
|
1611
1714
|
const timer = setTimeout(() => {
|
|
1612
|
-
this.
|
|
1613
|
-
void this.track(this.
|
|
1614
|
-
.catch((error) => this.warn(`
|
|
1715
|
+
this.recoveryTimers.delete(scopeKey);
|
|
1716
|
+
void this.track(this.enqueueSessionDerived(scopeKey, () => this.runSessionDerivedWork(scopeKey, agentId, attempt, reason)))
|
|
1717
|
+
.catch((error) => this.warn(`derived recovery for ${scopeKey}`, error));
|
|
1615
1718
|
}, delayMs);
|
|
1616
1719
|
timer.unref?.();
|
|
1617
|
-
this.
|
|
1720
|
+
this.recoveryTimers.set(scopeKey, timer);
|
|
1618
1721
|
}
|
|
1619
1722
|
|
|
1620
|
-
private async
|
|
1723
|
+
private async runSessionDerivedWork(scopeKey: string, agentId: string, attempt: number, reason: string): Promise<void> {
|
|
1621
1724
|
const session = this.state.sessions[scopeKey];
|
|
1622
|
-
if (!session || !this.
|
|
1725
|
+
if (!session || !this.sessionNeedsDerivedWork(session)) return;
|
|
1623
1726
|
if (!(await this.ensureDefaultRepoConfigured(agentId))) return;
|
|
1624
|
-
const { conv, mem } = this.getServices(agentId);
|
|
1727
|
+
const { route, conv, mem, client } = this.getServices(agentId);
|
|
1625
1728
|
const snap = await conv.loadSnapshot(session, []);
|
|
1626
1729
|
if (!conv.shouldMirror(session.sessionId, snap.messages) || snap.messages.length === 0) return;
|
|
1627
|
-
|
|
1730
|
+
|
|
1628
1731
|
let retryNeeded = false;
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1732
|
+
const derived = this.ensureDerived(session);
|
|
1733
|
+
const updateLegacyAndPersist = async (): Promise<void> => {
|
|
1734
|
+
this.syncLegacyTaskFields(session);
|
|
1735
|
+
await this.persistState();
|
|
1736
|
+
};
|
|
1737
|
+
const mirroredCount = Math.min(session.lastMirroredCount, snap.messages.length);
|
|
1738
|
+
if (mirroredCount <= 0) return;
|
|
1739
|
+
const canCombineDeltaDerivation = this.needsDigest(session)
|
|
1740
|
+
&& this.needsMemoryExtract(session)
|
|
1741
|
+
&& derived.digest.cursor === derived.memory.extractCursor;
|
|
1742
|
+
|
|
1743
|
+
if (canCombineDeltaDerivation) {
|
|
1744
|
+
const deriveTarget = mirroredCount;
|
|
1745
|
+
const deriveFromCursor = Math.min(derived.digest.cursor, deriveTarget);
|
|
1746
|
+
const deriveSnapshot: TranscriptSnapshot = { ...snap, messages: snap.messages.slice(0, deriveTarget) };
|
|
1747
|
+
const startedAt = new Date().toISOString();
|
|
1748
|
+
derived.digest.status = "running";
|
|
1749
|
+
derived.digest.updatedAt = startedAt;
|
|
1750
|
+
derived.memory.extractStatus = "running";
|
|
1751
|
+
derived.memory.updatedAt = startedAt;
|
|
1752
|
+
await updateLegacyAndPersist();
|
|
1753
|
+
try {
|
|
1754
|
+
const result = await conv.deriveDelta(session, deriveSnapshot, deriveFromCursor, derived.digest.text);
|
|
1755
|
+
derived.digest.text = result.digest.trim();
|
|
1756
|
+
derived.digest.title = result.title?.trim() || derived.digest.title;
|
|
1757
|
+
derived.digest.cursor = deriveTarget;
|
|
1758
|
+
derived.digest.status = deriveTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1759
|
+
derived.digest.attempt = 0;
|
|
1760
|
+
derived.digest.lastError = undefined;
|
|
1761
|
+
derived.digest.updatedAt = new Date().toISOString();
|
|
1762
|
+
if (result.title?.trim() && session.issueNumber) {
|
|
1763
|
+
await client.updateIssue(session.issueNumber, { title: result.title.trim() });
|
|
1764
|
+
session.issueTitle = result.title.trim();
|
|
1765
|
+
session.titleSource = "digest";
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
derived.memory.pendingCandidates = mergeMemoryCandidates(derived.memory.pendingCandidates, result.candidates);
|
|
1769
|
+
derived.memory.extractCursor = deriveTarget;
|
|
1770
|
+
derived.memory.extractStatus = deriveTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1771
|
+
derived.memory.attempt = 0;
|
|
1772
|
+
derived.memory.lastError = undefined;
|
|
1773
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1774
|
+
if (derived.memory.pendingCandidates.length === 0) {
|
|
1775
|
+
derived.memory.appliedCursor = Math.max(derived.memory.appliedCursor, deriveTarget);
|
|
1776
|
+
derived.memory.reconcileStatus = "complete";
|
|
1777
|
+
} else {
|
|
1778
|
+
derived.memory.reconcileStatus = "pending";
|
|
1779
|
+
}
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
const failedAt = new Date().toISOString();
|
|
1782
|
+
derived.digest.status = "error";
|
|
1783
|
+
derived.digest.attempt += 1;
|
|
1784
|
+
derived.digest.lastError = String(error);
|
|
1785
|
+
derived.digest.updatedAt = failedAt;
|
|
1786
|
+
derived.memory.extractStatus = "error";
|
|
1787
|
+
derived.memory.attempt += 1;
|
|
1788
|
+
derived.memory.lastError = String(error);
|
|
1789
|
+
derived.memory.updatedAt = failedAt;
|
|
1790
|
+
retryNeeded = true;
|
|
1791
|
+
this.warn(`background derive delta for ${session.sessionId}`, error);
|
|
1792
|
+
}
|
|
1793
|
+
await updateLegacyAndPersist();
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
if (!canCombineDeltaDerivation && this.needsDigest(session)) {
|
|
1797
|
+
const digestTarget = mirroredCount;
|
|
1798
|
+
const digestFromCursor = Math.min(derived.digest.cursor, digestTarget);
|
|
1799
|
+
const digestSnapshot: TranscriptSnapshot = { ...snap, messages: snap.messages.slice(0, digestTarget) };
|
|
1800
|
+
derived.digest.status = "running";
|
|
1801
|
+
derived.digest.updatedAt = new Date().toISOString();
|
|
1802
|
+
await updateLegacyAndPersist();
|
|
1803
|
+
try {
|
|
1804
|
+
const result = await conv.generateRollingDigest(session, digestSnapshot, digestFromCursor, derived.digest.text);
|
|
1805
|
+
derived.digest.text = result.digest.trim();
|
|
1806
|
+
derived.digest.title = result.title?.trim() || derived.digest.title;
|
|
1807
|
+
derived.digest.cursor = digestTarget;
|
|
1808
|
+
derived.digest.status = digestTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1809
|
+
derived.digest.attempt = 0;
|
|
1810
|
+
derived.digest.lastError = undefined;
|
|
1811
|
+
derived.digest.updatedAt = new Date().toISOString();
|
|
1812
|
+
if (result.title?.trim() && session.issueNumber) {
|
|
1813
|
+
await client.updateIssue(session.issueNumber, { title: result.title.trim() });
|
|
1814
|
+
session.issueTitle = result.title.trim();
|
|
1815
|
+
session.titleSource = "digest";
|
|
1816
|
+
}
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
derived.digest.status = "error";
|
|
1819
|
+
derived.digest.attempt += 1;
|
|
1820
|
+
derived.digest.lastError = String(error);
|
|
1821
|
+
derived.digest.updatedAt = new Date().toISOString();
|
|
1822
|
+
retryNeeded = true;
|
|
1823
|
+
this.warn(`background digest sync for ${session.sessionId}`, error);
|
|
1824
|
+
}
|
|
1825
|
+
await updateLegacyAndPersist();
|
|
1632
1826
|
}
|
|
1633
|
-
|
|
1827
|
+
|
|
1828
|
+
if (this.needsFinalSummary(session) && derived.digest.cursor >= session.lastMirroredCount) {
|
|
1829
|
+
const summaryTarget = Math.min(session.lastMirroredCount, snap.messages.length);
|
|
1830
|
+
const summarySnapshot: TranscriptSnapshot = { ...snap, messages: snap.messages.slice(0, summaryTarget) };
|
|
1831
|
+
derived.summary.status = "running";
|
|
1832
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1833
|
+
await updateLegacyAndPersist();
|
|
1634
1834
|
try {
|
|
1635
|
-
const result = await conv.
|
|
1636
|
-
await conv.syncLabels(session,
|
|
1637
|
-
await conv.syncBody(session,
|
|
1638
|
-
|
|
1835
|
+
const result = await conv.generateFinalSummaryFromDigest(session, summarySnapshot, derived.digest.text ?? "");
|
|
1836
|
+
await conv.syncLabels(session, summarySnapshot, true);
|
|
1837
|
+
await conv.syncBody(session, summarySnapshot, result.summary, true, result.title);
|
|
1838
|
+
derived.summary.text = result.summary;
|
|
1839
|
+
derived.summary.status = summaryTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1840
|
+
derived.summary.basedOnCursor = summaryTarget;
|
|
1841
|
+
derived.summary.lastError = undefined;
|
|
1842
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1639
1843
|
if (result.title?.trim()) {
|
|
1640
1844
|
session.issueTitle = result.title.trim();
|
|
1641
1845
|
session.titleSource = "llm";
|
|
1642
1846
|
}
|
|
1643
1847
|
this.maybeAutoNameRepo(agentId, result.summary, result.title);
|
|
1644
|
-
changed = true;
|
|
1645
1848
|
} catch (error) {
|
|
1849
|
+
derived.summary.status = "error";
|
|
1850
|
+
derived.summary.lastError = String(error);
|
|
1851
|
+
derived.summary.updatedAt = new Date().toISOString();
|
|
1646
1852
|
retryNeeded = true;
|
|
1647
1853
|
this.warn(`background summary sync for ${session.sessionId}`, error);
|
|
1648
1854
|
}
|
|
1855
|
+
await updateLegacyAndPersist();
|
|
1649
1856
|
}
|
|
1650
|
-
|
|
1857
|
+
|
|
1858
|
+
if (!canCombineDeltaDerivation && this.needsMemoryExtract(session)) {
|
|
1859
|
+
const extractTarget = Math.min(session.lastMirroredCount, snap.messages.length);
|
|
1860
|
+
const extractFromCursor = Math.min(derived.memory.extractCursor, extractTarget);
|
|
1861
|
+
const extractSnapshot: TranscriptSnapshot = { ...snap, messages: snap.messages.slice(0, extractTarget) };
|
|
1862
|
+
derived.memory.extractStatus = "running";
|
|
1863
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1864
|
+
await updateLegacyAndPersist();
|
|
1651
1865
|
try {
|
|
1652
|
-
await
|
|
1653
|
-
|
|
1866
|
+
const candidates = await mem.extractCandidates(session, extractSnapshot, extractFromCursor, derived.digest.text);
|
|
1867
|
+
derived.memory.pendingCandidates = mergeMemoryCandidates(derived.memory.pendingCandidates, candidates);
|
|
1868
|
+
derived.memory.extractCursor = extractTarget;
|
|
1869
|
+
derived.memory.extractStatus = extractTarget < session.lastMirroredCount ? "pending" : "complete";
|
|
1870
|
+
derived.memory.attempt = 0;
|
|
1871
|
+
derived.memory.lastError = undefined;
|
|
1872
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1873
|
+
if (derived.memory.pendingCandidates.length === 0) {
|
|
1874
|
+
derived.memory.appliedCursor = Math.max(derived.memory.appliedCursor, extractTarget);
|
|
1875
|
+
derived.memory.reconcileStatus = "complete";
|
|
1876
|
+
} else {
|
|
1877
|
+
derived.memory.reconcileStatus = "pending";
|
|
1878
|
+
}
|
|
1654
1879
|
} catch (error) {
|
|
1880
|
+
derived.memory.extractStatus = "error";
|
|
1881
|
+
derived.memory.attempt += 1;
|
|
1882
|
+
derived.memory.lastError = String(error);
|
|
1883
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1655
1884
|
retryNeeded = true;
|
|
1656
|
-
this.warn(`background
|
|
1885
|
+
this.warn(`background memory extract for ${session.sessionId}`, error);
|
|
1657
1886
|
}
|
|
1887
|
+
await updateLegacyAndPersist();
|
|
1658
1888
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
if (
|
|
1662
|
-
|
|
1663
|
-
|
|
1889
|
+
|
|
1890
|
+
if (this.needsMemoryReconcile(session)) {
|
|
1891
|
+
if (derived.memory.pendingCandidates.length === 0) {
|
|
1892
|
+
derived.memory.appliedCursor = derived.memory.extractCursor;
|
|
1893
|
+
derived.memory.reconcileStatus = "complete";
|
|
1894
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1895
|
+
await updateLegacyAndPersist();
|
|
1664
1896
|
} else {
|
|
1665
|
-
|
|
1897
|
+
const candidates = mergeMemoryCandidates([], derived.memory.pendingCandidates);
|
|
1898
|
+
derived.memory.reconcileStatus = "running";
|
|
1899
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1900
|
+
await updateLegacyAndPersist();
|
|
1901
|
+
try {
|
|
1902
|
+
const decision = await mem.reconcileCandidates(session, candidates);
|
|
1903
|
+
const { savedCount, staledCount } = await this.enqueueRepoWrite(
|
|
1904
|
+
this.repoWriteKey(route),
|
|
1905
|
+
() => mem.applyReconciledDecision(decision),
|
|
1906
|
+
);
|
|
1907
|
+
if (savedCount > 0 || staledCount > 0) {
|
|
1908
|
+
this.api.logger.info?.(
|
|
1909
|
+
`clawmem: synced memories for ${session.sessionId} (saved=${savedCount}, stale=${staledCount})`,
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
const consumed = new Set(candidates.map((candidate) => candidate.candidateId));
|
|
1913
|
+
derived.memory.pendingCandidates = derived.memory.pendingCandidates.filter((candidate) => !consumed.has(candidate.candidateId));
|
|
1914
|
+
derived.memory.appliedCursor = derived.memory.extractCursor;
|
|
1915
|
+
derived.memory.reconcileStatus = derived.memory.pendingCandidates.length > 0 ? "pending" : "complete";
|
|
1916
|
+
derived.memory.attempt = 0;
|
|
1917
|
+
derived.memory.lastError = undefined;
|
|
1918
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
derived.memory.reconcileStatus = "error";
|
|
1921
|
+
derived.memory.attempt += 1;
|
|
1922
|
+
derived.memory.lastError = String(error);
|
|
1923
|
+
derived.memory.updatedAt = new Date().toISOString();
|
|
1924
|
+
retryNeeded = true;
|
|
1925
|
+
this.warn(`background memory reconcile for ${session.sessionId}`, error);
|
|
1926
|
+
}
|
|
1927
|
+
await updateLegacyAndPersist();
|
|
1666
1928
|
}
|
|
1667
1929
|
}
|
|
1668
|
-
|
|
1669
|
-
if (
|
|
1670
|
-
|
|
1671
|
-
const delayMs = SESSION_MAINTENANCE_RETRY_DELAYS_MS[attempt];
|
|
1930
|
+
|
|
1931
|
+
if (retryNeeded && this.sessionNeedsDerivedWork(session)) {
|
|
1932
|
+
const delayMs = DERIVED_WORK_RECOVERY_DELAYS_MS[Math.min(attempt, DERIVED_WORK_RECOVERY_DELAYS_MS.length - 1)] ?? 120000;
|
|
1672
1933
|
this.api.logger.warn?.(
|
|
1673
|
-
`clawmem:
|
|
1934
|
+
`clawmem: derived work incomplete for ${session.sessionId}; retrying in ${Math.round(delayMs / 1000)}s (${reason})`,
|
|
1674
1935
|
);
|
|
1675
|
-
this.
|
|
1936
|
+
this.scheduleDerivedRecovery(scopeKey, agentId, { delayMs, attempt: attempt + 1, reason: "retry" });
|
|
1676
1937
|
return;
|
|
1677
1938
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1939
|
+
|
|
1940
|
+
if (this.sessionNeedsDerivedWork(session)) {
|
|
1941
|
+
this.kickDerivedWork(scopeKey, agentId, "follow-up");
|
|
1942
|
+
}
|
|
1681
1943
|
}
|
|
1682
1944
|
|
|
1683
|
-
private
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
return (session.lastMemorySyncCount ?? 0) < session.lastMirroredCount;
|
|
1945
|
+
private sessionNeedsDerivedWork(session: SessionMirrorState): boolean {
|
|
1946
|
+
return this.needsDigest(session)
|
|
1947
|
+
|| this.needsFinalSummary(session)
|
|
1948
|
+
|| this.needsMemoryExtract(session)
|
|
1949
|
+
|| this.needsMemoryReconcile(session);
|
|
1689
1950
|
}
|
|
1690
1951
|
|
|
1691
1952
|
private getServices(agentId?: string, repo?: string): { route: ClawMemResolvedRoute; conv: ConversationMirror; mem: MemoryStore; client: GitHubIssueClient } {
|
|
@@ -1699,7 +1960,7 @@ class ClawMemService {
|
|
|
1699
1960
|
};
|
|
1700
1961
|
}
|
|
1701
1962
|
private resolveToolAgentId(agentId: unknown): string {
|
|
1702
|
-
return normalizeAgentId(typeof agentId === "string" && agentId.trim() ? agentId :
|
|
1963
|
+
return normalizeAgentId(typeof agentId === "string" && agentId.trim() ? agentId : getOpenClawAgentIdFromEnv());
|
|
1703
1964
|
}
|
|
1704
1965
|
private resolveToolRepo(repo: unknown): { repo?: string; error?: string } {
|
|
1705
1966
|
if (repo === undefined || repo === null || repo === "") return {};
|
|
@@ -1957,13 +2218,8 @@ export function resolvePromptHookMode(api: Pick<OpenClawPluginApi, "runtime">):
|
|
|
1957
2218
|
export function resolveOpenClawHostVersion(api: Pick<OpenClawPluginApi, "runtime">): string | undefined {
|
|
1958
2219
|
const runtimeVersion = typeof api.runtime?.version === "string" ? api.runtime.version.trim() : "";
|
|
1959
2220
|
if (isUsableOpenClawVersion(runtimeVersion)) return runtimeVersion;
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
process.env.OPENCLAW_SERVICE_VERSION,
|
|
1963
|
-
]) {
|
|
1964
|
-
const trimmed = candidate?.trim();
|
|
1965
|
-
if (isUsableOpenClawVersion(trimmed)) return trimmed;
|
|
1966
|
-
}
|
|
2221
|
+
const envVersion = getOpenClawHostVersionFromEnv();
|
|
2222
|
+
if (isUsableOpenClawVersion(envVersion)) return envVersion;
|
|
1967
2223
|
return undefined;
|
|
1968
2224
|
}
|
|
1969
2225
|
|