@alfe.ai/openclaw-memory-cloud 0.0.38 → 0.0.39
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 +37 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -60
- package/dist/index.js +2 -396
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.d.cts +59 -0
- package/dist/plugin.d.cts.map +1 -0
- package/dist/plugin.d.ts +59 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +2 -0
- package/dist/plugin2.cjs +1107 -0
- package/dist/plugin2.d.cts +2 -0
- package/dist/plugin2.d.ts +2 -0
- package/dist/plugin2.js +1104 -0
- package/dist/plugin2.js.map +1 -0
- package/openclaw.plugin.json +6 -5
- package/package.json +27 -7
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -320
- package/dist/auto-capture.d.ts +0 -45
- package/dist/auto-capture.d.ts.map +0 -1
- package/dist/auto-capture.js +0 -101
- package/dist/auto-capture.js.map +0 -1
- package/dist/auto-recall.d.ts +0 -22
- package/dist/auto-recall.d.ts.map +0 -1
- package/dist/auto-recall.js +0 -63
- package/dist/auto-recall.js.map +0 -1
- package/dist/formatter.d.ts +0 -7
- package/dist/formatter.d.ts.map +0 -1
- package/dist/formatter.js +0 -27
- package/dist/formatter.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/ingest-epoch.d.ts +0 -38
- package/dist/ingest-epoch.d.ts.map +0 -1
- package/dist/ingest-epoch.js +0 -66
- package/dist/ingest-epoch.js.map +0 -1
- package/dist/session-backfill.d.ts +0 -54
- package/dist/session-backfill.d.ts.map +0 -1
- package/dist/session-backfill.js +0 -192
- package/dist/session-backfill.js.map +0 -1
- package/dist/types.d.ts +0 -113
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/src/__tests__/auto-capture.test.ts +0 -115
- package/src/__tests__/ingest-epoch.test.ts +0 -67
- package/src/__tests__/session-backfill.test.ts +0 -289
- package/src/auto-capture.ts +0 -108
- package/src/auto-recall.ts +0 -66
- package/src/formatter.ts +0 -30
- package/src/index.ts +0 -464
- package/src/ingest-epoch.ts +0 -78
- package/src/session-backfill.ts +0 -220
- package/src/types.ts +0 -93
- package/sst-env.d.ts +0 -10
- package/tsconfig.json +0 -20
package/src/session-backfill.ts
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session backfill — one-time ingestion of pre-existing chat history into
|
|
3
|
-
* cloud memory, run fire-and-forget at plugin startup (see index.ts).
|
|
4
|
-
*
|
|
5
|
-
* Reads the openclaw-chat plugin's on-disk session files directly:
|
|
6
|
-
* ~/.alfe/sessions/chat/{sessionId}.json
|
|
7
|
-
* The write side is packages/openclaw-chat/src/session-store.ts — that path
|
|
8
|
-
* and file shape are a read contract; keep the two in sync. The chat plugin
|
|
9
|
-
* is not a package dependency because its session-store functions aren't
|
|
10
|
-
* part of its public entry and the plugin isn't guaranteed to be installed
|
|
11
|
-
* alongside memory. Like the session store itself, this assumes one agent
|
|
12
|
-
* per machine (`alfe remove` deletes ~/.alfe, so files never outlive the
|
|
13
|
-
* agent they belong to).
|
|
14
|
-
*
|
|
15
|
-
* No-re-sync guarantees (in concert with services/memory):
|
|
16
|
-
* 1. Runs only while the server-side `sessionsBackfillSyncedAt` flag is
|
|
17
|
-
* unset; the flag survives integration uninstall/reinstall.
|
|
18
|
-
* 2. Only messages with timestamp < cutoffMs are ingested — for agents that
|
|
19
|
-
* already had memory installed, everything after `bootstrapSyncedAt` was
|
|
20
|
-
* already captured live by AutoCapture.
|
|
21
|
-
* 3. sessionKey = file sessionId and index = file array position, so the
|
|
22
|
-
* ingest consumer's per-session `lastProcessedIndex` high-water mark makes
|
|
23
|
-
* a crash-and-retry replay free (filtered server-side before any cost).
|
|
24
|
-
*
|
|
25
|
-
* Each session is sent as exactly ONE memoryIngest call (one SQS message,
|
|
26
|
-
* windowed to the most recent messages that fit the caps below). Splitting a
|
|
27
|
-
* session across multiple SQS messages would race the shared per-session
|
|
28
|
-
* high-water mark on the standard (non-FIFO) ingest queue: a transiently
|
|
29
|
-
* failed early chunk redelivered after a later chunk succeeds gets filtered
|
|
30
|
-
* out and silently lost.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
34
|
-
import { join } from "node:path";
|
|
35
|
-
import { homedir } from "node:os";
|
|
36
|
-
import type { MemoryCloudConfig, MemoryMessage, SessionMetadata } from "./types.js";
|
|
37
|
-
|
|
38
|
-
const DEFAULT_SESSIONS_DIR = join(homedir(), ".alfe", "sessions", "chat");
|
|
39
|
-
|
|
40
|
-
// Server-side zod caps on /agent/memory/ingest (services/memory
|
|
41
|
-
// api-agents/ingest/post.ts) — exceeding any of these is a deterministic 400,
|
|
42
|
-
// which would put the startup retry loop into a permanent failure.
|
|
43
|
-
const MAX_MESSAGES_PER_SESSION = 500;
|
|
44
|
-
const MAX_SESSION_KEY_CHARS = 256;
|
|
45
|
-
const MAX_MESSAGE_INDEX = 1_000_000;
|
|
46
|
-
const MAX_CONTENT_CHARS = 100_000;
|
|
47
|
-
/** One ingest call becomes one SQS SendMessage (256KB hard limit). */
|
|
48
|
-
const MAX_BATCH_BYTES = 180_000;
|
|
49
|
-
|
|
50
|
-
interface BackfillApi {
|
|
51
|
-
memoryIngest(sessionKey: string, messages: MemoryMessage[], metadata?: SessionMetadata): Promise<{ queued: boolean; messageCount: number }>;
|
|
52
|
-
memoryBootstrapStatusMark(scope?: "files" | "sessions"): Promise<{ synced: true; syncedAt: string }>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface BackfillLogger {
|
|
56
|
-
info: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
57
|
-
debug: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
58
|
-
warn: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface ParsedSession {
|
|
62
|
-
sessionId: string;
|
|
63
|
-
channel?: string;
|
|
64
|
-
userId?: string;
|
|
65
|
-
/** Recent-window messages carrying their original file-array position as `index`. */
|
|
66
|
-
messages: MemoryMessage[];
|
|
67
|
-
lastMessageMs: number;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface SessionsBackfillOptions {
|
|
71
|
-
cutoffMs: number;
|
|
72
|
-
sessionsDir?: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export async function runSessionsBackfill(
|
|
76
|
-
client: BackfillApi,
|
|
77
|
-
config: MemoryCloudConfig,
|
|
78
|
-
logger: BackfillLogger,
|
|
79
|
-
opts: SessionsBackfillOptions,
|
|
80
|
-
): Promise<void> {
|
|
81
|
-
const sessionsDir = opts.sessionsDir ?? DEFAULT_SESSIONS_DIR;
|
|
82
|
-
|
|
83
|
-
let fileNames: string[];
|
|
84
|
-
try {
|
|
85
|
-
fileNames = (await readdir(sessionsDir)).filter((name) => name.endsWith(".json"));
|
|
86
|
-
} catch (err) {
|
|
87
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
88
|
-
// No sessions directory — nothing to backfill, ever.
|
|
89
|
-
logger.debug("sessions backfill: no sessions directory, marking complete");
|
|
90
|
-
try { await client.memoryBootstrapStatusMark("sessions"); } catch { /* retry next start */ }
|
|
91
|
-
} else {
|
|
92
|
-
// Transient (EACCES/EMFILE/...) — don't forfeit the backfill.
|
|
93
|
-
logger.warn("sessions backfill: sessions directory unreadable; will retry on next startup", { err: String(err) });
|
|
94
|
-
}
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const sessions: ParsedSession[] = [];
|
|
99
|
-
for (const name of fileNames) {
|
|
100
|
-
const parsed = await parseSessionFile(join(sessionsDir, name), config.captureMaxChars, opts.cutoffMs);
|
|
101
|
-
if (parsed) sessions.push(parsed);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Most recent non-empty sessions first, capped. Filter BEFORE capping:
|
|
105
|
-
// on a retrofit agent the newest sessions are often entirely post-install
|
|
106
|
-
// (emptied by the cutoff) and must not occupy the cap slots.
|
|
107
|
-
const eligible = sessions
|
|
108
|
-
.filter((s) => s.messages.length > 0)
|
|
109
|
-
.sort((a, b) => b.lastMessageMs - a.lastMessageMs)
|
|
110
|
-
.slice(0, config.backfillMaxSessions);
|
|
111
|
-
|
|
112
|
-
if (eligible.length === 0) {
|
|
113
|
-
logger.debug("sessions backfill: no eligible sessions, marking complete");
|
|
114
|
-
try { await client.memoryBootstrapStatusMark("sessions"); } catch { /* retry next start */ }
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
logger.info("sessions backfill: ingesting sessions", {
|
|
119
|
-
sessionCount: eligible.length,
|
|
120
|
-
messageCount: eligible.reduce((n, s) => n + s.messages.length, 0),
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
for (const session of eligible) {
|
|
124
|
-
const metadata: SessionMetadata = {
|
|
125
|
-
...(session.channel ? { channelId: session.channel } : {}),
|
|
126
|
-
...(session.userId ? { userId: session.userId } : {}),
|
|
127
|
-
};
|
|
128
|
-
try {
|
|
129
|
-
await client.memoryIngest(session.sessionId, session.messages, metadata);
|
|
130
|
-
} catch (err) {
|
|
131
|
-
// Abort without marking — next startup replays, and the server-side
|
|
132
|
-
// lastProcessedIndex filter discards already-processed sessions free.
|
|
133
|
-
logger.warn("sessions backfill: ingest failed; will retry on next startup", {
|
|
134
|
-
sessionId: session.sessionId,
|
|
135
|
-
err: String(err),
|
|
136
|
-
});
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
await client.memoryBootstrapStatusMark("sessions");
|
|
143
|
-
logger.info("sessions backfill: complete", { sessionCount: eligible.length });
|
|
144
|
-
} catch (err) {
|
|
145
|
-
logger.warn("sessions backfill: mark failed; will retry on next startup", { err: String(err) });
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Tolerant parse of a single session file. Returns null for corrupt or
|
|
151
|
-
* shape-mismatched files (skipped, never fatal). Message `index` is the
|
|
152
|
-
* original array position in the file — stable because the store is
|
|
153
|
-
* append-only — so retries resume idempotently server-side.
|
|
154
|
-
*/
|
|
155
|
-
async function parseSessionFile(
|
|
156
|
-
filePath: string,
|
|
157
|
-
captureMaxChars: number,
|
|
158
|
-
cutoffMs: number,
|
|
159
|
-
): Promise<ParsedSession | null> {
|
|
160
|
-
let raw: unknown;
|
|
161
|
-
try {
|
|
162
|
-
raw = JSON.parse(await readFile(filePath, "utf8"));
|
|
163
|
-
} catch {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
if (typeof raw !== "object" || raw === null) return null;
|
|
167
|
-
|
|
168
|
-
const data = raw as Record<string, unknown>;
|
|
169
|
-
if (typeof data.sessionId !== "string" || data.sessionId.length === 0) return null;
|
|
170
|
-
if (data.sessionId.length > MAX_SESSION_KEY_CHARS) return null;
|
|
171
|
-
if (!Array.isArray(data.messages)) return null;
|
|
172
|
-
|
|
173
|
-
const maxContentChars = Math.min(captureMaxChars, MAX_CONTENT_CHARS);
|
|
174
|
-
let lastMessageMs = 0;
|
|
175
|
-
const filtered: MemoryMessage[] = [];
|
|
176
|
-
for (const [index, entry] of data.messages.entries()) {
|
|
177
|
-
if (index > MAX_MESSAGE_INDEX) break;
|
|
178
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
179
|
-
const msg = entry as Record<string, unknown>;
|
|
180
|
-
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
181
|
-
if (typeof msg.content !== "string" || msg.content.trim().length === 0) continue;
|
|
182
|
-
if (typeof msg.timestamp !== "number") continue;
|
|
183
|
-
|
|
184
|
-
lastMessageMs = Math.max(lastMessageMs, msg.timestamp);
|
|
185
|
-
if (msg.timestamp >= cutoffMs) continue;
|
|
186
|
-
|
|
187
|
-
filtered.push({
|
|
188
|
-
role: msg.role,
|
|
189
|
-
content: msg.content.length > maxContentChars ? msg.content.slice(0, maxContentChars) : msg.content,
|
|
190
|
-
index,
|
|
191
|
-
timestamp: new Date(msg.timestamp).toISOString(),
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
sessionId: data.sessionId,
|
|
197
|
-
channel: typeof data.channel === "string" ? data.channel : undefined,
|
|
198
|
-
userId: typeof data.userId === "string" ? data.userId : undefined,
|
|
199
|
-
messages: recentWindow(filtered),
|
|
200
|
-
lastMessageMs,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* The most recent contiguous run of messages fitting both ingest caps
|
|
206
|
-
* (message count and serialized bytes). Walks backwards from the newest
|
|
207
|
-
* message and stops at the first one that would overflow the byte budget.
|
|
208
|
-
*/
|
|
209
|
-
function recentWindow(messages: MemoryMessage[]): MemoryMessage[] {
|
|
210
|
-
const window: MemoryMessage[] = [];
|
|
211
|
-
let bytes = 0;
|
|
212
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
213
|
-
if (window.length >= MAX_MESSAGES_PER_SESSION) break;
|
|
214
|
-
const msgBytes = Buffer.byteLength(JSON.stringify(messages[i])) + 1;
|
|
215
|
-
if (bytes + msgBytes > MAX_BATCH_BYTES) break;
|
|
216
|
-
window.unshift(messages[i]);
|
|
217
|
-
bytes += msgBytes;
|
|
218
|
-
}
|
|
219
|
-
return window;
|
|
220
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
export interface MemoryCloudConfig {
|
|
2
|
-
autoCapture: boolean;
|
|
3
|
-
autoRecall: boolean;
|
|
4
|
-
captureMaxChars: number;
|
|
5
|
-
idleFlushSeconds: number;
|
|
6
|
-
backfillSessions: boolean;
|
|
7
|
-
backfillMaxSessions: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface MemoryMessage {
|
|
11
|
-
role: string;
|
|
12
|
-
content: string;
|
|
13
|
-
index: number;
|
|
14
|
-
timestamp?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface SessionMetadata {
|
|
18
|
-
channelId?: string;
|
|
19
|
-
userId?: string;
|
|
20
|
-
userName?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface MemorySearchResult {
|
|
24
|
-
facts: {
|
|
25
|
-
subject: string;
|
|
26
|
-
predicate: string;
|
|
27
|
-
object: string;
|
|
28
|
-
since: string;
|
|
29
|
-
confidence: number;
|
|
30
|
-
}[];
|
|
31
|
-
memories: {
|
|
32
|
-
id: string;
|
|
33
|
-
text: string;
|
|
34
|
-
topic: string;
|
|
35
|
-
subtopic: string;
|
|
36
|
-
tag: string;
|
|
37
|
-
importance: number;
|
|
38
|
-
timestamp: number;
|
|
39
|
-
score: number;
|
|
40
|
-
}[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface MemoryContext {
|
|
44
|
-
tier: number;
|
|
45
|
-
facts: { subject: string; predicate: string; object: string; since: string }[];
|
|
46
|
-
memories: { text: string; topic: string; subtopic: string; score: number }[];
|
|
47
|
-
tokenEstimate: number;
|
|
48
|
-
formatted: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Interface for memory API calls. Satisfied by AgentApiClient so the plugin
|
|
53
|
-
* doesn't need a hard dependency on the agent-api-client package.
|
|
54
|
-
*/
|
|
55
|
-
export interface MemoryApi {
|
|
56
|
-
memorySearch(query: string, opts?: {
|
|
57
|
-
limit?: number;
|
|
58
|
-
topic?: string;
|
|
59
|
-
subtopic?: string;
|
|
60
|
-
tag?: string;
|
|
61
|
-
includeKnowledge?: boolean;
|
|
62
|
-
}): Promise<MemorySearchResult>;
|
|
63
|
-
|
|
64
|
-
memoryStore(text: string, opts?: {
|
|
65
|
-
topic?: string;
|
|
66
|
-
subtopic?: string;
|
|
67
|
-
tag?: string;
|
|
68
|
-
importance?: number;
|
|
69
|
-
}): Promise<{ memoryId: string }>;
|
|
70
|
-
|
|
71
|
-
memoryIngest(sessionKey: string, messages: MemoryMessage[], metadata?: SessionMetadata, ingestEpoch?: number): Promise<{ queued: boolean; messageCount: number }>;
|
|
72
|
-
|
|
73
|
-
memoryLoadContext(tier?: number, topicHint?: string): Promise<{ formatted: string; [key: string]: unknown }>;
|
|
74
|
-
|
|
75
|
-
memoryLookupEntity(subject: string): Promise<{
|
|
76
|
-
subject: string;
|
|
77
|
-
triples: { tripleId: string; predicate: string; object: string; validFrom: string; validTo?: string; confidence: number }[];
|
|
78
|
-
}>;
|
|
79
|
-
|
|
80
|
-
memoryNavigate(): Promise<{
|
|
81
|
-
topics: { name: string; tripleCount: number; subtopics: string[] }[];
|
|
82
|
-
cursor: string | null;
|
|
83
|
-
}>;
|
|
84
|
-
|
|
85
|
-
memoryDelete(memoryId: string): Promise<{ deleted: boolean }>;
|
|
86
|
-
|
|
87
|
-
memoryStats(): Promise<{
|
|
88
|
-
vectorCount: number;
|
|
89
|
-
tripleCount: number;
|
|
90
|
-
storageEstimateBytes: number;
|
|
91
|
-
lastIngestionAt?: string;
|
|
92
|
-
}>;
|
|
93
|
-
}
|
package/sst-env.d.ts
DELETED
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "Node16",
|
|
5
|
-
"moduleResolution": "Node16",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"rootDir": "src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"declarationMap": true,
|
|
16
|
-
"sourceMap": true
|
|
17
|
-
},
|
|
18
|
-
"include": ["src/**/*"],
|
|
19
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
20
|
-
}
|