@clawmem-ai/clawmem 0.1.16 → 0.1.18

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/src/state.test.ts CHANGED
@@ -36,13 +36,10 @@ async function testMigratesLegacyV2State(): Promise<void> {
36
36
  }, async (filePath) => {
37
37
  const state = await loadState(filePath);
38
38
  const session = state.sessions["main:s-1"];
39
- assert(state.version === 3, "expected state version 3 after migration");
39
+ assert(state.version === 4, "expected state version 4 after migration");
40
40
  assert(Boolean(session), "expected migrated session to exist");
41
- assert(session?.derived?.digest.cursor === 0, "expected legacy sessions to rebuild digest from cursor 0");
42
- assert(session?.derived?.digest.status === "pending", "expected digest to become pending after migration");
43
- assert(session?.derived?.summary.status === "pending", "expected finalized legacy sessions to keep summary pending");
44
- assert(session?.derived?.memory.appliedCursor === 4, "expected legacy memory sync cursor to migrate into appliedCursor");
45
- assert(session?.lastMemorySyncCount === 4, "expected compatibility field to mirror appliedCursor");
41
+ assert(session?.derived?.summary.status === "error", "expected finalized legacy sessions without a final summary to surface as needing manual attention");
42
+ assert(session?.derived?.memory.capturedCursor === 4, "expected legacy memory sync cursor to migrate into capturedCursor");
46
43
  });
47
44
  }
48
45
 
@@ -56,17 +53,12 @@ async function testNormalizesRunningTaskStates(): Promise<void> {
56
53
  lastMirroredCount: 3,
57
54
  turnCount: 3,
58
55
  derived: {
59
- digest: { cursor: 1, status: "running", attempt: 2, text: "digest" },
60
56
  summary: { basedOnCursor: 0, status: "running" },
61
57
  memory: {
62
58
  extractCursor: 1,
63
59
  appliedCursor: 0,
64
60
  extractStatus: "running",
65
61
  reconcileStatus: "running",
66
- attempt: 1,
67
- pendingCandidates: [
68
- { candidateId: "abc", detail: "Remember Redis is atomic with Lua." },
69
- ],
70
62
  },
71
63
  },
72
64
  },
@@ -74,15 +66,54 @@ async function testNormalizesRunningTaskStates(): Promise<void> {
74
66
  }, async (filePath) => {
75
67
  const state = await loadState(filePath);
76
68
  const session = state.sessions["main:s-2"];
77
- assert(session?.derived?.digest.status === "pending", "expected running digest tasks to normalize to pending on load");
78
- assert(session?.derived?.summary.status === "pending", "expected running summary tasks to normalize to pending on load");
79
- assert(session?.derived?.memory.extractStatus === "pending", "expected running memory extract tasks to normalize to pending");
80
- assert(session?.derived?.memory.reconcileStatus === "pending", "expected running memory reconcile tasks to normalize to pending");
81
- assert(session?.derived?.memory.pendingCandidates.length === 1, "expected pending candidates to survive state reload");
69
+ assert(session?.derived?.summary.status === "idle", "expected running summary tasks to normalize to idle on load");
70
+ assert(session?.derived?.memory.status === "idle", "expected running memory tasks to normalize to idle on load");
71
+ assert(session?.derived?.memory.capturedCursor === 0, "expected captured cursor to preserve the applied progress");
72
+ });
73
+ }
74
+
75
+ async function testPreservesCachedFinalArtifacts(): Promise<void> {
76
+ await withTempStateFile({
77
+ version: 4,
78
+ sessions: {
79
+ "main:s-3": {
80
+ sessionId: "s-3",
81
+ agentId: "main",
82
+ lastMirroredCount: 5,
83
+ turnCount: 5,
84
+ derived: {
85
+ summary: {
86
+ basedOnCursor: 5,
87
+ status: "idle",
88
+ text: "Recovered summary",
89
+ title: "Recovered title",
90
+ },
91
+ memory: {
92
+ capturedCursor: 0,
93
+ status: "error",
94
+ candidates: [
95
+ {
96
+ candidateId: "cand-1",
97
+ detail: "Store this durable fact.",
98
+ kind: "lesson",
99
+ topics: ["redis"],
100
+ },
101
+ ],
102
+ },
103
+ },
104
+ },
105
+ },
106
+ }, async (filePath) => {
107
+ const state = await loadState(filePath);
108
+ const session = state.sessions["main:s-3"];
109
+ assert(session?.derived?.summary.title === "Recovered title", "expected cached finalize title to survive state load");
110
+ assert(session?.derived?.memory.candidates?.length === 1, "expected cached memory candidates to survive state load");
111
+ assert(session?.derived?.memory.candidates?.[0]?.detail === "Store this durable fact.", "expected cached candidate detail to survive state load");
82
112
  });
83
113
  }
