@clawmem-ai/clawmem 0.1.12 → 0.1.14
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 +5 -3
- package/openclaw.plugin.json +10 -1
- package/package.json +1 -1
- package/skills/clawmem/SKILL.md +10 -0
- package/skills/clawmem/references/collaboration.md +2 -0
- package/skills/clawmem/references/communication.md +5 -0
- package/skills/clawmem/references/manual-ops.md +9 -4
- package/skills/clawmem/references/repair.md +2 -1
- package/skills/clawmem/references/schema.md +8 -0
- package/src/collaboration.test.ts +71 -0
- package/src/collaboration.ts +109 -0
- package/src/config.test.ts +1 -0
- package/src/config.ts +4 -3
- package/src/github-client.ts +4 -4
- package/src/memory.test.ts +85 -8
- package/src/memory.ts +83 -22
- package/src/service.test.ts +141 -0
- package/src/service.ts +344 -65
- package/src/types.ts +2 -1
package/src/memory.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
|
3
3
|
import { LABEL_MEMORY_STALE, MEMORY_TITLE_PREFIX, extractLabelNames, labelVal } from "./config.js";
|
|
4
4
|
import type { GitHubIssueClient } from "./github-client.js";
|
|
5
5
|
import { normalizeMessages } from "./transcript.js";
|
|
6
|
-
import type { ClawMemPluginConfig, MemoryDraft, MemoryListOptions, MemorySchema,
|
|
6
|
+
import type { ClawMemPluginConfig, MemoryDraft, MemoryListOptions, MemorySchema, ParsedMemoryIssue, SessionMirrorState, TranscriptSnapshot } from "./types.js";
|
|
7
7
|
import { fmtTranscript, localDate, sha256, subKey } from "./utils.js";
|
|
8
8
|
import { parseFlatYaml, stringifyFlatYaml } from "./yaml.js";
|
|
9
9
|
|
|
@@ -84,8 +84,8 @@ export class MemoryStore {
|
|
|
84
84
|
|
|
85
85
|
const date = localDate();
|
|
86
86
|
const labels = memLabels(normalized.kind, normalized.topics);
|
|
87
|
-
const title =
|
|
88
|
-
const body =
|
|
87
|
+
const title = renderMemoryTitle(normalized);
|
|
88
|
+
const body = renderMemoryBody(detail, hash, date);
|
|
89
89
|
await this.client.ensureLabels(labels);
|
|
90
90
|
const issue = await this.client.createIssue({ title, body, labels });
|
|
91
91
|
return {
|
|
@@ -104,10 +104,15 @@ export class MemoryStore {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
async update(memoryId: string, patch: { detail?: string; kind?: string; topics?: string[] }): Promise<ParsedMemoryIssue | null> {
|
|
107
|
+
async update(memoryId: string, patch: { title?: string; detail?: string; kind?: string; topics?: string[] }): Promise<ParsedMemoryIssue | null> {
|
|
108
108
|
const current = await this.get(memoryId, "all");
|
|
109
109
|
if (!current) return null;
|
|
110
110
|
const nextDetail = typeof patch.detail === "string" && patch.detail.trim() ? norm(patch.detail) : current.detail;
|
|
111
|
+
const nextTitle = typeof patch.title === "string" && patch.title.trim()
|
|
112
|
+
? renderMemoryTitle({ title: patch.title.trim(), detail: nextDetail })
|
|
113
|
+
: patch.detail !== undefined
|
|
114
|
+
? renderMemoryTitle({ detail: nextDetail })
|
|
115
|
+
: current.title || renderMemoryTitle({ detail: nextDetail });
|
|
111
116
|
const nextKind = patch.kind !== undefined ? normalizeLabelValue(patch.kind, "kind:") : current.kind;
|
|
112
117
|
const nextTopics = patch.topics !== undefined
|
|
113
118
|
? uniqueNormalized(patch.topics.map((topic) => normalizeLabelValue(topic, "topic:")).filter(Boolean) as string[])
|
|
@@ -118,8 +123,7 @@ export class MemoryStore {
|
|
|
118
123
|
return (memory.memoryHash || sha256(norm(memory.detail))) === nextHash;
|
|
119
124
|
});
|
|
120
125
|
if (duplicate) throw new Error(`another active memory already stores this detail as [${duplicate.memoryId}]`);
|
|
121
|
-
const
|
|
122
|
-
const nextBody = stringifyFlatYaml([["memory_hash", nextHash], ["date", current.date], ["detail", nextDetail]]);
|
|
126
|
+
const nextBody = renderMemoryBody(nextDetail, nextHash, current.date);
|
|
123
127
|
const nextLabels = memLabels(nextKind, nextTopics);
|
|
124
128
|
await this.client.ensureLabels(nextLabels);
|
|
125
129
|
await this.client.updateIssue(current.issueNumber, { title: nextTitle, body: nextBody });
|
|
@@ -201,15 +205,16 @@ export class MemoryStore {
|
|
|
201
205
|
const kind = labelVal(labels, "kind:");
|
|
202
206
|
const topics = labels.filter((l) => l.startsWith("topic:")).map((l) => l.slice(6).trim()).filter(Boolean);
|
|
203
207
|
const rawBody = (issue.body ?? "").trim();
|
|
204
|
-
const
|
|
205
|
-
const detail =
|
|
208
|
+
const parsed = parseStoredMemoryBody(rawBody);
|
|
209
|
+
const detail = parsed.detail?.trim() || rawBody;
|
|
206
210
|
const status = issue.state === "closed" || labels.includes(LABEL_MEMORY_STALE) ? "stale" : "active";
|
|
207
211
|
if (!detail) return null;
|
|
208
212
|
return {
|
|
209
|
-
issueNumber: issue.number,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
issueNumber: issue.number,
|
|
214
|
+
title: issue.title?.trim() || "",
|
|
215
|
+
memoryId: parsed.meta.memory_id?.trim() || String(issue.number),
|
|
216
|
+
memoryHash: parsed.meta.memory_hash?.trim() || undefined,
|
|
217
|
+
date: parsed.meta.date?.trim() || "1970-01-01",
|
|
213
218
|
detail,
|
|
214
219
|
...(kind ? { kind } : {}),
|
|
215
220
|
...(topics.length > 0 ? { topics } : {}),
|
|
@@ -235,8 +240,8 @@ export class MemoryStore {
|
|
|
235
240
|
}
|
|
236
241
|
const labels = memLabels(draft.kind, draft.topics);
|
|
237
242
|
const date = localDate();
|
|
238
|
-
const title =
|
|
239
|
-
const body =
|
|
243
|
+
const title = renderMemoryTitle(draft);
|
|
244
|
+
const body = renderMemoryBody(detail, hash, date);
|
|
240
245
|
await this.client.ensureLabels(labels);
|
|
241
246
|
const issue = await this.client.createIssue({ title, body, labels });
|
|
242
247
|
activeByHash.set(hash, {
|
|
@@ -279,8 +284,10 @@ export class MemoryStore {
|
|
|
279
284
|
const sessionKey = subKey(session, "memory");
|
|
280
285
|
const message = [
|
|
281
286
|
"Extract durable memories from the conversation below.",
|
|
282
|
-
'Return JSON only in the form {"save":[{"detail":"...","kind":"...","topics":["..."]}],"stale":["memory-id"]}.',
|
|
287
|
+
'Return JSON only in the form {"save":[{"title":"...","detail":"...","kind":"...","topics":["..."]}],"stale":["memory-id"]}.',
|
|
288
|
+
"Each save item must contain one durable fact. If a turn contains several independent facts, save them separately instead of bundling them into one summary memory.",
|
|
283
289
|
"Use save for stable, reusable facts, preferences, decisions, constraints, workflows, and ongoing context worth remembering later.",
|
|
290
|
+
"Title is optional. If you provide one, make it concise and human-readable.",
|
|
284
291
|
"Use stale for existing memory IDs only when the conversation clearly supersedes or invalidates them.",
|
|
285
292
|
"Infer kind and topics when they would help future retrieval. Reuse existing kinds and topics when possible.",
|
|
286
293
|
"If no existing kind fits, you may propose a new short kind label. Keep kinds concise and reusable.",
|
|
@@ -293,9 +300,12 @@ export class MemoryStore {
|
|
|
293
300
|
].join("\n");
|
|
294
301
|
try {
|
|
295
302
|
const run = await subagent.run({
|
|
296
|
-
sessionKey,
|
|
303
|
+
sessionKey,
|
|
304
|
+
message,
|
|
305
|
+
deliver: false,
|
|
306
|
+
lane: "clawmem-memory",
|
|
297
307
|
idempotencyKey: sha256(`${session.sessionId}:${snapshot.messages.length}:memory-decision`),
|
|
298
|
-
extraSystemPrompt: "You extract durable memory updates from OpenClaw conversations. Output JSON only with save objects containing detail, optional kind, and optional topics, plus stale string ids.",
|
|
308
|
+
extraSystemPrompt: "You extract durable memory updates from OpenClaw conversations. Output JSON only with save objects containing detail, optional title, optional kind, and optional topics, plus stale string ids. Keep each save item to one durable fact.",
|
|
299
309
|
});
|
|
300
310
|
const wait = await subagent.waitForRun({ runId: run.runId, timeoutMs: this.config.summaryWaitTimeoutMs });
|
|
301
311
|
if (wait.status === "timeout") throw new Error("memory decision subagent timed out");
|
|
@@ -304,7 +314,9 @@ export class MemoryStore {
|
|
|
304
314
|
const text = [...msgs].reverse().find((e) => e.role === "assistant" && e.text.trim())?.text;
|
|
305
315
|
if (!text) throw new Error("memory decision subagent returned no assistant text");
|
|
306
316
|
return parseDecision(text);
|
|
307
|
-
} finally {
|
|
317
|
+
} finally {
|
|
318
|
+
subagent.deleteSession({ sessionKey, deleteTranscript: true }).catch(() => {});
|
|
319
|
+
}
|
|
308
320
|
}
|
|
309
321
|
|
|
310
322
|
private async mergeSchema(memory: ParsedMemoryIssue, draft: MemoryDraft): Promise<ParsedMemoryIssue> {
|
|
@@ -333,11 +345,42 @@ function memLabels(kind?: string, topics?: string[]): string[] {
|
|
|
333
345
|
...((topics ?? []).map((topic) => `topic:${topic}`)),
|
|
334
346
|
];
|
|
335
347
|
}
|
|
348
|
+
|
|
349
|
+
function renderMemoryTitle(draft: Pick<MemoryDraft, "detail" | "title">): string {
|
|
350
|
+
const raw = typeof draft.title === "string" && draft.title.trim() ? draft.title : draft.detail;
|
|
351
|
+
const normalized = norm(raw);
|
|
352
|
+
return normalized.startsWith(MEMORY_TITLE_PREFIX) ? normalized : `${MEMORY_TITLE_PREFIX}${normalized}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function renderMemoryBody(detail: string, memoryHash: string, date: string): string {
|
|
356
|
+
return stringifyFlatYaml([["memory_hash", memoryHash], ["date", date], ["detail", norm(detail)]]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function parseStoredMemoryBody(rawBody: string): { detail: string; meta: Record<string, string> } {
|
|
360
|
+
const trimmed = rawBody.trim();
|
|
361
|
+
if (!trimmed) return { detail: "", meta: {} };
|
|
362
|
+
|
|
363
|
+
const legacyYaml = parseFlatYaml(trimmed);
|
|
364
|
+
if (legacyYaml.detail?.trim()) {
|
|
365
|
+
return { detail: legacyYaml.detail.trim(), meta: legacyYaml };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const hiddenMeta = /(?:^|\n)<!--\s*clawmem-meta\s*\n([\s\S]*?)\n-->\s*$/.exec(trimmed);
|
|
369
|
+
if (!hiddenMeta) {
|
|
370
|
+
return { detail: trimmed, meta: {} };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const meta = parseFlatYaml(hiddenMeta[1] ?? "");
|
|
374
|
+
const detail = trimmed.slice(0, hiddenMeta.index).trim() || meta.detail?.trim() || "";
|
|
375
|
+
return { detail, meta };
|
|
376
|
+
}
|
|
377
|
+
|
|
336
378
|
function norm(v: string): string { return v.replace(/\s+/g, " ").trim(); }
|
|
337
379
|
function trunc(v: string, max: number): string { const s = norm(v); return s.length <= max ? s : `${s.slice(0, max - 1).trimEnd()}…`; }
|
|
338
380
|
function normalizeSearch(v: string): string {
|
|
339
381
|
return v.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
|
|
340
382
|
}
|
|
383
|
+
|
|
341
384
|
function buildSearchIndex(memory: ParsedMemoryIssue): SearchIndex {
|
|
342
385
|
return {
|
|
343
386
|
title: normalizeSearch(memory.title),
|
|
@@ -346,6 +389,7 @@ function buildSearchIndex(memory: ParsedMemoryIssue): SearchIndex {
|
|
|
346
389
|
topics: (memory.topics ?? []).map(normalizeSearch).filter(Boolean),
|
|
347
390
|
};
|
|
348
391
|
}
|
|
392
|
+
|
|
349
393
|
function searchTokens(v: string): string[] {
|
|
350
394
|
const seen = new Set<string>();
|
|
351
395
|
for (const token of v.split(/[^0-9\p{L}]+/u)) {
|
|
@@ -359,6 +403,7 @@ function searchTokens(v: string): string[] {
|
|
|
359
403
|
}
|
|
360
404
|
return [...seen];
|
|
361
405
|
}
|
|
406
|
+
|
|
362
407
|
function charBigrams(v: string): Set<string> {
|
|
363
408
|
const compact = v.replace(/\s+/g, "");
|
|
364
409
|
if (compact.length < 2) return new Set(compact ? [compact] : []);
|
|
@@ -366,16 +411,19 @@ function charBigrams(v: string): Set<string> {
|
|
|
366
411
|
for (let i = 0; i < compact.length - 1; i++) out.add(compact.slice(i, i + 2));
|
|
367
412
|
return out;
|
|
368
413
|
}
|
|
414
|
+
|
|
369
415
|
function overlapRatio(left: Set<string>, right: Set<string>): number {
|
|
370
416
|
if (left.size === 0 || right.size === 0) return 0;
|
|
371
417
|
let hits = 0;
|
|
372
418
|
for (const token of left) if (right.has(token)) hits++;
|
|
373
419
|
return hits / Math.max(left.size, right.size);
|
|
374
420
|
}
|
|
421
|
+
|
|
375
422
|
function buildMemorySearchQuery(query: string, repo: string): string {
|
|
376
423
|
const parts = [query.trim(), `repo:${repo}`, "is:issue", "state:open", 'label:"type:memory"'].filter(Boolean);
|
|
377
424
|
return parts.join(" ");
|
|
378
425
|
}
|
|
426
|
+
|
|
379
427
|
export function scoreMemoryMatch(memory: ParsedMemoryIssue, rawQuery: string): number {
|
|
380
428
|
const query = normalizeSearch(rawQuery);
|
|
381
429
|
if (!query) return 0;
|
|
@@ -411,17 +459,21 @@ export function scoreMemoryMatch(memory: ParsedMemoryIssue, rawQuery: string): n
|
|
|
411
459
|
|
|
412
460
|
return score;
|
|
413
461
|
}
|
|
462
|
+
|
|
414
463
|
function normalizeDraft(input: MemoryDraft): MemoryDraft {
|
|
415
464
|
const detail = norm(input.detail);
|
|
416
465
|
if (!detail) throw new Error("memory detail is empty");
|
|
466
|
+
const title = typeof input.title === "string" && input.title.trim() ? norm(input.title) : undefined;
|
|
417
467
|
const kind = normalizeLabelValue(input.kind, "kind:");
|
|
418
468
|
const topics = uniqueNormalized((input.topics ?? []).map((topic) => normalizeLabelValue(topic, "topic:")).filter(Boolean) as string[]);
|
|
419
469
|
return {
|
|
470
|
+
...(title ? { title } : {}),
|
|
420
471
|
detail,
|
|
421
472
|
...(kind ? { kind } : {}),
|
|
422
473
|
...(topics.length > 0 ? { topics } : {}),
|
|
423
474
|
};
|
|
424
475
|
}
|
|
476
|
+
|
|
425
477
|
function normalizeLabelValue(value: string | undefined, prefix: string): string | undefined {
|
|
426
478
|
if (!value) return undefined;
|
|
427
479
|
const raw = value.trim().replace(new RegExp(`^${prefix}`, "i"), "");
|
|
@@ -433,6 +485,7 @@ function normalizeLabelValue(value: string | undefined, prefix: string): string
|
|
|
433
485
|
.replace(/^-+|-+$/g, "");
|
|
434
486
|
return normalized || undefined;
|
|
435
487
|
}
|
|
488
|
+
|
|
436
489
|
function normalizeOptionalLabelValue(value: string | undefined, prefix: string): string | undefined {
|
|
437
490
|
try {
|
|
438
491
|
return normalizeLabelValue(value, prefix);
|
|
@@ -440,16 +493,22 @@ function normalizeOptionalLabelValue(value: string | undefined, prefix: string):
|
|
|
440
493
|
return undefined;
|
|
441
494
|
}
|
|
442
495
|
}
|
|
496
|
+
|
|
443
497
|
function uniqueNormalized(values: string[]): string[] {
|
|
444
498
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort();
|
|
445
499
|
}
|
|
500
|
+
|
|
446
501
|
function parseDecision(raw: string): MemoryDecision {
|
|
447
502
|
const tryParse = (s: string): MemoryDecision | null => {
|
|
448
503
|
try {
|
|
449
504
|
const p = JSON.parse(s) as Record<string, unknown>;
|
|
450
|
-
return {
|
|
451
|
-
|
|
452
|
-
|
|
505
|
+
return {
|
|
506
|
+
save: Array.isArray(p.save) ? p.save.map(parseSaveItem).filter((v): v is MemoryDraft => Boolean(v)) : [],
|
|
507
|
+
stale: Array.isArray(p.stale) ? p.stale.filter((v): v is string => typeof v === "string") : [],
|
|
508
|
+
};
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
453
512
|
};
|
|
454
513
|
const t = raw.trim();
|
|
455
514
|
return tryParse(t) ?? (() => {
|
|
@@ -459,6 +518,7 @@ function parseDecision(raw: string): MemoryDecision {
|
|
|
459
518
|
throw new Error("memory decision subagent returned invalid JSON");
|
|
460
519
|
})();
|
|
461
520
|
}
|
|
521
|
+
|
|
462
522
|
function parseSaveItem(value: unknown): MemoryDraft | null {
|
|
463
523
|
if (typeof value === "string") {
|
|
464
524
|
const detail = norm(value);
|
|
@@ -466,12 +526,13 @@ function parseSaveItem(value: unknown): MemoryDraft | null {
|
|
|
466
526
|
}
|
|
467
527
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
468
528
|
const record = value as Record<string, unknown>;
|
|
529
|
+
const title = typeof record.title === "string" ? record.title : undefined;
|
|
469
530
|
const detail = typeof record.detail === "string" ? norm(record.detail) : "";
|
|
470
531
|
if (!detail) return null;
|
|
471
532
|
const kind = typeof record.kind === "string" ? record.kind : undefined;
|
|
472
533
|
const topics = Array.isArray(record.topics) ? record.topics.filter((v): v is string => typeof v === "string") : undefined;
|
|
473
534
|
try {
|
|
474
|
-
return normalizeDraft({ detail, ...(kind ? { kind } : {}), ...(topics ? { topics } : {}) });
|
|
535
|
+
return normalizeDraft({ ...(title ? { title } : {}), detail, ...(kind ? { kind } : {}), ...(topics ? { topics } : {}) });
|
|
475
536
|
} catch {
|
|
476
537
|
return null;
|
|
477
538
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildLegacyRelevantMemoriesContext,
|
|
3
|
+
buildRelevantMemoriesSystemContext,
|
|
4
|
+
extractPromptTextForRecall,
|
|
5
|
+
resolveOpenClawHostVersion,
|
|
6
|
+
resolvePromptHookMode,
|
|
7
|
+
} from "./service.js";
|
|
8
|
+
|
|
9
|
+
function assert(condition: unknown, message: string): void {
|
|
10
|
+
if (!condition) throw new Error(message);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function testExtractPromptFromString(): void {
|
|
14
|
+
assert(extractPromptTextForRecall(" help me fix redis ") === "help me fix redis", "expected direct string prompts to be trimmed");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function testExtractPromptFromPromptField(): void {
|
|
18
|
+
assert(
|
|
19
|
+
extractPromptTextForRecall({ prompt: "Summarize the release notes." }) === "Summarize the release notes.",
|
|
20
|
+
"expected prompt field to be used when present",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function testExtractPromptFromLatestUserMessage(): void {
|
|
25
|
+
const prompt = extractPromptTextForRecall({
|
|
26
|
+
messages: [
|
|
27
|
+
{ role: "assistant", text: "How can I help?" },
|
|
28
|
+
{ role: "user", text: "Please fix the login bug." },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
assert(prompt === "Please fix the login bug.", "expected the latest user message to drive recall");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function testExtractPromptFromStructuredContent(): void {
|
|
35
|
+
const prompt = extractPromptTextForRecall({
|
|
36
|
+
messages: [
|
|
37
|
+
{
|
|
38
|
+
role: "user",
|
|
39
|
+
content: [
|
|
40
|
+
{ type: "text", text: "Check the deployment logs" },
|
|
41
|
+
{ type: "text", text: "and verify nginx." },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
assert(prompt === "Check the deployment logs\nand verify nginx.", "expected structured text content to be flattened");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function testBuildRelevantMemoriesSystemContext(): void {
|
|
50
|
+
const context = buildRelevantMemoriesSystemContext([
|
|
51
|
+
{ detail: "OpenClaw main agent identity uses Gandalf." },
|
|
52
|
+
{ detail: "Shared memories can break if the repo path changes." },
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
assert(context.includes("ClawMem relevant memories:"), "expected a human-readable heading");
|
|
56
|
+
assert(context.includes("- OpenClaw main agent identity uses Gandalf."), "expected memories to be listed as bullets");
|
|
57
|
+
assert(!context.includes("<relevant-memories>"), "expected the legacy XML wrapper to be removed");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function testBuildLegacyRelevantMemoriesContext(): void {
|
|
61
|
+
const context = buildLegacyRelevantMemoriesContext([
|
|
62
|
+
{ detail: "Use the shared repo for team memory." },
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
assert(context.includes("Relevant ClawMem memories for this request:"), "expected a legacy-safe heading");
|
|
66
|
+
assert(context.includes("- Use the shared repo for team memory."), "expected memories to stay readable");
|
|
67
|
+
assert(!context.includes("<relevant-memories>"), "expected legacy context to avoid XML wrappers too");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function testResolveHostVersionFromRuntime(): void {
|
|
71
|
+
const version = resolveOpenClawHostVersion({ runtime: { version: "2026.3.28" } } as never);
|
|
72
|
+
assert(version === "2026.3.28", "expected runtime.version to take precedence");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function testResolveHostVersionFromEnvFallback(): void {
|
|
76
|
+
const previous = {
|
|
77
|
+
OPENCLAW_VERSION: process.env.OPENCLAW_VERSION,
|
|
78
|
+
OPENCLAW_SERVICE_VERSION: process.env.OPENCLAW_SERVICE_VERSION,
|
|
79
|
+
npm_package_version: process.env.npm_package_version,
|
|
80
|
+
};
|
|
81
|
+
try {
|
|
82
|
+
delete process.env.OPENCLAW_VERSION;
|
|
83
|
+
process.env.OPENCLAW_SERVICE_VERSION = "2026.3.6";
|
|
84
|
+
delete process.env.npm_package_version;
|
|
85
|
+
const version = resolveOpenClawHostVersion({ runtime: {} } as never);
|
|
86
|
+
assert(version === "2026.3.6", "expected OPENCLAW_SERVICE_VERSION fallback");
|
|
87
|
+
} finally {
|
|
88
|
+
process.env.OPENCLAW_VERSION = previous.OPENCLAW_VERSION;
|
|
89
|
+
process.env.OPENCLAW_SERVICE_VERSION = previous.OPENCLAW_SERVICE_VERSION;
|
|
90
|
+
process.env.npm_package_version = previous.npm_package_version;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function testIgnoresNpmPackageVersionFallback(): void {
|
|
95
|
+
const previous = {
|
|
96
|
+
OPENCLAW_VERSION: process.env.OPENCLAW_VERSION,
|
|
97
|
+
OPENCLAW_SERVICE_VERSION: process.env.OPENCLAW_SERVICE_VERSION,
|
|
98
|
+
npm_package_version: process.env.npm_package_version,
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
delete process.env.OPENCLAW_VERSION;
|
|
102
|
+
delete process.env.OPENCLAW_SERVICE_VERSION;
|
|
103
|
+
process.env.npm_package_version = "2026.3.99";
|
|
104
|
+
const version = resolveOpenClawHostVersion({ runtime: {} } as never);
|
|
105
|
+
assert(version === undefined, "expected npm_package_version to be ignored for host detection");
|
|
106
|
+
} finally {
|
|
107
|
+
process.env.OPENCLAW_VERSION = previous.OPENCLAW_VERSION;
|
|
108
|
+
process.env.OPENCLAW_SERVICE_VERSION = previous.OPENCLAW_SERVICE_VERSION;
|
|
109
|
+
process.env.npm_package_version = previous.npm_package_version;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function testResolvePromptHookModeModern(): void {
|
|
114
|
+
const mode = resolvePromptHookMode({ runtime: { version: "2026.3.28" } } as never);
|
|
115
|
+
assert(mode === "modern", "expected modern hook mode for OpenClaw 2026.3.28");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function testResolvePromptHookModeLegacy(): void {
|
|
119
|
+
const mode = resolvePromptHookMode({ runtime: { version: "2026.3.6" } } as never);
|
|
120
|
+
assert(mode === "legacy", "expected legacy hook mode before 2026.3.7");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function testResolvePromptHookModeLegacyForUnknownVersion(): void {
|
|
124
|
+
const mode = resolvePromptHookMode({ runtime: {} } as never);
|
|
125
|
+
assert(mode === "legacy", "expected unknown host versions to fall back to legacy mode");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
testExtractPromptFromString();
|
|
129
|
+
testExtractPromptFromPromptField();
|
|
130
|
+
testExtractPromptFromLatestUserMessage();
|
|
131
|
+
testExtractPromptFromStructuredContent();
|
|
132
|
+
testBuildRelevantMemoriesSystemContext();
|
|
133
|
+
testBuildLegacyRelevantMemoriesContext();
|
|
134
|
+
testResolveHostVersionFromRuntime();
|
|
135
|
+
testResolveHostVersionFromEnvFallback();
|
|
136
|
+
testIgnoresNpmPackageVersionFallback();
|
|
137
|
+
testResolvePromptHookModeModern();
|
|
138
|
+
testResolvePromptHookModeLegacy();
|
|
139
|
+
testResolvePromptHookModeLegacyForUnknownVersion();
|
|
140
|
+
|
|
141
|
+
console.log("service tests passed");
|