@unblocklabs/unblock-memory 0.2.2 → 0.2.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/dist/src/plugin.js +2 -2
- package/dist/src/runtime.d.ts +13 -3
- package/dist/src/runtime.js +140 -32
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/src/plugin.js
CHANGED
|
@@ -96,7 +96,7 @@ function createSyncSessionsTool(runtime, ctx) {
|
|
|
96
96
|
parameters: syncSessionsParameters,
|
|
97
97
|
async execute(_toolCallId, params) {
|
|
98
98
|
const { force } = Value.Parse(syncSessionsParameters, params);
|
|
99
|
-
return jsonResult(runtime.startSessionSync(active, force));
|
|
99
|
+
return jsonResult(await runtime.startSessionSync(active, force));
|
|
100
100
|
},
|
|
101
101
|
};
|
|
102
102
|
}
|
|
@@ -111,7 +111,7 @@ function createSyncStatusTool(runtime, ctx) {
|
|
|
111
111
|
parameters: syncStatusParameters,
|
|
112
112
|
async execute(_toolCallId, params) {
|
|
113
113
|
Value.Parse(syncStatusParameters, params);
|
|
114
|
-
return jsonResult(runtime.sessionSyncStatus(active.agentId));
|
|
114
|
+
return jsonResult(await runtime.sessionSyncStatus(active.agentId));
|
|
115
115
|
},
|
|
116
116
|
};
|
|
117
117
|
}
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -29,9 +29,18 @@ export type SessionSyncStartResult = {
|
|
|
29
29
|
status: "unavailable";
|
|
30
30
|
error: string;
|
|
31
31
|
};
|
|
32
|
+
type StoredSessionSyncStatus = Exclude<SessionSyncStatus, {
|
|
33
|
+
status: "idle";
|
|
34
|
+
}>;
|
|
35
|
+
type StoredRunningSessionSync = Extract<StoredSessionSyncStatus, {
|
|
36
|
+
status: "running";
|
|
37
|
+
}> & {
|
|
38
|
+
pid: number;
|
|
39
|
+
};
|
|
40
|
+
export declare function recoverInterruptedSessionSync(directory: string, statusPath: string, stale: StoredRunningSessionSync): Promise<SessionSyncStatus>;
|
|
32
41
|
export declare class QmdMemoryRuntime implements MemoryPluginRuntimeContract {
|
|
33
42
|
#private;
|
|
34
|
-
constructor(corpora: readonly CorpusConfig[], analysisExecutable?: string);
|
|
43
|
+
constructor(corpora: readonly CorpusConfig[], analysisExecutable?: string, stateRoot?: string);
|
|
35
44
|
getMemorySearchManager(params: {
|
|
36
45
|
cfg: OpenClawConfig;
|
|
37
46
|
agentId: string;
|
|
@@ -49,10 +58,11 @@ export declare class QmdMemoryRuntime implements MemoryPluginRuntimeContract {
|
|
|
49
58
|
startSessionSync(params: {
|
|
50
59
|
cfg: OpenClawConfig;
|
|
51
60
|
agentId: string;
|
|
52
|
-
}, force?: boolean): SessionSyncStartResult
|
|
53
|
-
sessionSyncStatus(agentId: string): SessionSyncStatus
|
|
61
|
+
}, force?: boolean): Promise<SessionSyncStartResult>;
|
|
62
|
+
sessionSyncStatus(agentId: string): Promise<SessionSyncStatus>;
|
|
54
63
|
closeMemorySearchManager(params: {
|
|
55
64
|
agentId: string;
|
|
56
65
|
}): Promise<void>;
|
|
57
66
|
closeAllMemorySearchManagers(): Promise<void>;
|
|
58
67
|
}
|
|
68
|
+
export {};
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
1
3
|
import { join } from "node:path";
|
|
2
4
|
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveStateDir, } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
3
5
|
import { resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
@@ -5,14 +7,70 @@ import { QmdMemoryManager } from "./manager.js";
|
|
|
5
7
|
import { resolveTimezone } from "./session-projector.js";
|
|
6
8
|
import { resolveSessionSource, resolveSources } from "./sources.js";
|
|
7
9
|
import { classifyWorkspaceMemoryPaths } from "./workspace-path-classifier.js";
|
|
10
|
+
const activeSessionSyncs = new Map();
|
|
11
|
+
async function readJson(path) {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === "ENOENT")
|
|
17
|
+
return undefined;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function removeIfPresent(path) {
|
|
22
|
+
try {
|
|
23
|
+
await unlink(path);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== "ENOENT")
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function atomicWriteJson(path, value) {
|
|
31
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
32
|
+
try {
|
|
33
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
34
|
+
await rename(temporary, path);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await removeIfPresent(temporary);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function recoverInterruptedSessionSync(directory, statusPath, stale) {
|
|
41
|
+
activeSessionSyncs.set(directory, stale.startedAt);
|
|
42
|
+
try {
|
|
43
|
+
const current = await readJson(statusPath);
|
|
44
|
+
if (!current)
|
|
45
|
+
return { status: "idle" };
|
|
46
|
+
if (current.status !== "running")
|
|
47
|
+
return current;
|
|
48
|
+
if (current.startedAt !== stale.startedAt) {
|
|
49
|
+
return { status: "running", phase: current.phase, startedAt: current.startedAt };
|
|
50
|
+
}
|
|
51
|
+
const failed = {
|
|
52
|
+
status: "failed",
|
|
53
|
+
startedAt: stale.startedAt,
|
|
54
|
+
completedAt: new Date().toISOString(),
|
|
55
|
+
error: "session sync interrupted by Gateway restart",
|
|
56
|
+
};
|
|
57
|
+
await atomicWriteJson(statusPath, failed);
|
|
58
|
+
return failed;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
if (activeSessionSyncs.get(directory) === stale.startedAt)
|
|
62
|
+
activeSessionSyncs.delete(directory);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
8
65
|
export class QmdMemoryRuntime {
|
|
9
66
|
#corpora;
|
|
10
67
|
#analysisExecutable;
|
|
68
|
+
#stateRoot;
|
|
11
69
|
#managers = new Map();
|
|
12
|
-
|
|
13
|
-
constructor(corpora, analysisExecutable) {
|
|
70
|
+
constructor(corpora, analysisExecutable, stateRoot = resolveStateDir()) {
|
|
14
71
|
this.#corpora = corpora;
|
|
15
72
|
this.#analysisExecutable = analysisExecutable;
|
|
73
|
+
this.#stateRoot = stateRoot;
|
|
16
74
|
}
|
|
17
75
|
async getMemorySearchManager(params) {
|
|
18
76
|
let pending = this.#managers.get(params.agentId);
|
|
@@ -32,46 +90,93 @@ export class QmdMemoryRuntime {
|
|
|
32
90
|
return { backend: "builtin" };
|
|
33
91
|
}
|
|
34
92
|
classifyWorkspaceMemoryPaths = classifyWorkspaceMemoryPaths;
|
|
35
|
-
startSessionSync(params, force = false) {
|
|
93
|
+
async startSessionSync(params, force = false) {
|
|
36
94
|
if (!this.#corpora.some((corpus) => corpus.kind === "sessions")) {
|
|
37
95
|
return {
|
|
38
96
|
status: "unavailable",
|
|
39
97
|
error: 'memory session sync requires a configured "sessions" corpus',
|
|
40
98
|
};
|
|
41
99
|
}
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
100
|
+
const directory = this.#sessionSyncDirectory(params.agentId);
|
|
101
|
+
const statusPath = join(directory, "session-sync-status.json");
|
|
102
|
+
const running = activeSessionSyncs.get(directory);
|
|
103
|
+
if (running)
|
|
104
|
+
return { status: "already_running", startedAt: running };
|
|
46
105
|
const startedAt = new Date().toISOString();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
void run().then((result) => {
|
|
57
|
-
this.#sessionSyncStatuses.set(params.agentId, {
|
|
58
|
-
status: "completed",
|
|
59
|
-
startedAt,
|
|
60
|
-
completedAt: new Date().toISOString(),
|
|
61
|
-
...result,
|
|
62
|
-
});
|
|
63
|
-
}, (error) => {
|
|
64
|
-
this.#sessionSyncStatuses.set(params.agentId, {
|
|
65
|
-
status: "failed",
|
|
106
|
+
activeSessionSyncs.set(directory, startedAt);
|
|
107
|
+
try {
|
|
108
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
109
|
+
await atomicWriteJson(statusPath, {
|
|
110
|
+
status: "running",
|
|
111
|
+
phase: "queued",
|
|
112
|
+
pid: process.pid,
|
|
66
113
|
startedAt,
|
|
67
|
-
completedAt: new Date().toISOString(),
|
|
68
|
-
error: error instanceof Error ? error.message : String(error),
|
|
69
114
|
});
|
|
70
|
-
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (activeSessionSyncs.get(directory) === startedAt)
|
|
118
|
+
activeSessionSyncs.delete(directory);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
void (async () => {
|
|
122
|
+
let statusWrites = Promise.resolve();
|
|
123
|
+
const writePhase = (phase) => {
|
|
124
|
+
statusWrites = statusWrites.then(() => atomicWriteJson(statusPath, {
|
|
125
|
+
status: "running",
|
|
126
|
+
phase,
|
|
127
|
+
pid: process.pid,
|
|
128
|
+
startedAt,
|
|
129
|
+
}));
|
|
130
|
+
};
|
|
131
|
+
try {
|
|
132
|
+
const { manager, error } = await this.getMemorySearchManager(params);
|
|
133
|
+
if (!manager)
|
|
134
|
+
throw new Error(error ?? "memory unavailable");
|
|
135
|
+
const result = await manager.syncSessions(force, writePhase);
|
|
136
|
+
await statusWrites;
|
|
137
|
+
await atomicWriteJson(statusPath, {
|
|
138
|
+
status: "completed",
|
|
139
|
+
startedAt,
|
|
140
|
+
completedAt: new Date().toISOString(),
|
|
141
|
+
...result,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
await statusWrites.catch(() => { });
|
|
146
|
+
try {
|
|
147
|
+
await atomicWriteJson(statusPath, {
|
|
148
|
+
status: "failed",
|
|
149
|
+
startedAt,
|
|
150
|
+
completedAt: new Date().toISOString(),
|
|
151
|
+
error: error instanceof Error ? error.message : String(error),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// The next status read converts the persisted running state to interrupted.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
if (activeSessionSyncs.get(directory) === startedAt)
|
|
160
|
+
activeSessionSyncs.delete(directory);
|
|
161
|
+
}
|
|
162
|
+
})().catch(() => { });
|
|
71
163
|
return { status: "started", startedAt };
|
|
72
164
|
}
|
|
73
|
-
sessionSyncStatus(agentId) {
|
|
74
|
-
|
|
165
|
+
async sessionSyncStatus(agentId) {
|
|
166
|
+
const directory = this.#sessionSyncDirectory(agentId);
|
|
167
|
+
const statusPath = join(directory, "session-sync-status.json");
|
|
168
|
+
const status = await readJson(statusPath);
|
|
169
|
+
const running = activeSessionSyncs.get(directory);
|
|
170
|
+
if (running) {
|
|
171
|
+
return status?.status === "running" && status.startedAt === running
|
|
172
|
+
? { status: "running", phase: status.phase, startedAt: running }
|
|
173
|
+
: { status: "running", phase: "queued", startedAt: running };
|
|
174
|
+
}
|
|
175
|
+
if (!status)
|
|
176
|
+
return { status: "idle" };
|
|
177
|
+
if (status.status !== "running")
|
|
178
|
+
return status;
|
|
179
|
+
return await recoverInterruptedSessionSync(directory, statusPath, status);
|
|
75
180
|
}
|
|
76
181
|
async closeMemorySearchManager(params) {
|
|
77
182
|
const pending = this.#managers.get(params.agentId);
|
|
@@ -85,7 +190,7 @@ export class QmdMemoryRuntime {
|
|
|
85
190
|
}
|
|
86
191
|
async #createManager(cfg, agentId) {
|
|
87
192
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
88
|
-
const stateDir = join(
|
|
193
|
+
const stateDir = join(this.#stateRoot, "agents", agentId, "unblock-memory");
|
|
89
194
|
const fileCorpora = this.#corpora.filter((corpus) => corpus.kind === "files");
|
|
90
195
|
const sessionCorpus = this.#corpora.find((corpus) => corpus.kind === "sessions");
|
|
91
196
|
const sources = resolveSources(workspaceDir, fileCorpora);
|
|
@@ -115,4 +220,7 @@ export class QmdMemoryRuntime {
|
|
|
115
220
|
await manager.start();
|
|
116
221
|
return manager;
|
|
117
222
|
}
|
|
223
|
+
#sessionSyncDirectory(agentId) {
|
|
224
|
+
return join(this.#stateRoot, "agents", agentId, "unblock-memory");
|
|
225
|
+
}
|
|
118
226
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": false },
|