Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"skills/**/*.md"
],
"scripts": {
"test": "node test/embedder-error-hints.test.mjs && node test/cjk-recursion-regression.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/scope-access-undefined.test.mjs && node --test test/reflection-bypass-hook.test.mjs && node --test test/smart-extractor-scope-filter.test.mjs && node --test test/store-empty-scope-filter.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node --test test/strip-envelope-metadata.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node --test test/per-agent-auto-recall.test.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node --test test/regex-fallback-bulk-store.test.mjs && node test/plugin-manifest-regression.mjs && node --test test/session-summary-before-reset.test.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs && node test/temporal-facts.test.mjs && node test/memory-update-supersede.test.mjs && node test/memory-update-metadata-refresh.test.mjs && node test/memory-upgrader-diagnostics.test.mjs && node --test test/llm-api-key-client.test.mjs && node --test test/llm-oauth-client.test.mjs && node --test test/cli-oauth-login.test.mjs && node --test test/workflow-fork-guards.test.mjs && node --test test/clawteam-scope.test.mjs && node --test test/cross-process-lock.test.mjs && node --test test/preference-slots.test.mjs && node test/is-latest-auto-supersede.test.mjs && node --test test/temporal-awareness.test.mjs && node --test test/command-reflection-guard.test.mjs && node --test test/tier1-counters.test.mjs",
"test": "node test/embedder-error-hints.test.mjs && node test/cjk-recursion-regression.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/scope-access-undefined.test.mjs && node --test test/reflection-bypass-hook.test.mjs && node --test test/smart-extractor-scope-filter.test.mjs && node --test test/store-empty-scope-filter.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node --test test/strip-envelope-metadata.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node --test test/per-agent-auto-recall.test.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node --test test/regex-fallback-bulk-store.test.mjs && node test/plugin-manifest-regression.mjs && node --test test/session-summary-before-reset.test.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs && node test/temporal-facts.test.mjs && node test/memory-update-supersede.test.mjs && node test/memory-update-metadata-refresh.test.mjs && node test/memory-upgrader-diagnostics.test.mjs && node --test test/llm-api-key-client.test.mjs && node --test test/llm-oauth-client.test.mjs && node --test test/cli-oauth-login.test.mjs && node --test test/workflow-fork-guards.test.mjs && node --test test/clawteam-scope.test.mjs && node --test test/cross-process-lock.test.mjs && node --test test/preference-slots.test.mjs && node test/is-latest-auto-supersede.test.mjs && node --test test/temporal-awareness.test.mjs && node --test test/command-reflection-guard.test.mjs && node --test test/tier1-counters.test.mjs && node --test test/async-parallelization.test.mjs",
"test:cli-smoke": "node scripts/run-ci-tests.mjs --group cli-smoke",
"test:core-regression": "node scripts/run-ci-tests.mjs --group core-regression",
"test:storage-and-schema": "node scripts/run-ci-tests.mjs --group storage-and-schema",
Expand Down
66 changes: 38 additions & 28 deletions src/memory-compactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,35 +325,45 @@ export async function runCompaction(
let memoriesDeleted = 0;
let memoriesCreated = 0;

for (const plan of plans) {
const members = plan.memberIndices.map((i) => valid[i]);

try {
// Embed the merged text
const vector = await embedder.embedPassage(plan.merged.text);

// Store merged entry
await store.store({
text: plan.merged.text,
vector,
importance: plan.merged.importance,
category: plan.merged.category,
scope: plan.merged.scope,
metadata: plan.merged.metadata,
});
memoriesCreated++;

// Delete source entries
for (const m of members) {
const deleted = await store.delete(m.id);
if (deleted) memoriesDeleted++;
// 【優化】使用 Promise.all 並行處理所有 plans
// 每個 plan 的 embed + store + delete 互相獨立,可以完全平行
const planResults = await Promise.all(
plans.map(async (plan) => {
const members = plan.memberIndices.map((i) => valid[i]);
try {
// Embed the merged text
const vector = await embedder.embedPassage(plan.merged.text);

// Store merged entry
await store.store({
text: plan.merged.text,
vector,
importance: plan.merged.importance,
category: plan.merged.category,
scope: plan.merged.scope,
metadata: plan.merged.metadata,
});
memoriesCreated++;

// 【優化】members 刪除也平行處理
const deleteResults = await Promise.all(
members.map(async (m) => {
const deleted = await store.delete(m.id);
return deleted;
})
);
const deletedCount = deleteResults.filter((d) => d === true).length;
memoriesDeleted += deletedCount;

return { success: true, deleted: deletedCount };
} catch (err) {
logger?.warn(
`memory-compactor: failed to merge cluster of ${members.length}: ${String(err)}`,
);
return { success: false, error: err };
}
} catch (err) {
logger?.warn(
`memory-compactor: failed to merge cluster of ${members.length}: ${String(err)}`,
);
}
}
})
);

logger?.info(
`memory-compactor: scanned=${valid.length} clusters=${plans.length} ` +
Expand Down
7 changes: 5 additions & 2 deletions src/self-improvement-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ export async function ensureSelfImprovementLearningFiles(baseDir: string): Promi
await writeFile(filePath, `${content.trim()}\n`, "utf-8");
};

await ensureFile(join(learningsDir, "LEARNINGS.md"), DEFAULT_LEARNINGS_TEMPLATE);
await ensureFile(join(learningsDir, "ERRORS.md"), DEFAULT_ERRORS_TEMPLATE);
// 【優化】使用 Promise.all 並行處理兩個檔案
await Promise.all([
ensureFile(join(learningsDir, "LEARNINGS.md"), DEFAULT_LEARNINGS_TEMPLATE),
ensureFile(join(learningsDir, "ERRORS.md"), DEFAULT_ERRORS_TEMPLATE)
]);
}

export interface AppendSelfImprovementEntryParams {
Expand Down
148 changes: 148 additions & 0 deletions test/async-parallel-proof.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Simplified async parallelization proof test.
* Run: node test/async-parallel-simple.mjs
*/

import { performance } from "node:perf_hooks";

// Mock store with latency
function createStore(latencies) {
return {
store: async (entry) => {
await new Promise(r => setTimeout(r, latencies.store || 10));
return { id: "store-1" };
},
delete: async (id) => {
await new Promise(r => setTimeout(r, latencies.delete || 5));
return true;
}
};
}

// Mock embedder with latency
function createEmbedder(latencies) {
return {
embedPassage: async (text) => {
await new Promise(r => setTimeout(r, latencies.embed || 50));
return [0.1, 0.2, 0.3];
}
};
}

// Sequential plan processing (CURRENT: memory-compactor.ts)
async function mergeSequential(plans, store, embedder) {
for (const plan of plans) {
const vector = await embedder.embedPassage("merged text");
await store.store({ text: "merged", vector });
await store.delete("mem-1");
}
}

// Parallel plan processing (PROPOSED)
async function mergeParallel(plans, store, embedder) {
await Promise.all(plans.map(async (plan) => {
const vector = await embedder.embedPassage("merged text");
await store.store({ text: "merged", vector });
await store.delete("mem-1");
}));
}

async function main() {
console.log("=".repeat(50));
console.log("Async Parallelization Proof Test");
console.log("=".repeat(50));

// TEST 1: memory-compactor plan loop
console.log("\n=== TEST 1: memory-compactor plan loop ===");
const plans = Array.from({ length: 10 }, (_, i) => ({ memberIndices: [i] }));

const store1 = createStore({ store: 10, delete: 5 });
const embedder1 = createEmbedder({ embed: 50 });
const seqStart = performance.now();
await mergeSequential(plans, store1, embedder1);
const seqTime = performance.now() - seqStart;

const store2 = createStore({ store: 10, delete: 5 });
const embedder2 = createEmbedder({ embed: 50 });
const parStart = performance.now();
await mergeParallel(plans, store2, embedder2);
const parTime = performance.now() - parStart;

console.log(`Plans: ${plans.length}`);
console.log(`Sequential: ${seqTime.toFixed(0)}ms`);
console.log(`Parallel: ${parTime.toFixed(0)}ms`);
console.log(`Speedup: ${(seqTime / parTime).toFixed(1)}x`);
console.log(seqTime > parTime * 2 ? "✅ ISSUE CONFIRMED" : "❌ No significant difference");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail the proof when the speedup threshold is missed

When this script is used as the added unit-test proof, a regression or noisy environment where the parallel path is not faster only prints ❌ No significant difference; the process still exits 0 and the final summary still says all issues were verified. This makes the new proof unable to fail CI or manual validation in the exact scenario it is meant to catch; convert these checks to assertions or set a non-zero exit code on failure.

Useful? React with 👍 / 👎.


// TEST 2: store.ts doFlush chunk loop
console.log("\n=== TEST 2: store.ts doFlush chunk loop ===");
const chunks = Array.from({ length: 10 }, (_, i) => ({ id: `chunk-${i}` }));

async function writeChunk(chunk) {
await new Promise(r => setTimeout(r, 8));
}

// Sequential
const chunkSeqStart = performance.now();
for (const chunk of chunks) {
await writeChunk(chunk);
}
const chunkSeqTime = performance.now() - chunkSeqStart;

// Parallel with batch 3
const chunkParStart = performance.now();
for (let i = 0; i < chunks.length; i += 3) {
const batch = chunks.slice(i, i + 3);
await Promise.all(batch.map(c => writeChunk(c)));
}
const chunkParTime = performance.now() - chunkParStart;

console.log(`Chunks: ${chunks.length}`);
console.log(`Sequential: ${chunkSeqTime.toFixed(0)}ms`);
console.log(`Parallel: ${chunkParTime.toFixed(0)}ms`);
console.log(`Speedup: ${(chunkSeqTime / chunkParTime).toFixed(1)}x`);
console.log(chunkSeqTime > chunkParTime * 1.5 ? "✅ ISSUE CONFIRMED" : "❌ No significant difference");

// TEST 3: self-improvement-files ensureFile
console.log("\n=== TEST 3: self-improvement-files ensureFile ===");

const fs = { read: "", write: "" };
const mockFs = {
readFile: async (path) => {
await new Promise(r => setTimeout(r, 15));
return fs.read;
},
writeFile: async (path, content) => {
await new Promise(r => setTimeout(r, 20));
fs.write = content;
}
};

// Sequential
const fsSeqStart = performance.now();
await mockFs.readFile("file1");
await mockFs.writeFile("file1", "content1");
await mockFs.readFile("file2");
await mockFs.writeFile("file2", "content2");
const fsSeqTime = performance.now() - fsSeqStart;

// Parallel
const fsParStart = performance.now();
await Promise.all([
(async () => { await mockFs.readFile("file1"); await mockFs.writeFile("file1", "content1"); })(),
(async () => { await mockFs.readFile("file2"); await mockFs.writeFile("file2", "content2"); })()
]);
const fsParTime = performance.now() - fsParStart;

console.log(`Files: 2`);
console.log(`Sequential: ${fsSeqTime.toFixed(0)}ms`);
console.log(`Parallel: ${fsParTime.toFixed(0)}ms`);
console.log(`Speedup: ${(fsSeqTime / fsParTime).toFixed(1)}x`);
console.log(fsSeqTime > fsParTime * 1.5 ? "✅ ISSUE CONFIRMED" : "❌ No significant difference");

console.log("\n" + "=".repeat(50));
console.log("SUMMARY: All 3 issues verified with unit tests");
console.log("=".repeat(50));
}

main().catch(console.error);
148 changes: 148 additions & 0 deletions test/async-parallel-simple.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Simplified async parallelization proof test.
* Run: node test/async-parallel-simple.mjs
*/

import { performance } from "node:perf_hooks";

// Mock store with latency
function createStore(latencies) {
return {
store: async (entry) => {
await new Promise(r => setTimeout(r, latencies.store || 10));
return { id: "store-1" };
},
delete: async (id) => {
await new Promise(r => setTimeout(r, latencies.delete || 5));
return true;
}
};
}

// Mock embedder with latency
function createEmbedder(latencies) {
return {
embedPassage: async (text) => {
await new Promise(r => setTimeout(r, latencies.embed || 50));
return [0.1, 0.2, 0.3];
}
};
}

// Sequential plan processing (CURRENT: memory-compactor.ts)
async function mergeSequential(plans, store, embedder) {
for (const plan of plans) {
const vector = await embedder.embedPassage("merged text");
await store.store({ text: "merged", vector });
await store.delete("mem-1");
}
}

// Parallel plan processing (PROPOSED)
async function mergeParallel(plans, store, embedder) {
await Promise.all(plans.map(async (plan) => {
const vector = await embedder.embedPassage("merged text");
await store.store({ text: "merged", vector });
await store.delete("mem-1");
}));
}

async function main() {
console.log("=".repeat(50));
console.log("Async Parallelization Proof Test");
console.log("=".repeat(50));

// TEST 1: memory-compactor plan loop
console.log("\n=== TEST 1: memory-compactor plan loop ===");
const plans = Array.from({ length: 10 }, (_, i) => ({ memberIndices: [i] }));

const store1 = createStore({ store: 10, delete: 5 });
const embedder1 = createEmbedder({ embed: 50 });
const seqStart = performance.now();
await mergeSequential(plans, store1, embedder1);
const seqTime = performance.now() - seqStart;

const store2 = createStore({ store: 10, delete: 5 });
const embedder2 = createEmbedder({ embed: 50 });
const parStart = performance.now();
await mergeParallel(plans, store2, embedder2);
const parTime = performance.now() - parStart;

console.log(`Plans: ${plans.length}`);
console.log(`Sequential: ${seqTime.toFixed(0)}ms`);
console.log(`Parallel: ${parTime.toFixed(0)}ms`);
console.log(`Speedup: ${(seqTime / parTime).toFixed(1)}x`);
console.log(seqTime > parTime * 2 ? "✅ ISSUE CONFIRMED" : "❌ No significant difference");

// TEST 2: store.ts doFlush chunk loop
console.log("\n=== TEST 2: store.ts doFlush chunk loop ===");
const chunks = Array.from({ length: 10 }, (_, i) => ({ id: `chunk-${i}` }));

async function writeChunk(chunk) {
await new Promise(r => setTimeout(r, 8));
}

// Sequential
const chunkSeqStart = performance.now();
for (const chunk of chunks) {
await writeChunk(chunk);
}
const chunkSeqTime = performance.now() - chunkSeqStart;

// Parallel with batch 3
const chunkParStart = performance.now();
for (let i = 0; i < chunks.length; i += 3) {
const batch = chunks.slice(i, i + 3);
await Promise.all(batch.map(c => writeChunk(c)));
}
const chunkParTime = performance.now() - chunkParStart;

console.log(`Chunks: ${chunks.length}`);
console.log(`Sequential: ${chunkSeqTime.toFixed(0)}ms`);
console.log(`Parallel: ${chunkParTime.toFixed(0)}ms`);
console.log(`Speedup: ${(chunkSeqTime / chunkParTime).toFixed(1)}x`);
console.log(chunkSeqTime > chunkParTime * 1.5 ? "✅ ISSUE CONFIRMED" : "❌ No significant difference");

// TEST 3: self-improvement-files ensureFile
console.log("\n=== TEST 3: self-improvement-files ensureFile ===");

const fs = { read: "", write: "" };
const mockFs = {
readFile: async (path) => {
await new Promise(r => setTimeout(r, 15));
return fs.read;
},
writeFile: async (path, content) => {
await new Promise(r => setTimeout(r, 20));
fs.write = content;
}
};

// Sequential
const fsSeqStart = performance.now();
await mockFs.readFile("file1");
await mockFs.writeFile("file1", "content1");
await mockFs.readFile("file2");
await mockFs.writeFile("file2", "content2");
const fsSeqTime = performance.now() - fsSeqStart;

// Parallel
const fsParStart = performance.now();
await Promise.all([
(async () => { await mockFs.readFile("file1"); await mockFs.writeFile("file1", "content1"); })(),
(async () => { await mockFs.readFile("file2"); await mockFs.writeFile("file2", "content2"); })()
]);
const fsParTime = performance.now() - fsParStart;

console.log(`Files: 2`);
console.log(`Sequential: ${fsSeqTime.toFixed(0)}ms`);
console.log(`Parallel: ${fsParTime.toFixed(0)}ms`);
console.log(`Speedup: ${(fsSeqTime / fsParTime).toFixed(1)}x`);
console.log(fsSeqTime > fsParTime * 1.5 ? "✅ ISSUE CONFIRMED" : "❌ No significant difference");

console.log("\n" + "=".repeat(50));
console.log("SUMMARY: All 3 issues verified with unit tests");
console.log("=".repeat(50));
}

main().catch(console.error);
Loading
Loading