@clawmem-ai/clawmem 0.1.18 → 0.1.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 +28 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/src/collaboration.d.ts +49 -0
- package/dist/src/collaboration.js +69 -0
- package/dist/src/config.d.ts +21 -0
- package/dist/src/config.js +119 -0
- package/dist/src/conversation.d.ts +30 -0
- package/dist/src/conversation.js +323 -0
- package/dist/src/github-client.d.ts +269 -0
- package/dist/src/github-client.js +350 -0
- package/dist/src/keyed-async-queue.d.ts +12 -0
- package/dist/src/keyed-async-queue.js +23 -0
- package/dist/src/memory.d.ts +29 -0
- package/dist/src/memory.js +451 -0
- package/dist/src/recall-sanitize.d.ts +1 -0
- package/dist/src/recall-sanitize.js +149 -0
- package/dist/src/runtime-env.d.ts +2 -0
- package/dist/src/runtime-env.js +12 -0
- package/dist/src/service.d.ts +18 -0
- package/dist/src/service.js +3645 -0
- package/dist/src/state.d.ts +4 -0
- package/dist/src/state.js +182 -0
- package/dist/src/transcript.d.ts +3 -0
- package/dist/src/transcript.js +164 -0
- package/dist/src/types.d.ts +130 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +26 -0
- package/dist/src/utils.js +62 -0
- package/dist/src/yaml.d.ts +2 -0
- package/dist/src/yaml.js +81 -0
- package/openclaw.plugin.json +14 -1
- package/package.json +21 -7
- package/skills/clawmem/SKILL.md +26 -5
- package/skills/clawmem/references/collaboration.md +13 -5
- package/skills/clawmem/references/review.md +77 -0
- package/skills/clawmem/references/schema.md +44 -1
- package/index.ts +0 -6
- package/src/collaboration.test.ts +0 -71
- package/src/collaboration.ts +0 -109
- package/src/config.test.ts +0 -83
- package/src/config.ts +0 -117
- package/src/conversation.test.ts +0 -120
- package/src/conversation.ts +0 -304
- package/src/github-client.test.ts +0 -101
- package/src/github-client.ts +0 -363
- package/src/keyed-async-queue.ts +0 -26
- package/src/memory.test.ts +0 -588
- package/src/memory.ts +0 -444
- package/src/recall-sanitize.ts +0 -143
- package/src/runtime-env.ts +0 -12
- package/src/service.test.ts +0 -337
- package/src/service.ts +0 -2786
- package/src/state.test.ts +0 -119
- package/src/state.ts +0 -206
- package/src/transcript.ts +0 -186
- package/src/types.ts +0 -86
- package/src/utils.ts +0 -74
- package/src/yaml.ts +0 -88
- package/tsconfig.json +0 -15
package/src/state.test.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { loadState } from "./state.js";
|
|
5
|
-
|
|
6
|
-
function assert(condition: unknown, message: string): void {
|
|
7
|
-
if (!condition) throw new Error(message);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async function withTempStateFile(payload: unknown, fn: (filePath: string) => Promise<void>): Promise<void> {
|
|
11
|
-
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "clawmem-state-"));
|
|
12
|
-
const filePath = path.join(dir, "state.json");
|
|
13
|
-
try {
|
|
14
|
-
await fs.promises.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
|
|
15
|
-
await fn(filePath);
|
|
16
|
-
} finally {
|
|
17
|
-
await fs.promises.rm(dir, { recursive: true, force: true });
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function testMigratesLegacyV2State(): Promise<void> {
|
|
22
|
-
await withTempStateFile({
|
|
23
|
-
version: 2,
|
|
24
|
-
sessions: {
|
|
25
|
-
"main:s-1": {
|
|
26
|
-
sessionId: "s-1",
|
|
27
|
-
agentId: "main",
|
|
28
|
-
issueNumber: 10,
|
|
29
|
-
lastMirroredCount: 6,
|
|
30
|
-
turnCount: 6,
|
|
31
|
-
lastMemorySyncCount: 4,
|
|
32
|
-
summaryStatus: "pending",
|
|
33
|
-
finalizedAt: "2026-04-03T10:00:00.000Z",
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
}, async (filePath) => {
|
|
37
|
-
const state = await loadState(filePath);
|
|
38
|
-
const session = state.sessions["main:s-1"];
|
|
39
|
-
assert(state.version === 4, "expected state version 4 after migration");
|
|
40
|
-
assert(Boolean(session), "expected migrated session to exist");
|
|
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");
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function testNormalizesRunningTaskStates(): Promise<void> {
|
|
47
|
-
await withTempStateFile({
|
|
48
|
-
version: 3,
|
|
49
|
-
sessions: {
|
|
50
|
-
"main:s-2": {
|
|
51
|
-
sessionId: "s-2",
|
|
52
|
-
agentId: "main",
|
|
53
|
-
lastMirroredCount: 3,
|
|
54
|
-
turnCount: 3,
|
|
55
|
-
derived: {
|
|
56
|
-
summary: { basedOnCursor: 0, status: "running" },
|
|
57
|
-
memory: {
|
|
58
|
-
extractCursor: 1,
|
|
59
|
-
appliedCursor: 0,
|
|
60
|
-
extractStatus: "running",
|
|
61
|
-
reconcileStatus: "running",
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
}, async (filePath) => {
|
|
67
|
-
const state = await loadState(filePath);
|
|
68
|
-
const session = state.sessions["main:s-2"];
|
|
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");
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
await testMigratesLegacyV2State();
|
|
116
|
-
await testNormalizesRunningTaskStates();
|
|
117
|
-
await testPreservesCachedFinalArtifacts();
|
|
118
|
-
|
|
119
|
-
console.log("state tests passed");
|
package/src/state.ts
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { MemoryCandidate, PluginState, SessionDerivedState, SessionMirrorState, SessionTaskStatus } from "./types.js";
|
|
4
|
-
import { normalizeAgentId, sessionScopeKey } from "./utils.js";
|
|
5
|
-
|
|
6
|
-
const EMPTY_STATE: PluginState = {
|
|
7
|
-
version: 4,
|
|
8
|
-
sessions: {},
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function resolveStatePath(stateDir: string): string {
|
|
12
|
-
return path.join(stateDir, "clawmem", "state.json");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function loadState(filePath: string): Promise<PluginState> {
|
|
16
|
-
try {
|
|
17
|
-
const raw = await fs.promises.readFile(filePath, "utf8");
|
|
18
|
-
return sanitizeState(JSON.parse(raw));
|
|
19
|
-
} catch (error) {
|
|
20
|
-
const code = (error as { code?: string }).code;
|
|
21
|
-
if (code === "ENOENT") {
|
|
22
|
-
return structuredClone(EMPTY_STATE);
|
|
23
|
-
}
|
|
24
|
-
return structuredClone(EMPTY_STATE);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function saveState(filePath: string, state: PluginState): Promise<void> {
|
|
29
|
-
await fs.promises.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
30
|
-
const next = JSON.stringify(state, null, 2) + "\n";
|
|
31
|
-
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
32
|
-
await fs.promises.writeFile(tmpPath, next, { encoding: "utf8", mode: 0o600 });
|
|
33
|
-
await fs.promises.rename(tmpPath, filePath);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function sanitizeState(value: unknown): PluginState {
|
|
37
|
-
if (!value || typeof value !== "object") {
|
|
38
|
-
return structuredClone(EMPTY_STATE);
|
|
39
|
-
}
|
|
40
|
-
const raw = value as Record<string, unknown>;
|
|
41
|
-
const sessions = raw.sessions && typeof raw.sessions === "object"
|
|
42
|
-
? (raw.sessions as Record<string, unknown>)
|
|
43
|
-
: {};
|
|
44
|
-
const migrations: Record<string, string> = {};
|
|
45
|
-
if (raw.migrations && typeof raw.migrations === "object") {
|
|
46
|
-
for (const [k, v] of Object.entries(raw.migrations as Record<string, unknown>)) {
|
|
47
|
-
const s = readString(v);
|
|
48
|
-
if (s) migrations[k] = s;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const out: PluginState = {
|
|
52
|
-
version: 4,
|
|
53
|
-
sessions: {},
|
|
54
|
-
...(Object.keys(migrations).length > 0 ? { migrations } : {}),
|
|
55
|
-
};
|
|
56
|
-
for (const [storedKey, sessionValue] of Object.entries(sessions)) {
|
|
57
|
-
if (!sessionValue || typeof sessionValue !== "object" || !storedKey.trim()) continue;
|
|
58
|
-
const rawSession = sessionValue as Record<string, unknown>;
|
|
59
|
-
const sessionId = readString(rawSession.sessionId) ?? storedKey.trim();
|
|
60
|
-
if (!sessionId) continue;
|
|
61
|
-
const agentId = normalizeAgentId(readString(rawSession.agentId));
|
|
62
|
-
const lastMirroredCount = readNumber(rawSession.lastMirroredCount) ?? 0;
|
|
63
|
-
const finalizedAt = readString(rawSession.finalizedAt);
|
|
64
|
-
const derived = sanitizeDerivedState(rawSession, lastMirroredCount, finalizedAt);
|
|
65
|
-
out.sessions[sessionScopeKey(sessionId, agentId)] = {
|
|
66
|
-
sessionId,
|
|
67
|
-
sessionKey: readString(rawSession.sessionKey),
|
|
68
|
-
sessionFile: readString(rawSession.sessionFile),
|
|
69
|
-
agentId,
|
|
70
|
-
issueNumber: readNumber(rawSession.issueNumber),
|
|
71
|
-
issueTitle: readString(rawSession.issueTitle),
|
|
72
|
-
titleSource: readTitleSource(rawSession.titleSource),
|
|
73
|
-
lastMirroredCount,
|
|
74
|
-
turnCount: readNumber(rawSession.turnCount) ?? 0,
|
|
75
|
-
finalizedAt,
|
|
76
|
-
lastSummaryHash: readString(rawSession.lastSummaryHash),
|
|
77
|
-
derived,
|
|
78
|
-
createdAt: readString(rawSession.createdAt),
|
|
79
|
-
updatedAt: readString(rawSession.updatedAt),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function sanitizeDerivedState(
|
|
86
|
-
rawSession: Record<string, unknown>,
|
|
87
|
-
lastMirroredCount: number,
|
|
88
|
-
finalizedAt?: string,
|
|
89
|
-
): SessionDerivedState {
|
|
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);
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
summary: {
|
|
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) } : {}),
|
|
129
|
-
},
|
|
130
|
-
memory: {
|
|
131
|
-
capturedCursor,
|
|
132
|
-
status: memoryStatus,
|
|
133
|
-
...(candidates ? { candidates } : {}),
|
|
134
|
-
...(readString(rawMemory?.lastError) ? { lastError: readString(rawMemory?.lastError) } : {}),
|
|
135
|
-
...(readString(rawMemory?.updatedAt) ? { updatedAt: readString(rawMemory?.updatedAt) } : {}),
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function readString(value: unknown): string | undefined {
|
|
141
|
-
if (typeof value !== "string") return undefined;
|
|
142
|
-
const trimmed = value.trim();
|
|
143
|
-
return trimmed ? trimmed : undefined;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function readEnum<T extends string>(value: unknown, allowed: T[]): T | undefined {
|
|
147
|
-
const s = readString(value);
|
|
148
|
-
return s && (allowed as string[]).includes(s) ? (s as T) : undefined;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function readNumber(value: unknown): number | undefined {
|
|
152
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
153
|
-
return Math.max(0, Math.floor(value));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function readTaskStatus(value: unknown, fallback: SessionTaskStatus): SessionTaskStatus {
|
|
157
|
-
const status = readEnum(value, ["idle", "pending", "running", "complete", "error"]);
|
|
158
|
-
if (!status) return fallback;
|
|
159
|
-
if (status === "pending" || status === "running") return "idle";
|
|
160
|
-
return status;
|
|
161
|
-
}
|
|
162
|
-
|
|
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 {
|
|
178
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
179
|
-
const record = value as Record<string, unknown>;
|
|
180
|
-
const candidateId = readString(record.candidateId);
|
|
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);
|
|
189
|
-
return {
|
|
190
|
-
candidateId,
|
|
191
|
-
detail,
|
|
192
|
-
...(title ? { title } : {}),
|
|
193
|
-
...(kind ? { kind } : {}),
|
|
194
|
-
...(topics && topics.length > 0 ? { topics } : {}),
|
|
195
|
-
...(evidence ? { evidence } : {}),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
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));
|
|
201
|
-
return Math.min(max, Math.max(0, Math.floor(value)));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
205
|
-
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
|
|
206
|
-
}
|
package/src/transcript.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import type { NormalizedMessage, TranscriptSnapshot } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export async function readTranscriptSnapshot(filePath: string): Promise<TranscriptSnapshot> {
|
|
5
|
-
const raw = await fs.promises.readFile(filePath, "utf8");
|
|
6
|
-
const lines = raw
|
|
7
|
-
.split(/\r?\n/)
|
|
8
|
-
.map((line) => line.trim())
|
|
9
|
-
.filter((line) => line.length > 0);
|
|
10
|
-
|
|
11
|
-
let sessionId: string | undefined;
|
|
12
|
-
const messages: NormalizedMessage[] = [];
|
|
13
|
-
|
|
14
|
-
for (const line of lines) {
|
|
15
|
-
let parsed: unknown;
|
|
16
|
-
try {
|
|
17
|
-
parsed = JSON.parse(line);
|
|
18
|
-
} catch {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
const record = asRecord(parsed);
|
|
22
|
-
if (!record) {
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
if (!sessionId && record.type === "session" && typeof record.id === "string") {
|
|
26
|
-
sessionId = record.id.trim() || undefined;
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
const message = normalizeMessage(record.message ?? record);
|
|
30
|
-
if (message) {
|
|
31
|
-
messages.push(message);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return { sessionId, messages };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function normalizeMessages(items: unknown[]): NormalizedMessage[] {
|
|
39
|
-
const out: NormalizedMessage[] = [];
|
|
40
|
-
for (const item of items) {
|
|
41
|
-
const record = asRecord(item);
|
|
42
|
-
if (!record) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
const message = normalizeMessage(record.message ?? record);
|
|
46
|
-
if (message) {
|
|
47
|
-
out.push(message);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function normalizeMessage(value: unknown): NormalizedMessage | null {
|
|
54
|
-
const record = asRecord(value);
|
|
55
|
-
if (!record) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const role = typeof record.role === "string" ? record.role : undefined;
|
|
59
|
-
if (role !== "assistant" && role !== "user") {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
if (record.tool_call_id || record.toolCallId) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const directText = typeof record.text === "string" ? normalizeChatText(record.text) : null;
|
|
67
|
-
const text = directText ?? extractChatText(record.content);
|
|
68
|
-
if (!text) {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const timestamp = normalizeTimestamp(record.timestamp);
|
|
73
|
-
const stopReason = typeof record.stopReason === "string" ? record.stopReason : undefined;
|
|
74
|
-
return {
|
|
75
|
-
role,
|
|
76
|
-
text,
|
|
77
|
-
...(timestamp ? { timestamp } : {}),
|
|
78
|
-
...(stopReason ? { stopReason } : {}),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function extractChatText(content: unknown): string {
|
|
83
|
-
if (typeof content === "string") {
|
|
84
|
-
return normalizeChatText(content) ?? "";
|
|
85
|
-
}
|
|
86
|
-
if (!Array.isArray(content)) {
|
|
87
|
-
return "";
|
|
88
|
-
}
|
|
89
|
-
const parts: string[] = [];
|
|
90
|
-
for (const block of content) {
|
|
91
|
-
if (typeof block === "string") {
|
|
92
|
-
const normalized = normalizeChatText(block);
|
|
93
|
-
if (normalized) {
|
|
94
|
-
parts.push(normalized);
|
|
95
|
-
}
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const record = asRecord(block);
|
|
99
|
-
if (!record) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
|
|
103
|
-
if (type.includes("tool")) {
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const textCandidates = [
|
|
107
|
-
record.text,
|
|
108
|
-
record.value,
|
|
109
|
-
record.content,
|
|
110
|
-
record.outputText,
|
|
111
|
-
record.inputText,
|
|
112
|
-
];
|
|
113
|
-
for (const candidate of textCandidates) {
|
|
114
|
-
if (typeof candidate === "string") {
|
|
115
|
-
const normalized = normalizeChatText(candidate);
|
|
116
|
-
if (normalized) {
|
|
117
|
-
parts.push(normalized);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return compactText(parts);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function normalizeChatText(value: string): string | null {
|
|
126
|
-
let trimmed = stripUntrustedMetadataPrefixes(squashWhitespace(value));
|
|
127
|
-
if (!trimmed) {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
trimmed = trimmed.replace(/^\[\[\s*reply_to[^\]]*\]\]\s*/i, "").trim();
|
|
131
|
-
if (!trimmed) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
const upper = trimmed.toUpperCase();
|
|
135
|
-
if (upper === "NO_REPLY" || upper === "HEARTBEAT_OK" || upper === "IDLE-CHAT") {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
return trimmed;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function normalizeTimestamp(value: unknown): string | undefined {
|
|
142
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
143
|
-
return new Date(value).toISOString();
|
|
144
|
-
}
|
|
145
|
-
if (typeof value === "string") {
|
|
146
|
-
const trimmed = value.trim();
|
|
147
|
-
return trimmed || undefined;
|
|
148
|
-
}
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function compactText(parts: Array<string | undefined>): string {
|
|
153
|
-
return parts
|
|
154
|
-
.map((part) => (typeof part === "string" ? part.trim() : ""))
|
|
155
|
-
.filter((part) => part.length > 0)
|
|
156
|
-
.join("\n\n");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function squashWhitespace(value: string): string {
|
|
160
|
-
return value.replace(/\r/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function stripUntrustedMetadataPrefixes(value: string): string {
|
|
164
|
-
let current = value.trim();
|
|
165
|
-
|
|
166
|
-
for (;;) {
|
|
167
|
-
const next = current
|
|
168
|
-
.replace(
|
|
169
|
-
/^Conversation info \(untrusted metadata\):\s*```(?:json)?\s*[\s\S]*?```\s*/i,
|
|
170
|
-
"",
|
|
171
|
-
)
|
|
172
|
-
.replace(
|
|
173
|
-
/^Sender \(untrusted metadata\):\s*```(?:json)?\s*[\s\S]*?```\s*/i,
|
|
174
|
-
"",
|
|
175
|
-
)
|
|
176
|
-
.trim();
|
|
177
|
-
if (next === current) {
|
|
178
|
-
return current;
|
|
179
|
-
}
|
|
180
|
-
current = next;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
185
|
-
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
|
186
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// Shared types for the clawmem plugin.
|
|
2
|
-
export type ClawMemAgentConfig = {
|
|
3
|
-
baseUrl?: string;
|
|
4
|
-
defaultRepo?: string;
|
|
5
|
-
repo?: string;
|
|
6
|
-
token?: string;
|
|
7
|
-
authScheme?: "token" | "bearer";
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export type ClawMemPluginConfig = {
|
|
11
|
-
baseUrl: string;
|
|
12
|
-
defaultRepo?: string;
|
|
13
|
-
repo?: string;
|
|
14
|
-
token?: string;
|
|
15
|
-
authScheme: "token" | "bearer";
|
|
16
|
-
agents: Record<string, ClawMemAgentConfig>;
|
|
17
|
-
memoryRecallLimit: number;
|
|
18
|
-
memoryAutoRecallLimit: number;
|
|
19
|
-
summaryWaitTimeoutMs: number;
|
|
20
|
-
memoryExtractWaitTimeoutMs: number;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type ClawMemResolvedRoute = {
|
|
24
|
-
agentId: string;
|
|
25
|
-
baseUrl: string;
|
|
26
|
-
defaultRepo?: string;
|
|
27
|
-
repo?: string;
|
|
28
|
-
token?: string;
|
|
29
|
-
authScheme: "token" | "bearer";
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type BootstrapIdentityResponse = { token: string; repo_full_name: string };
|
|
33
|
-
export type AgentRegistrationResponse = BootstrapIdentityResponse & { login: string };
|
|
34
|
-
export type AnonymousSessionResponse = BootstrapIdentityResponse & { owner_login: string; repo_name: string };
|
|
35
|
-
export type SessionTaskStatus = "idle" | "complete" | "error";
|
|
36
|
-
export type MemoryCandidate = {
|
|
37
|
-
candidateId: string;
|
|
38
|
-
detail: string;
|
|
39
|
-
title?: string;
|
|
40
|
-
kind?: string;
|
|
41
|
-
topics?: string[];
|
|
42
|
-
evidence?: string;
|
|
43
|
-
};
|
|
44
|
-
export type SessionSummaryState = {
|
|
45
|
-
basedOnCursor: number;
|
|
46
|
-
status: SessionTaskStatus;
|
|
47
|
-
text?: string;
|
|
48
|
-
title?: string;
|
|
49
|
-
lastError?: string;
|
|
50
|
-
updatedAt?: string;
|
|
51
|
-
};
|
|
52
|
-
export type SessionMemoryState = {
|
|
53
|
-
capturedCursor: number;
|
|
54
|
-
status: SessionTaskStatus;
|
|
55
|
-
candidates?: MemoryCandidate[];
|
|
56
|
-
lastError?: string;
|
|
57
|
-
updatedAt?: string;
|
|
58
|
-
};
|
|
59
|
-
export type SessionDerivedState = {
|
|
60
|
-
summary: SessionSummaryState;
|
|
61
|
-
memory: SessionMemoryState;
|
|
62
|
-
};
|
|
63
|
-
export type SessionMirrorState = {
|
|
64
|
-
sessionId: string; sessionKey?: string; sessionFile?: string; agentId?: string;
|
|
65
|
-
issueNumber?: number; issueTitle?: string; titleSource?: "placeholder" | "llm";
|
|
66
|
-
lastMirroredCount: number; turnCount: number;
|
|
67
|
-
finalizedAt?: string; lastSummaryHash?: string;
|
|
68
|
-
derived?: SessionDerivedState;
|
|
69
|
-
createdAt?: string; updatedAt?: string;
|
|
70
|
-
};
|
|
71
|
-
export type PluginState = { version: 4; sessions: Record<string, SessionMirrorState>; migrations?: Record<string, string> };
|
|
72
|
-
export type NormalizedMessage = { role: string; text: string; toolName?: string; timestamp?: string; stopReason?: string };
|
|
73
|
-
export type TranscriptSnapshot = { sessionId?: string; messages: NormalizedMessage[] };
|
|
74
|
-
export type MemoryDraft = { title?: string; detail: string; kind?: string; topics?: string[] };
|
|
75
|
-
export type MemorySchema = { kinds: string[]; topics: string[] };
|
|
76
|
-
export type MemoryListOptions = {
|
|
77
|
-
status?: "active" | "stale" | "all";
|
|
78
|
-
kind?: string;
|
|
79
|
-
topic?: string;
|
|
80
|
-
limit?: number;
|
|
81
|
-
};
|
|
82
|
-
export type ParsedMemoryIssue = {
|
|
83
|
-
issueNumber: number; title: string; memoryId: string; memoryHash?: string;
|
|
84
|
-
date: string; detail: string;
|
|
85
|
-
kind?: string; topics?: string[]; status: "active" | "stale";
|
|
86
|
-
};
|
package/src/utils.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// Shared utility helpers used by memory.ts and conversation.ts.
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import type { NormalizedMessage } from "./types.js";
|
|
5
|
-
|
|
6
|
-
export const DEFAULT_AGENT_ID = "main";
|
|
7
|
-
export const DEFAULT_BOOTSTRAP_REPO_NAME = "memory";
|
|
8
|
-
|
|
9
|
-
const MAX_AGENT_LOGIN_PREFIX_LEN = 32;
|
|
10
|
-
|
|
11
|
-
export function sha256(v: string): string { return crypto.createHash("sha256").update(v).digest("hex"); }
|
|
12
|
-
|
|
13
|
-
export function normalizeAgentId(value: string | undefined | null): string {
|
|
14
|
-
const trimmed = (value ?? "").trim().toLowerCase();
|
|
15
|
-
if (!trimmed) return DEFAULT_AGENT_ID;
|
|
16
|
-
return trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || DEFAULT_AGENT_ID;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function buildAgentBootstrapRegistration(agentId: string): { prefixLogin: string; defaultRepoName: string } {
|
|
20
|
-
const prefixLogin = normalizeAgentId(agentId)
|
|
21
|
-
.replace(/_/g, "-")
|
|
22
|
-
.replace(/-+/g, "-")
|
|
23
|
-
.replace(/^-+|-+$/g, "")
|
|
24
|
-
.slice(0, MAX_AGENT_LOGIN_PREFIX_LEN)
|
|
25
|
-
.replace(/-+$/g, "") || DEFAULT_AGENT_ID;
|
|
26
|
-
return { prefixLogin, defaultRepoName: DEFAULT_BOOTSTRAP_REPO_NAME };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function sessionScopeKey(sessionId: string, agentId?: string): string {
|
|
30
|
-
return `${normalizeAgentId(agentId)}:${sessionId.trim()}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function inferAgentIdFromTranscriptPath(filePath: string): string | undefined {
|
|
34
|
-
const parts = path.resolve(filePath).split(path.sep);
|
|
35
|
-
const idx = parts.lastIndexOf("agents");
|
|
36
|
-
if (idx < 0 || !parts[idx + 1] || parts[idx + 2] !== "sessions") return undefined;
|
|
37
|
-
return normalizeAgentId(parts[idx + 1]);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function subKey(s: { sessionId: string; agentId?: string }, suffix: string): string {
|
|
41
|
-
const san = (v: string) => v.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "main";
|
|
42
|
-
return `agent:${san(s.agentId || "main")}:subagent:clawmem-${suffix}-${san(s.sessionId)}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function fmtTranscript(msgs: NormalizedMessage[]): string {
|
|
46
|
-
return msgs.map((m, i) => `${i + 1}. ${m.role === "assistant" ? "assistant" : "user"}: ${m.text}`).join("\n\n");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function fmtTranscriptFrom(msgs: NormalizedMessage[], startIndex: number): string {
|
|
50
|
-
return msgs.map((m, i) => `${startIndex + i + 1}. ${m.role === "assistant" ? "assistant" : "user"}: ${m.text}`).join("\n\n");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function sliceTranscriptDelta(
|
|
54
|
-
msgs: NormalizedMessage[],
|
|
55
|
-
fromIndex: number,
|
|
56
|
-
anchorCount = 2,
|
|
57
|
-
): { anchorStart: number; deltaStart: number; anchorMessages: NormalizedMessage[]; deltaMessages: NormalizedMessage[] } {
|
|
58
|
-
const deltaStart = Math.min(Math.max(0, Math.floor(fromIndex)), msgs.length);
|
|
59
|
-
const anchorStart = Math.max(0, deltaStart - Math.max(0, Math.floor(anchorCount)));
|
|
60
|
-
return {
|
|
61
|
-
anchorStart,
|
|
62
|
-
deltaStart,
|
|
63
|
-
anchorMessages: msgs.slice(anchorStart, deltaStart),
|
|
64
|
-
deltaMessages: msgs.slice(deltaStart),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function localDate(d: Date = new Date()): string {
|
|
69
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function localDateTime(d: Date): string {
|
|
73
|
-
return `${localDate(d)}T${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
|
|
74
|
-
}
|