@unblocklabs/unblock-memory 0.3.17 → 0.3.19
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 +13 -2
- package/dist/src/config.js +1 -1
- package/dist/src/manager.js +6 -1
- package/dist/src/runtime.d.ts +1 -1
- package/dist/src/runtime.js +29 -20
- package/dist/src/session-sync.d.ts +16 -3
- package/dist/src/session-sync.js +96 -15
- package/openclaw.plugin.json +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -731,12 +731,12 @@ session manifest. The projected file modification time matches the session
|
|
|
731
731
|
start time for meaningful chronological cluster reads. Session results include
|
|
732
732
|
provider, chat type, conversation identity, and start time as an ISO 8601 timestamp. They
|
|
733
733
|
participate in the same search and clustering index as file memory. The plugin
|
|
734
|
-
automatically
|
|
734
|
+
automatically checks each configured agent's sessions every 60 minutes while
|
|
735
735
|
the Gateway runs. Set `syncIntervalMinutes` on the `sessions` corpus to an integer
|
|
736
736
|
from `1` to `1440`, or `0` for manual-only syncing. For example:
|
|
737
737
|
|
|
738
738
|
```json
|
|
739
|
-
{ "name": "sessions", "kind": "sessions", "syncIntervalMinutes":
|
|
739
|
+
{ "name": "sessions", "kind": "sessions", "syncIntervalMinutes": 60 }
|
|
740
740
|
```
|
|
741
741
|
|
|
742
742
|
The first refresh runs after one interval, not during startup. Restart the
|
|
@@ -745,6 +745,17 @@ sync is skipped, and failures are visible through `memory_sync_status` and retri
|
|
|
745
745
|
at the next interval. `memory_sync_sessions` still provides an immediate manual
|
|
746
746
|
refresh. Syncing and embedding run inside the Gateway process, without an LLM turn.
|
|
747
747
|
|
|
748
|
+
Quiet checks compare source metadata and the last successful index checkpoint
|
|
749
|
+
before initializing the memory manager. Unchanged sessions skip QMD updates and
|
|
750
|
+
embedding. New assistant answers count too, not just human messages. Changed
|
|
751
|
+
transcripts are projected and content-hashed; tool-only or filtered additions
|
|
752
|
+
that leave the indexed text unchanged also skip indexing. Empty/filtered sessions
|
|
753
|
+
are remembered. Index changes, missing projections, changed projection settings,
|
|
754
|
+
QMD upgrades and incomplete runs invalidate the skip checkpoint; `force: true`
|
|
755
|
+
bypasses both gates. `memory_sync_status` reports `lastCheckedAt`, `lastIndexedAt`
|
|
756
|
+
and `skipReason` (`no_changes` or `no_indexable_changes`) separately. Existing
|
|
757
|
+
explicit intervals remain unchanged on upgrade; set them to `60` for hourly checks.
|
|
758
|
+
|
|
748
759
|
Indexes live at `~/.openclaw/agents/<agentId>/unblock-memory/index.sqlite` (or the
|
|
749
760
|
equivalent configured OpenClaw state directory). Durable agent-supplied event
|
|
750
761
|
dates and maintenance proposals live separately in `curation.sqlite`, so a QMD
|
package/dist/src/config.js
CHANGED
|
@@ -172,7 +172,7 @@ function resolveCorpora(value) {
|
|
|
172
172
|
!chatTypes.every((chatType) => CHAT_TYPES.includes(chatType))) {
|
|
173
173
|
throw new Error(`unblock-memory corpus sessions chatTypes must contain channel, group, or direct`);
|
|
174
174
|
}
|
|
175
|
-
const syncIntervalMinutes = corpus.syncIntervalMinutes ??
|
|
175
|
+
const syncIntervalMinutes = corpus.syncIntervalMinutes ?? 60;
|
|
176
176
|
if (typeof syncIntervalMinutes !== "number" || !Number.isInteger(syncIntervalMinutes) ||
|
|
177
177
|
syncIntervalMinutes < 0 || syncIntervalMinutes > 1440) {
|
|
178
178
|
throw new Error("unblock-memory corpus sessions syncIntervalMinutes must be an integer between 0 and 1440");
|
package/dist/src/manager.js
CHANGED
|
@@ -534,12 +534,14 @@ export class QmdMemoryManager {
|
|
|
534
534
|
if (!sessions)
|
|
535
535
|
throw new Error('memory session sync requires a configured "sessions" corpus');
|
|
536
536
|
onPhase?.("projecting");
|
|
537
|
-
const store = await this.#getStore();
|
|
538
537
|
const synced = await syncSessionProjections({
|
|
539
538
|
...sessions,
|
|
540
539
|
force,
|
|
540
|
+
indexPath: this.#dbPath,
|
|
541
|
+
indexReady: async () => (await (await this.#getStore()).getStatus()).needsEmbedding === 0,
|
|
541
542
|
index: async () => {
|
|
542
543
|
onPhase?.("indexing");
|
|
544
|
+
const store = await this.#getStore();
|
|
543
545
|
const update = await store.update({ collections: [sessions.collection] });
|
|
544
546
|
this.#cleanupRemovedDocuments?.(update.updated + update.removed);
|
|
545
547
|
const analysisStore = store;
|
|
@@ -561,6 +563,9 @@ export class QmdMemoryManager {
|
|
|
561
563
|
},
|
|
562
564
|
});
|
|
563
565
|
this.#sessionMetadata = sessionMetadataByPath(synced.manifest);
|
|
566
|
+
if (synced.result.skipReason)
|
|
567
|
+
return synced.result;
|
|
568
|
+
const store = await this.#getStore();
|
|
564
569
|
const status = await store.getStatus();
|
|
565
570
|
const collections = await store.listCollections();
|
|
566
571
|
this.#files = collections.reduce((total, collection) => total + collection.active_count, 0);
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
|
|
2
2
|
import type { CorpusConfig } from "./config.js";
|
|
3
3
|
import type { MemoryPluginRuntimeContract } from "./contracts.js";
|
|
4
4
|
import { QmdMemoryManager } from "./manager.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type SessionSyncResult } from "./session-sync.js";
|
|
6
6
|
export type SessionSyncStatus = {
|
|
7
7
|
status: "idle";
|
|
8
8
|
} | {
|
package/dist/src/runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolveAgentDir, resolveAgentWorkspaceDir, resolveStateDir, } from "ope
|
|
|
5
5
|
import { listAgentIds, resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
6
6
|
import { QmdMemoryManager } from "./manager.js";
|
|
7
7
|
import { resolveTimezone } from "./session-projector.js";
|
|
8
|
+
import { unchangedSessionSync } from "./session-sync.js";
|
|
8
9
|
import { resolveConfiguredSkillPath, resolveSessionSource, resolveSources } from "./sources.js";
|
|
9
10
|
import { classifyWorkspaceMemoryPaths } from "./workspace-path-classifier.js";
|
|
10
11
|
const activeSessionSyncs = new Map();
|
|
@@ -149,10 +150,15 @@ export class QmdMemoryRuntime {
|
|
|
149
150
|
}));
|
|
150
151
|
};
|
|
151
152
|
try {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
const sessions = this.#sessionConfig(params.cfg, params.agentId);
|
|
154
|
+
let result = !force && sessions
|
|
155
|
+
? await unchangedSessionSync(sessions, join(directory, "index.sqlite")) : undefined;
|
|
156
|
+
if (!result) {
|
|
157
|
+
const { manager, error } = await this.getMemorySearchManager(params);
|
|
158
|
+
if (!manager)
|
|
159
|
+
throw new Error(error ?? "memory unavailable");
|
|
160
|
+
result = await manager.syncSessions(force, writePhase);
|
|
161
|
+
}
|
|
156
162
|
await statusWrites;
|
|
157
163
|
await atomicWriteJson(statusPath, {
|
|
158
164
|
status: "completed",
|
|
@@ -224,10 +230,10 @@ export class QmdMemoryRuntime {
|
|
|
224
230
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
225
231
|
const stateDir = join(this.#stateRoot, "agents", agentId, "unblock-memory");
|
|
226
232
|
const fileCorpora = this.#corpora.filter((corpus) => corpus.kind === "files" || corpus.kind === "skills");
|
|
227
|
-
const
|
|
233
|
+
const sessions = this.#sessionConfig(cfg, agentId);
|
|
228
234
|
const sources = resolveSources(workspaceDir, fileCorpora);
|
|
229
|
-
const sessionSource =
|
|
230
|
-
? resolveSessionSource(
|
|
235
|
+
const sessionSource = sessions
|
|
236
|
+
? resolveSessionSource(sessions.outputDir, sessions.chatTypes)
|
|
231
237
|
: undefined;
|
|
232
238
|
if (sessionSource)
|
|
233
239
|
sources.push(sessionSource);
|
|
@@ -238,23 +244,26 @@ export class QmdMemoryRuntime {
|
|
|
238
244
|
sources,
|
|
239
245
|
keepModelsWarm: this.#keepEmbeddingModelWarm,
|
|
240
246
|
analysisExecutable: this.#analysisExecutable,
|
|
241
|
-
|
|
242
|
-
sessions: {
|
|
243
|
-
agentId,
|
|
244
|
-
agentName: resolveAgentIdentity(cfg, agentId)?.name?.trim() || agentId,
|
|
245
|
-
chatTypes: sessionCorpus.chatTypes,
|
|
246
|
-
maxExpandedTokens: sessionCorpus.maxExpandedTokens,
|
|
247
|
-
collection: sessionSource.collection,
|
|
248
|
-
databasePath: join(resolveAgentDir(cfg, agentId), "openclaw-agent.sqlite"),
|
|
249
|
-
manifestPath: join(stateDir, "sessions-manifest.json"),
|
|
250
|
-
outputDir: sessionSource.root,
|
|
251
|
-
timezone: resolveTimezone(cfg.agents?.defaults?.userTimezone?.trim()),
|
|
252
|
-
},
|
|
253
|
-
} : {}),
|
|
247
|
+
sessions,
|
|
254
248
|
});
|
|
255
249
|
await manager.start();
|
|
256
250
|
return manager;
|
|
257
251
|
}
|
|
252
|
+
#sessionConfig(cfg, agentId) {
|
|
253
|
+
const corpus = this.#corpora.find(corpus => corpus.kind === "sessions");
|
|
254
|
+
if (!corpus)
|
|
255
|
+
return;
|
|
256
|
+
const stateDir = this.#sessionSyncDirectory(agentId);
|
|
257
|
+
const source = resolveSessionSource(join(stateDir, "sessions"), corpus.chatTypes);
|
|
258
|
+
return {
|
|
259
|
+
agentId, agentName: resolveAgentIdentity(cfg, agentId)?.name?.trim() || agentId,
|
|
260
|
+
chatTypes: corpus.chatTypes, maxExpandedTokens: corpus.maxExpandedTokens,
|
|
261
|
+
collection: source.collection, outputDir: source.root,
|
|
262
|
+
databasePath: join(resolveAgentDir(cfg, agentId), "openclaw-agent.sqlite"),
|
|
263
|
+
manifestPath: join(stateDir, "sessions-manifest.json"),
|
|
264
|
+
timezone: resolveTimezone(cfg.agents?.defaults?.userTimezone?.trim()),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
258
267
|
#sessionSyncDirectory(agentId) {
|
|
259
268
|
return join(this.#stateRoot, "agents", agentId, "unblock-memory");
|
|
260
269
|
}
|
|
@@ -9,10 +9,15 @@ type IndexedSession = SessionMetadata & {
|
|
|
9
9
|
projectionHash: string;
|
|
10
10
|
documentPath: string;
|
|
11
11
|
projectorVersion: number;
|
|
12
|
+
sourceFingerprint?: string;
|
|
12
13
|
};
|
|
13
14
|
export type SessionManifest = {
|
|
14
15
|
version: number;
|
|
15
16
|
lastSuccessfulSyncAt?: number;
|
|
17
|
+
lastIndexedAt?: number;
|
|
18
|
+
projectionKey?: string;
|
|
19
|
+
indexSignature?: string;
|
|
20
|
+
ignoredSessions?: Record<string, string>;
|
|
16
21
|
sessions: Record<string, IndexedSession>;
|
|
17
22
|
};
|
|
18
23
|
export type SessionSyncResult = {
|
|
@@ -24,11 +29,12 @@ export type SessionSyncResult = {
|
|
|
24
29
|
failed: number;
|
|
25
30
|
embedded: number;
|
|
26
31
|
lastSuccessfulSyncAt: number;
|
|
32
|
+
lastCheckedAt?: number;
|
|
33
|
+
lastIndexedAt?: number;
|
|
34
|
+
skipReason?: "no_changes" | "no_indexable_changes";
|
|
27
35
|
diagnostics?: NonNullable<SessionProjectionInput["diagnostics"]>;
|
|
28
36
|
};
|
|
29
|
-
|
|
30
|
-
export declare function sessionMetadataByPath(manifest: SessionManifest): Map<string, SessionMetadata>;
|
|
31
|
-
export declare function syncSessionProjections(params: {
|
|
37
|
+
type ProjectionOptions = {
|
|
32
38
|
databasePath: string;
|
|
33
39
|
outputDir: string;
|
|
34
40
|
manifestPath: string;
|
|
@@ -36,7 +42,14 @@ export declare function syncSessionProjections(params: {
|
|
|
36
42
|
agentName: string;
|
|
37
43
|
timezone: string;
|
|
38
44
|
chatTypes: readonly ChatType[];
|
|
45
|
+
};
|
|
46
|
+
export declare function unchangedSessionSync(params: ProjectionOptions, indexPath: string): Promise<SessionSyncResult | undefined>;
|
|
47
|
+
export declare function readSessionManifest(path: string): Promise<SessionManifest>;
|
|
48
|
+
export declare function sessionMetadataByPath(manifest: SessionManifest): Map<string, SessionMetadata>;
|
|
49
|
+
export declare function syncSessionProjections(params: ProjectionOptions & {
|
|
39
50
|
force?: boolean;
|
|
51
|
+
indexPath?: string;
|
|
52
|
+
indexReady?: () => Promise<boolean>;
|
|
40
53
|
index?: () => Promise<number>;
|
|
41
54
|
}): Promise<{
|
|
42
55
|
result: SessionSyncResult;
|
package/dist/src/session-sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { chmod, mkdir, readFile, rename, unlink, utimes, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -7,6 +7,12 @@ import { projectSession, sessionDocumentPath, } from "./session-projector.js";
|
|
|
7
7
|
const MANIFEST_VERSION = 1;
|
|
8
8
|
export const PROJECTOR_VERSION = 6;
|
|
9
9
|
const SUPPORTED_SCHEMA_VERSIONS = new Set([17, 18, 19]);
|
|
10
|
+
// Source lives in src/, published code in dist/src/. Read our own pinned dependency
|
|
11
|
+
// metadata, not QMD internals (which may also be substituted by runtime inspectors).
|
|
12
|
+
const sourcePackage = new URL("../package.json", import.meta.url);
|
|
13
|
+
const packageMetadata = JSON.parse(readFileSync(existsSync(sourcePackage)
|
|
14
|
+
? sourcePackage : new URL("../../package.json", import.meta.url), "utf8"));
|
|
15
|
+
const indexVersion = [packageMetadata.version, packageMetadata.dependencies["@unblocklabs/qmd"]];
|
|
10
16
|
const REQUIRED_COLUMNS = {
|
|
11
17
|
schema_meta: ["meta_key", "role", "schema_version", "agent_id", "app_version"],
|
|
12
18
|
session_windows: [
|
|
@@ -21,6 +27,32 @@ const REQUIRED_COLUMNS = {
|
|
|
21
27
|
session_transcript_active_events: ["session_id", "active_position", "event_seq", "message_position"],
|
|
22
28
|
transcript_rewrite_watermarks: ["session_id", "generation"],
|
|
23
29
|
};
|
|
30
|
+
function projectionKey(params) {
|
|
31
|
+
return JSON.stringify([PROJECTOR_VERSION, params.databasePath, params.agentId,
|
|
32
|
+
params.agentName, params.timezone, [...params.chatTypes].sort()]);
|
|
33
|
+
}
|
|
34
|
+
// Conservative proof: any index/WAL write, replacement or QMD upgrade invalidates it.
|
|
35
|
+
// This avoids depending on QMD's private embedding schema or opening/loading its store.
|
|
36
|
+
function sessionIndexSignature(databasePath) {
|
|
37
|
+
try {
|
|
38
|
+
const fingerprint = (path) => {
|
|
39
|
+
const stat = statSync(path, { bigint: true });
|
|
40
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].map(String);
|
|
41
|
+
};
|
|
42
|
+
let wal = null;
|
|
43
|
+
try {
|
|
44
|
+
wal = fingerprint(`${databasePath}-wal`);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== "ENOENT")
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return JSON.stringify([indexVersion, fingerprint(databasePath), wal]);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
24
56
|
function projectionPath(outputDir, documentPath) {
|
|
25
57
|
const root = resolve(outputDir);
|
|
26
58
|
const target = resolve(root, documentPath);
|
|
@@ -106,6 +138,7 @@ function readSnapshot(params) {
|
|
|
106
138
|
ORDER BY active.active_position
|
|
107
139
|
`);
|
|
108
140
|
const events = new Map();
|
|
141
|
+
const changed = new Set();
|
|
109
142
|
for (const window of windows) {
|
|
110
143
|
const metadata = {
|
|
111
144
|
sessionId: window.sessionId,
|
|
@@ -117,18 +150,21 @@ function readSnapshot(params) {
|
|
|
117
150
|
};
|
|
118
151
|
const previous = params.previousManifest.sessions[window.sessionId];
|
|
119
152
|
const documentPath = sessionDocumentPath(metadata);
|
|
153
|
+
const sourceFingerprint = JSON.stringify(window);
|
|
120
154
|
const unchanged = !params.force &&
|
|
121
|
-
previous
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
155
|
+
(previous ? previous.sourceFingerprint === sourceFingerprint &&
|
|
156
|
+
previous.projectorVersion === PROJECTOR_VERSION &&
|
|
157
|
+
previous.documentPath === documentPath &&
|
|
158
|
+
existsSync(projectionPath(params.outputDir, documentPath)) :
|
|
159
|
+
params.previousManifest.ignoredSessions?.[window.sessionId] === sourceFingerprint);
|
|
126
160
|
if (!unchanged) {
|
|
127
|
-
|
|
161
|
+
changed.add(window.sessionId);
|
|
162
|
+
if (!params.metadataOnly)
|
|
163
|
+
events.set(window.sessionId, readEvents.all(window.sessionId));
|
|
128
164
|
}
|
|
129
165
|
}
|
|
130
166
|
db.exec("COMMIT");
|
|
131
|
-
return { windows, events };
|
|
167
|
+
return { windows, events, changed };
|
|
132
168
|
}
|
|
133
169
|
catch (error) {
|
|
134
170
|
try {
|
|
@@ -141,6 +177,20 @@ function readSnapshot(params) {
|
|
|
141
177
|
db.close();
|
|
142
178
|
}
|
|
143
179
|
}
|
|
180
|
+
export async function unchangedSessionSync(params, indexPath) {
|
|
181
|
+
const manifest = await readSessionManifest(params.manifestPath);
|
|
182
|
+
if (!manifest.lastSuccessfulSyncAt || manifest.projectionKey !== projectionKey(params) ||
|
|
183
|
+
!manifest.indexSignature || manifest.indexSignature !== sessionIndexSignature(indexPath))
|
|
184
|
+
return;
|
|
185
|
+
const snapshot = readSnapshot({ ...params, previousManifest: manifest, force: false, metadataOnly: true });
|
|
186
|
+
const ids = new Set(snapshot.windows.map(window => window.sessionId));
|
|
187
|
+
if (snapshot.changed.size || Object.keys(manifest.sessions).some(id => !ids.has(id)) ||
|
|
188
|
+
Object.keys(manifest.ignoredSessions ?? {}).some(id => !ids.has(id)))
|
|
189
|
+
return;
|
|
190
|
+
return { scanned: ids.size, unchanged: ids.size, updated: 0, removed: 0,
|
|
191
|
+
skipped: 0, failed: 0, embedded: 0, lastSuccessfulSyncAt: manifest.lastSuccessfulSyncAt,
|
|
192
|
+
lastCheckedAt: Date.now(), lastIndexedAt: manifest.lastIndexedAt, skipReason: "no_changes" };
|
|
193
|
+
}
|
|
144
194
|
function emptyManifest() {
|
|
145
195
|
return { version: MANIFEST_VERSION, sessions: {} };
|
|
146
196
|
}
|
|
@@ -205,10 +255,11 @@ export async function syncSessionProjections(params) {
|
|
|
205
255
|
const previousManifest = await readSessionManifest(params.manifestPath);
|
|
206
256
|
const snapshot = readSnapshot({
|
|
207
257
|
...params,
|
|
208
|
-
force: params.force === true,
|
|
258
|
+
force: params.force === true || previousManifest.projectionKey !== projectionKey(params),
|
|
209
259
|
previousManifest,
|
|
210
260
|
});
|
|
211
261
|
const sessions = {};
|
|
262
|
+
const ignoredSessions = {};
|
|
212
263
|
const counts = { unchanged: 0, updated: 0, removed: 0, skipped: 0, failed: 0 };
|
|
213
264
|
const diagnostics = { internalMessagesCleaned: 0, attachmentsCleaned: 0, attachmentBudgetSkipped: 0 };
|
|
214
265
|
await mkdir(params.outputDir, { recursive: true, mode: 0o700 });
|
|
@@ -226,7 +277,10 @@ export async function syncSessionProjections(params) {
|
|
|
226
277
|
};
|
|
227
278
|
const documentPath = sessionDocumentPath(metadata);
|
|
228
279
|
if (events === undefined) {
|
|
229
|
-
|
|
280
|
+
if (previous)
|
|
281
|
+
sessions[window.sessionId] = previous;
|
|
282
|
+
else
|
|
283
|
+
ignoredSessions[window.sessionId] = JSON.stringify(window);
|
|
230
284
|
counts.unchanged += 1;
|
|
231
285
|
continue;
|
|
232
286
|
}
|
|
@@ -255,6 +309,7 @@ export async function syncSessionProjections(params) {
|
|
|
255
309
|
continue;
|
|
256
310
|
}
|
|
257
311
|
if (!content) {
|
|
312
|
+
ignoredSessions[window.sessionId] = JSON.stringify(window);
|
|
258
313
|
counts.skipped += 1;
|
|
259
314
|
if (previous) {
|
|
260
315
|
await remove(projectionPath(params.outputDir, previous.documentPath));
|
|
@@ -263,8 +318,13 @@ export async function syncSessionProjections(params) {
|
|
|
263
318
|
continue;
|
|
264
319
|
}
|
|
265
320
|
const target = projectionPath(params.outputDir, documentPath);
|
|
266
|
-
|
|
267
|
-
|
|
321
|
+
const hash = projectionHash(content);
|
|
322
|
+
const contentChanged = params.force === true || previous?.projectorVersion !== PROJECTOR_VERSION ||
|
|
323
|
+
previous.projectionHash !== hash || previous.documentPath !== documentPath || !existsSync(target);
|
|
324
|
+
if (contentChanged) {
|
|
325
|
+
await atomicWrite(target, content, 0o600);
|
|
326
|
+
await utimes(target, new Date(), new Date(metadata.startedAt));
|
|
327
|
+
}
|
|
268
328
|
if (previous?.documentPath && previous.documentPath !== documentPath) {
|
|
269
329
|
await remove(projectionPath(params.outputDir, previous.documentPath));
|
|
270
330
|
}
|
|
@@ -274,11 +334,15 @@ export async function syncSessionProjections(params) {
|
|
|
274
334
|
maxSeq: window.maxSeq,
|
|
275
335
|
activeEventCount: window.activeEventCount,
|
|
276
336
|
sizeBytes: Buffer.byteLength(content),
|
|
277
|
-
projectionHash:
|
|
337
|
+
projectionHash: hash,
|
|
278
338
|
documentPath,
|
|
279
339
|
projectorVersion: PROJECTOR_VERSION,
|
|
340
|
+
sourceFingerprint: JSON.stringify(window),
|
|
280
341
|
};
|
|
281
|
-
|
|
342
|
+
if (contentChanged)
|
|
343
|
+
counts.updated += 1;
|
|
344
|
+
else
|
|
345
|
+
counts.unchanged += 1;
|
|
282
346
|
}
|
|
283
347
|
for (const [sessionId, session] of Object.entries(previousManifest.sessions)) {
|
|
284
348
|
if (sessions[sessionId] || snapshot.windows.some((window) => window.sessionId === sessionId))
|
|
@@ -286,11 +350,25 @@ export async function syncSessionProjections(params) {
|
|
|
286
350
|
await remove(projectionPath(params.outputDir, session.documentPath));
|
|
287
351
|
counts.removed += 1;
|
|
288
352
|
}
|
|
289
|
-
const
|
|
353
|
+
const needsIndex = params.force === true || counts.updated > 0 || counts.removed > 0 ||
|
|
354
|
+
previousManifest.projectionKey !== projectionKey(params) ||
|
|
355
|
+
!previousManifest.indexSignature || !params.indexPath ||
|
|
356
|
+
previousManifest.indexSignature !== sessionIndexSignature(params.indexPath);
|
|
357
|
+
const embedded = needsIndex ? await params.index?.() ?? 0 : 0;
|
|
290
358
|
const lastSuccessfulSyncAt = Date.now();
|
|
359
|
+
const indexed = needsIndex && params.index !== undefined;
|
|
360
|
+
const signature = params.indexPath ? sessionIndexSignature(params.indexPath) : undefined;
|
|
361
|
+
const indexReady = indexed && (await params.indexReady?.() ?? false);
|
|
362
|
+
const lastIndexedAt = indexed ? lastSuccessfulSyncAt : previousManifest.lastIndexedAt;
|
|
291
363
|
const manifest = {
|
|
292
364
|
version: MANIFEST_VERSION,
|
|
293
365
|
lastSuccessfulSyncAt,
|
|
366
|
+
lastIndexedAt,
|
|
367
|
+
projectionKey: projectionKey(params),
|
|
368
|
+
// Never certify an index mutation that happened during a skipped run or readiness check.
|
|
369
|
+
indexSignature: counts.failed > 0 ? undefined : !needsIndex ? previousManifest.indexSignature :
|
|
370
|
+
indexReady && params.indexPath && signature === sessionIndexSignature(params.indexPath) ? signature : undefined,
|
|
371
|
+
ignoredSessions,
|
|
294
372
|
sessions,
|
|
295
373
|
};
|
|
296
374
|
await atomicWrite(params.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 0o600);
|
|
@@ -300,6 +378,9 @@ export async function syncSessionProjections(params) {
|
|
|
300
378
|
...counts,
|
|
301
379
|
embedded,
|
|
302
380
|
lastSuccessfulSyncAt,
|
|
381
|
+
lastCheckedAt: lastSuccessfulSyncAt,
|
|
382
|
+
lastIndexedAt,
|
|
383
|
+
...(!needsIndex ? { skipReason: "no_indexable_changes" } : {}),
|
|
303
384
|
diagnostics,
|
|
304
385
|
},
|
|
305
386
|
manifest,
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.19",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": true },
|
|
@@ -186,8 +186,8 @@
|
|
|
186
186
|
"type": "integer",
|
|
187
187
|
"minimum": 0,
|
|
188
188
|
"maximum": 1440,
|
|
189
|
-
"default":
|
|
190
|
-
"description": "
|
|
189
|
+
"default": 60,
|
|
190
|
+
"description": "Check for session changes every N minutes while the Gateway runs; unchanged sessions skip indexing. 0 disables automatic sync. First check is after one interval."
|
|
191
191
|
},
|
|
192
192
|
"chatTypes": {
|
|
193
193
|
"type": "array",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unblocklabs/unblock-memory",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
4
4
|
"description": "Workspace-native memory for OpenClaw, powered by QMD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"preflight": "npm run knip && npm run build && npm run typecheck && npm test && npm run plugin:inspect && npm run plugin:inspect:runtime && npm pack --dry-run"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.
|
|
38
|
+
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.10.0/unblocklabs-qmd-2.10.0.tgz",
|
|
39
39
|
"chokidar": "5.0.0",
|
|
40
40
|
"picomatch": "^4.0.5",
|
|
41
41
|
"typebox": "1.3.6"
|