84
114
 
85
115
  await testMigratesLegacyV2State();
86
116
  await testNormalizesRunningTaskStates();
117
+ await testPreservesCachedFinalArtifacts();
87
118
 
88
119
  console.log("state tests passed");
package/src/state.ts CHANGED
@@ -4,7 +4,7 @@ import type { MemoryCandidate, PluginState, SessionDerivedState, SessionMirrorSt
4
4
  import { normalizeAgentId, sessionScopeKey } from "./utils.js";
5
5
 
6
6
  const EMPTY_STATE: PluginState = {
7
- version: 3,
7
+ version: 4,
8
8
  sessions: {},
9
9
  };
10
10
 
@@ -38,10 +38,9 @@ function sanitizeState(value: unknown): PluginState {
38
38
  return structuredClone(EMPTY_STATE);
39
39
  }
40
40
  const raw = value as Record<string, unknown>;
41
- const sessions =
42
- raw.sessions && typeof raw.sessions === "object"
43
- ? (raw.sessions as Record<string, unknown>)
44
- : {};
41
+ const sessions = raw.sessions && typeof raw.sessions === "object"
42
+ ? (raw.sessions as Record<string, unknown>)
43
+ : {};
45
44
  const migrations: Record<string, string> = {};
46
45
  if (raw.migrations && typeof raw.migrations === "object") {
47
46
  for (const [k, v] of Object.entries(raw.migrations as Record<string, unknown>)) {
@@ -50,30 +49,19 @@ function sanitizeState(value: unknown): PluginState {
50
49
  }
51
50
  }
52
51
  const out: PluginState = {
53
- version: 3,
52
+ version: 4,
54
53
  sessions: {},
55
54
  ...(Object.keys(migrations).length > 0 ? { migrations } : {}),
56
55
  };
57
56
  for (const [storedKey, sessionValue] of Object.entries(sessions)) {
58
- if (!sessionValue || typeof sessionValue !== "object" || !storedKey.trim()) {
59
- continue;
60
- }
57
+ if (!sessionValue || typeof sessionValue !== "object" || !storedKey.trim()) continue;
61
58
  const rawSession = sessionValue as Record<string, unknown>;
62
59
  const sessionId = readString(rawSession.sessionId) ?? storedKey.trim();
63
- if (!sessionId) {
64
- continue;
65
- }
60
+ if (!sessionId) continue;
66
61
  const agentId = normalizeAgentId(readString(rawSession.agentId));
67
62
  const lastMirroredCount = readNumber(rawSession.lastMirroredCount) ?? 0;
68
63
  const finalizedAt = readString(rawSession.finalizedAt);
69
- const summaryStatus = readEnum(rawSession.summaryStatus, ["pending", "complete"]);
70
- const lastMemorySyncCount = readNumber(rawSession.lastMemorySyncCount);
71
- const derived = sanitizeDerivedState(rawSession.derived, {
72
- lastMirroredCount,
73
- finalizedAt,
74
- summaryStatus,
75
- lastMemorySyncCount,
76
- });
64
+ const derived = sanitizeDerivedState(rawSession, lastMirroredCount, finalizedAt);
77
65
  out.sessions[sessionScopeKey(sessionId, agentId)] = {
78
66
  sessionId,
79
67
  sessionKey: readString(rawSession.sessionKey),
@@ -81,14 +69,11 @@ function sanitizeState(value: unknown): PluginState {
81
69
  agentId,
82
70
  issueNumber: readNumber(rawSession.issueNumber),
83
71
  issueTitle: readString(rawSession.issueTitle),
84
- titleSource: readEnum(rawSession.titleSource, ["placeholder", "digest", "llm"]),
72
+ titleSource: readTitleSource(rawSession.titleSource),
85
73
  lastMirroredCount,
86
74
  turnCount: readNumber(rawSession.turnCount) ?? 0,
87
- lastMemorySyncCount: derived.memory.appliedCursor,
88
- summaryStatus: derived.summary.status === "complete" ? "complete" : finalizedAt ? "pending" : undefined,
89
75
  finalizedAt,
90
76
  lastSummaryHash: readString(rawSession.lastSummaryHash),
91
- lastTurnHash: readString(rawSession.lastTurnHash),
92
77
  derived,
93
78
  createdAt: readString(rawSession.createdAt),
94
79
  updatedAt: readString(rawSession.updatedAt),
@@ -98,92 +83,62 @@ function sanitizeState(value: unknown): PluginState {
98
83
  }
99
84
 
100
85
  function sanitizeDerivedState(
101
- value: unknown,
102
- fallback: {
103
- lastMirroredCount: number;
104
- finalizedAt?: string;
105
- summaryStatus?: "pending" | "complete";
106
- lastMemorySyncCount?: number;
107
- },
86
+ rawSession: Record<string, unknown>,
87
+ lastMirroredCount: number,
88
+ finalizedAt?: string,
108
89
  ): SessionDerivedState {
109
- if (!value || typeof value !== "object") {
110
- return migrateDerivedState(fallback);
111
- }
112
- const record = value as Record<string, unknown>;
113
- const digest = asRecord(record.digest);
114
- const summary = asRecord(record.summary);
115
- const memory = asRecord(record.memory);
116
- const lastMirroredCount = fallback.lastMirroredCount;
117
- const extractCursor = clampCursor(readNumber(memory?.extractCursor), lastMirroredCount);
118
- const appliedCursor = clampCursor(readNumber(memory?.appliedCursor), extractCursor);
119
- return {
120
- digest: {
121
- cursor: clampCursor(readNumber(digest?.cursor), lastMirroredCount),
122
- status: readTaskStatus(digest?.status, lastMirroredCount > 0 ? "pending" : "idle"),
123
- attempt: readNumber(digest?.attempt) ?? 0,
124
- text: readString(digest?.text),
125
- title: readString(digest?.title),
126
- lastError: readString(digest?.lastError),
127
- updatedAt: readString(digest?.updatedAt),
128
- },
129
- summary: {
130
- basedOnCursor: clampCursor(readNumber(summary?.basedOnCursor), lastMirroredCount),
131
- status: readTaskStatus(summary?.status, fallback.finalizedAt ? "pending" : "idle"),
132
- text: readString(summary?.text),
133
- lastError: readString(summary?.lastError),
134
- updatedAt: readString(summary?.updatedAt),
135
- },
136
- memory: {
137
- extractCursor,
138
- appliedCursor,
139
- extractStatus: readTaskStatus(memory?.extractStatus, extractCursor < lastMirroredCount ? "pending" : "idle"),
140
- reconcileStatus: readTaskStatus(memory?.reconcileStatus, appliedCursor < extractCursor ? "pending" : "idle"),
141
- attempt: readNumber(memory?.attempt) ?? 0,
142
- pendingCandidates: Array.isArray(memory?.pendingCandidates)
143
- ? memory.pendingCandidates.map(sanitizeCandidate).filter((candidate): candidate is MemoryCandidate => Boolean(candidate))
144
- : [],
145
- lastError: readString(memory?.lastError),
146
- updatedAt: readString(memory?.updatedAt),
147
- },
148
- };
149
- }
90
+ const rawDerived = asRecord(rawSession.derived);
91
+ const rawSummary = asRecord(rawDerived?.summary);
92
+ const rawMemory = asRecord(rawDerived?.memory);
93
+ const legacySummaryStatus = readEnum(rawSession.summaryStatus, ["pending", "complete"]);
94
+ const legacyMemoryCursor = readNumber(rawSession.lastMemorySyncCount);
95
+
96
+ const summaryText = readString(rawSummary?.text);
97
+ const summaryTitle = readString(rawSummary?.title);
98
+ const summaryStatus = readTaskStatus(
99
+ rawSummary?.status,
100
+ summaryText || legacySummaryStatus === "complete" ? "complete" : "idle",
101
+ );
102
+ const summaryCursor = clampCursor(
103
+ readNumber(rawSummary?.basedOnCursor),
104
+ summaryStatus === "complete" ? lastMirroredCount : 0,
105
+ lastMirroredCount,
106
+ );
107
+
108
+ const capturedCursor = clampCursor(
109
+ readNumber(rawMemory?.capturedCursor)
110
+ ?? readNumber(rawMemory?.appliedCursor)
111
+ ?? legacyMemoryCursor,
112
+ summaryStatus === "complete" ? lastMirroredCount : 0,
113
+ lastMirroredCount,
114
+ );
115
+ const memoryStatus = readTaskStatus(
116
+ rawMemory?.status ?? rawMemory?.extractStatus ?? rawMemory?.reconcileStatus,
117
+ capturedCursor >= lastMirroredCount && lastMirroredCount > 0 ? "complete" : "idle",
118
+ );
119
+ const candidates = readMemoryCandidates(rawMemory?.candidates);
150
120
 
151
- function migrateDerivedState(fallback: {
152
- lastMirroredCount: number;
153
- finalizedAt?: string;
154
- summaryStatus?: "pending" | "complete";
155
- lastMemorySyncCount?: number;
156
- }): SessionDerivedState {
157
- const mirrorCursor = fallback.lastMirroredCount;
158
- const finalized = Boolean(fallback.finalizedAt);
159
- const summaryComplete = fallback.summaryStatus === "complete";
160
- const appliedCursor = clampCursor(fallback.lastMemorySyncCount, mirrorCursor);
161
- const digestCursor = finalized && summaryComplete ? mirrorCursor : 0;
162
121
  return {
163
- digest: {
164
- cursor: digestCursor,
165
- status: digestCursor < mirrorCursor ? "pending" : "idle",
166
- attempt: 0,
167
- },
168
122
  summary: {
169
- basedOnCursor: summaryComplete ? mirrorCursor : 0,
170
- status: finalized ? (summaryComplete ? "complete" : "pending") : "idle",
123
+ basedOnCursor: summaryCursor,
124
+ status: finalizedAt && summaryStatus === "idle" && lastMirroredCount > 0 ? "error" : summaryStatus,
125
+ ...(summaryText ? { text: summaryText } : {}),
126
+ ...(summaryTitle ? { title: summaryTitle } : {}),
127
+ ...(readString(rawSummary?.lastError) ? { lastError: readString(rawSummary?.lastError) } : {}),
128
+ ...(readString(rawSummary?.updatedAt) ? { updatedAt: readString(rawSummary?.updatedAt) } : {}),
171
129
  },
172
130
  memory: {
173
- extractCursor: appliedCursor,
174
- appliedCursor,
175
- extractStatus: appliedCursor < mirrorCursor ? "pending" : "idle",
176
- reconcileStatus: "idle",
177
- attempt: 0,
178
- pendingCandidates: [],
131
+ capturedCursor,
132
+ status: memoryStatus,
133
+ ...(candidates ? { candidates } : {}),
134
+ ...(readString(rawMemory?.lastError) ? { lastError: readString(rawMemory?.lastError) } : {}),
135
+ ...(readString(rawMemory?.updatedAt) ? { updatedAt: readString(rawMemory?.updatedAt) } : {}),
179
136
  },
180
137
  };
181
138
  }
182
139
 
183
140
  function readString(value: unknown): string | undefined {
184
- if (typeof value !== "string") {
185
- return undefined;
186
- }
141
+ if (typeof value !== "string") return undefined;
187
142
  const trimmed = value.trim();
188
143
  return trimmed ? trimmed : undefined;
189
144
  }
@@ -194,42 +149,55 @@ function readEnum<T extends string>(value: unknown, allowed: T[]): T | undefined
194
149
  }
195
150
 
196
151
  function readNumber(value: unknown): number | undefined {
197
- if (typeof value !== "number" || !Number.isFinite(value)) {
198
- return undefined;
199
- }
152
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
200
153
  return Math.max(0, Math.floor(value));
201
154
  }
202
155
 
203
156
  function readTaskStatus(value: unknown, fallback: SessionTaskStatus): SessionTaskStatus {
204
157
  const status = readEnum(value, ["idle", "pending", "running", "complete", "error"]);
205
158
  if (!status) return fallback;
206
- return status === "running" ? "pending" : status;
159
+ if (status === "pending" || status === "running") return "idle";
160
+ return status;
207
161
  }
208
162
 
209
- function sanitizeCandidate(value: unknown): MemoryCandidate | null {
163
+ function readTitleSource(value: unknown): "placeholder" | "llm" | undefined {
164
+ const source = readEnum(value, ["placeholder", "digest", "llm"]);
165
+ if (!source) return undefined;
166
+ return source === "digest" ? "llm" : source;
167
+ }
168
+
169
+ function readMemoryCandidates(value: unknown): MemoryCandidate[] | undefined {
170
+ if (!Array.isArray(value)) return undefined;
171
+ const out = value
172
+ .map((entry) => sanitizeMemoryCandidate(entry))
173
+ .filter((candidate): candidate is MemoryCandidate => candidate !== null);
174
+ return out;
175
+ }
176
+
177
+ function sanitizeMemoryCandidate(value: unknown): MemoryCandidate | null {
210
178
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
211
179
  const record = value as Record<string, unknown>;
212
- const detail = readString(record.detail);
213
180
  const candidateId = readString(record.candidateId);
214
- if (!detail) return null;
181
+ const detail = readString(record.detail);
182
+ if (!candidateId || !detail) return null;
183
+ const title = readString(record.title);
184
+ const kind = readString(record.kind);
185
+ const topics = Array.isArray(record.topics)
186
+ ? record.topics.map((topic) => readString(topic)).filter((topic): topic is string => Boolean(topic))
187
+ : undefined;
188
+ const evidence = readString(record.evidence);
215
189
  return {
216
- candidateId: candidateId ?? detail,
190
+ candidateId,
217
191
  detail,
218
- ...(readString(record.title) ? { title: readString(record.title) } : {}),
219
- ...(readString(record.kind) ? { kind: readString(record.kind) } : {}),
220
- ...(Array.isArray(record.topics)
221
- ? {
222
- topics: record.topics
223
- .map((topic) => readString(topic))
224
- .filter((topic): topic is string => Boolean(topic)),
225
- }
226
- : {}),
227
- ...(readString(record.evidence) ? { evidence: readString(record.evidence) } : {}),
192
+ ...(title ? { title } : {}),
193
+ ...(kind ? { kind } : {}),
194
+ ...(topics && topics.length > 0 ? { topics } : {}),
195
+ ...(evidence ? { evidence } : {}),
228
196
  };
229
197
  }
230
198
 
231
- function clampCursor(value: number | undefined, max: number): number {
232
- if (typeof value !== "number" || !Number.isFinite(value)) return 0;
199
+ function clampCursor(value: number | undefined, fallback: number, max: number): number {
200
+ if (typeof value !== "number" || !Number.isFinite(value)) return Math.min(max, Math.max(0, fallback));
233
201
  return Math.min(max, Math.max(0, Math.floor(value)));
234
202
  }
235
203
 
package/src/types.ts CHANGED
@@ -16,11 +16,8 @@ export type ClawMemPluginConfig = {
16
16
  agents: Record<string, ClawMemAgentConfig>;
17
17
  memoryRecallLimit: number;
18
18
  memoryAutoRecallLimit: number;
19
- turnCommentDelayMs: number;
20
- digestWaitTimeoutMs: number;
21
19
  summaryWaitTimeoutMs: number;
22
20
  memoryExtractWaitTimeoutMs: number;
23
- memoryReconcileWaitTimeoutMs: number;
24
21
  };
25
22
 
26
23
  export type ClawMemResolvedRoute = {
@@ -35,7 +32,7 @@ export type ClawMemResolvedRoute = {
35
32
  export type BootstrapIdentityResponse = { token: string; repo_full_name: string };
36
33
  export type AgentRegistrationResponse = BootstrapIdentityResponse & { login: string };
37
34
  export type AnonymousSessionResponse = BootstrapIdentityResponse & { owner_login: string; repo_name: string };
38
- export type SessionTaskStatus = "idle" | "pending" | "running" | "complete" | "error";
35
+ export type SessionTaskStatus = "idle" | "complete" | "error";
39
36
  export type MemoryCandidate = {
40
37
  candidateId: string;
41
38
  detail: string;
@@ -44,48 +41,34 @@ export type MemoryCandidate = {
44
41
  topics?: string[];
45
42
  evidence?: string;
46
43
  };
47
- export type SessionDigestState = {
48
- cursor: number;
49
- status: SessionTaskStatus;
50
- attempt: number;
51
- text?: string;
52
- title?: string;
53
- lastError?: string;
54
- updatedAt?: string;
55
- };
56
44
  export type SessionSummaryState = {
57
45
  basedOnCursor: number;
58
46
  status: SessionTaskStatus;
59
47
  text?: string;
48
+ title?: string;
60
49
  lastError?: string;
61
50
  updatedAt?: string;
62
51
  };
63
52
  export type SessionMemoryState = {
64
- extractCursor: number;
65
- appliedCursor: number;
66
- extractStatus: SessionTaskStatus;
67
- reconcileStatus: SessionTaskStatus;
68
- attempt: number;
69
- pendingCandidates: MemoryCandidate[];
53
+ capturedCursor: number;
54
+ status: SessionTaskStatus;
55
+ candidates?: MemoryCandidate[];
70
56
  lastError?: string;
71
57
  updatedAt?: string;
72
58
  };
73
59
  export type SessionDerivedState = {
74
- digest: SessionDigestState;
75
60
  summary: SessionSummaryState;
76
61
  memory: SessionMemoryState;
77
62
  };
78
63
  export type SessionMirrorState = {
79
64
  sessionId: string; sessionKey?: string; sessionFile?: string; agentId?: string;
80
- issueNumber?: number; issueTitle?: string; titleSource?: "placeholder" | "digest" | "llm";
81
- lastMirroredCount: number; turnCount: number; lastAssistantText?: string;
82
- lastMemorySyncCount?: number;
83
- summaryStatus?: "pending" | "complete";
84
- finalizedAt?: string; lastSummaryHash?: string; lastTurnHash?: string;
65
+ issueNumber?: number; issueTitle?: string; titleSource?: "placeholder" | "llm";
66
+ lastMirroredCount: number; turnCount: number;
67
+ finalizedAt?: string; lastSummaryHash?: string;
85
68
  derived?: SessionDerivedState;
86
69
  createdAt?: string; updatedAt?: string;
87
70
  };
88
- export type PluginState = { version: 3; sessions: Record<string, SessionMirrorState>; migrations?: Record<string, string> };
71
+ export type PluginState = { version: 4; sessions: Record<string, SessionMirrorState>; migrations?: Record<string, string> };
89
72
  export type NormalizedMessage = { role: string; text: string; toolName?: string; timestamp?: string; stopReason?: string };
90
73
  export type TranscriptSnapshot = { sessionId?: string; messages: NormalizedMessage[] };
91
74
  export type MemoryDraft = { title?: string; detail: string; kind?: string; topics?: string[] };
package/tsconfig.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "extends": "../openclaw/tsconfig.json",
3
3
  "compilerOptions": {
4
+ "outDir": "./dist",
4
5
  "baseUrl": ".",
5
6
  "paths": {
6
7
  "openclaw/plugin-sdk": ["../openclaw/src/plugin-sdk/index.ts"],