Skip to content

Commit 16ca3c9

Browse files
committed
Merge remote-tracking branch 'origin/feat/clawxmemory' into tool-governor
2 parents e486f53 + c60bc32 commit 16ca3c9

17 files changed

Lines changed: 9554 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ apps/macos/.build-local/
4747
apps/macos/.swiftpm/
4848
apps/shared/MoltbotKit/.swiftpm/
4949
apps/shared/OpenClawKit/.swiftpm/
50-
Core/
50+
/Core/
5151
apps/ios/*.xcodeproj/
5252
apps/ios/*.xcworkspace/
5353
apps/ios/.swiftpm/
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export * from "./types.js";
2+
export * from "./utils/id.js";
3+
export * from "./utils/text.js";
4+
export * from "./skills/types.js";
5+
export * from "./skills/defaults.js";
6+
export * from "./skills/loader.js";
7+
export * from "./skills/llm-extraction.js";
8+
export * from "./skills/intent-skill.js";
9+
export * from "./indexers/l1-extractor.js";
10+
export * from "./indexers/l2-builder.js";
11+
export * from "./review/dream-review.js";
12+
export * from "./pipeline/heartbeat.js";
13+
export * from "./retrieval/reasoning-loop.js";
14+
export * from "./storage/sqlite.js";
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { LlmMemoryExtractor } from "../skills/llm-extraction.js";
2+
import type { L0SessionRecord, L1WindowRecord, MemoryMessage } from "../types.js";
3+
import { buildL1IndexId, nowIso } from "../utils/id.js";
4+
5+
function sameMessage(left: MemoryMessage | undefined, right: MemoryMessage | undefined): boolean {
6+
if (!left || !right) return false;
7+
return left.role === right.role && left.content === right.content;
8+
}
9+
10+
function startsWithMessages(list: MemoryMessage[], prefix: MemoryMessage[]): boolean {
11+
if (prefix.length > list.length) return false;
12+
for (let index = 0; index < prefix.length; index += 1) {
13+
if (!sameMessage(list[index], prefix[index])) return false;
14+
}
15+
return true;
16+
}
17+
18+
function findOverlap(existing: MemoryMessage[], incoming: MemoryMessage[]): number {
19+
const max = Math.min(existing.length, incoming.length);
20+
for (let size = max; size > 0; size -= 1) {
21+
let matched = true;
22+
for (let index = 0; index < size; index += 1) {
23+
if (!sameMessage(existing[existing.length - size + index], incoming[index])) {
24+
matched = false;
25+
break;
26+
}
27+
}
28+
if (matched) return size;
29+
}
30+
return 0;
31+
}
32+
33+
function dedupeAdjacentMessages(messages: MemoryMessage[]): MemoryMessage[] {
34+
const merged: MemoryMessage[] = [];
35+
for (const message of messages) {
36+
const previous = merged[merged.length - 1];
37+
if (sameMessage(previous, message)) continue;
38+
merged.push(message);
39+
}
40+
return merged;
41+
}
42+
43+
function mergeMessageStream(existing: MemoryMessage[], incoming: MemoryMessage[]): MemoryMessage[] {
44+
if (incoming.length === 0) return existing;
45+
if (existing.length === 0) return dedupeAdjacentMessages(incoming);
46+
if (startsWithMessages(incoming, existing)) return dedupeAdjacentMessages(incoming);
47+
if (startsWithMessages(existing, incoming)) return dedupeAdjacentMessages(existing);
48+
const overlap = findOverlap(existing, incoming);
49+
return dedupeAdjacentMessages([...existing, ...incoming.slice(overlap)]);
50+
}
51+
52+
function mergeWindowMessages(records: L0SessionRecord[]): MemoryMessage[] {
53+
return records.reduce<MemoryMessage[]>(
54+
(combined, record) => mergeMessageStream(combined, record.messages),
55+
[],
56+
);
57+
}
58+
59+
function buildTimePeriod(startedAt: string, endedAt: string): string {
60+
const start = new Date(startedAt);
61+
const end = new Date(endedAt);
62+
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return "unknown";
63+
const yyyyMmDd = (date: Date): string => date.toISOString().slice(0, 10);
64+
const hhMm = (date: Date): string =>
65+
`${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
66+
if (yyyyMmDd(start) === yyyyMmDd(end)) {
67+
return `${yyyyMmDd(start)} ${hhMm(start)}-${hhMm(end)}`;
68+
}
69+
return `${yyyyMmDd(start)} ${hhMm(start)} -> ${yyyyMmDd(end)} ${hhMm(end)}`;
70+
}
71+
72+
export async function extractL1FromWindow(
73+
records: L0SessionRecord[],
74+
extractor: LlmMemoryExtractor,
75+
): Promise<L1WindowRecord> {
76+
if (records.length === 0) {
77+
throw new Error("Cannot build L1 window from empty L0 records");
78+
}
79+
const ordered = [...records].sort((left, right) => left.timestamp.localeCompare(right.timestamp));
80+
const startedAt = ordered[0]!.timestamp;
81+
const endedAt = ordered[ordered.length - 1]!.timestamp;
82+
const mergedMessages = mergeWindowMessages(ordered);
83+
const extracted = await extractor.extract({
84+
timestamp: endedAt,
85+
messages: mergedMessages,
86+
});
87+
const l0Source = ordered.map((record) => record.l0IndexId);
88+
const l1IndexId = buildL1IndexId(startedAt, l0Source);
89+
return {
90+
l1IndexId,
91+
sessionKey: ordered[0]!.sessionKey,
92+
timePeriod: buildTimePeriod(startedAt, endedAt),
93+
startedAt,
94+
endedAt,
95+
summary: extracted.summary,
96+
facts: extracted.facts,
97+
situationTimeInfo: extracted.situationTimeInfo,
98+
projectTags: extracted.projectDetails.map((item) => item.name),
99+
projectDetails: extracted.projectDetails,
100+
l0Source,
101+
createdAt: nowIso(),
102+
};
103+
}
104+
105+
export async function extractL1FromL0(
106+
record: L0SessionRecord,
107+
extractor: LlmMemoryExtractor,
108+
): Promise<L1WindowRecord> {
109+
return extractL1FromWindow([record], extractor);
110+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type {
2+
L1WindowRecord,
3+
L2ProjectIndexRecord,
4+
L2TimeIndexRecord,
5+
ProjectDetail,
6+
} from "../types.js";
7+
import { buildL2ProjectIndexId, buildL2TimeIndexId, nowIso } from "../utils/id.js";
8+
9+
function formatLocalDayKey(value: string): string {
10+
const date = new Date(value);
11+
if (Number.isNaN(date.getTime())) return value.slice(0, 10) || "unknown-day";
12+
const year = String(date.getFullYear());
13+
const month = String(date.getMonth() + 1).padStart(2, "0");
14+
const day = String(date.getDate()).padStart(2, "0");
15+
return `${year}-${month}-${day}`;
16+
}
17+
18+
export function buildL2TimeFromL1(l1: L1WindowRecord, summary: string): L2TimeIndexRecord {
19+
const dateKey = formatLocalDayKey(l1.startedAt || l1.endedAt || l1.createdAt);
20+
const now = nowIso();
21+
return {
22+
l2IndexId: buildL2TimeIndexId(dateKey),
23+
dateKey,
24+
summary,
25+
l1Source: [l1.l1IndexId],
26+
createdAt: now,
27+
updatedAt: now,
28+
};
29+
}
30+
31+
export function buildL2ProjectFromDetail(
32+
project: ProjectDetail,
33+
l1IndexId: string,
34+
): L2ProjectIndexRecord {
35+
const now = nowIso();
36+
return {
37+
l2IndexId: buildL2ProjectIndexId(project.key),
38+
projectKey: project.key,
39+
projectName: project.name,
40+
summary: project.summary,
41+
currentStatus: project.status,
42+
latestProgress: project.latestProgress,
43+
l1Source: [l1IndexId],
44+
createdAt: now,
45+
updatedAt: now,
46+
};
47+
}
48+
49+
export function buildL2ProjectsFromL1(l1: L1WindowRecord): L2ProjectIndexRecord[] {
50+
return l1.projectDetails.map((project) => buildL2ProjectFromDetail(project, l1.l1IndexId));
51+
}

0 commit comments

Comments
 (0)