mono-pilot 0.2.10 → 0.2.13
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 +260 -2
- package/dist/src/agents-paths.js +36 -0
- package/dist/src/brief/blocks.js +83 -0
- package/dist/src/brief/defaults.js +60 -0
- package/dist/src/brief/frontmatter.js +53 -0
- package/dist/src/brief/paths.js +10 -0
- package/dist/src/brief/reflection.js +27 -0
- package/dist/src/cli.js +62 -5
- package/dist/src/cluster/bus.js +102 -0
- package/dist/src/cluster/follower.js +137 -0
- package/dist/src/cluster/init.js +182 -0
- package/dist/src/cluster/leader.js +97 -0
- package/dist/src/cluster/log.js +49 -0
- package/dist/src/cluster/protocol.js +34 -0
- package/dist/src/cluster/services/bus.js +243 -0
- package/dist/src/cluster/services/embedding.js +12 -0
- package/dist/src/cluster/socket.js +86 -0
- package/dist/src/cluster/test-bus.js +175 -0
- package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
- package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
- package/dist/src/cluster_v2/connection.js +159 -0
- package/dist/src/cluster_v2/connection.test.js +55 -0
- package/dist/src/cluster_v2/events.js +102 -0
- package/dist/src/cluster_v2/index.js +2 -0
- package/dist/src/cluster_v2/observability.js +99 -0
- package/dist/src/cluster_v2/observability.test.js +46 -0
- package/dist/src/cluster_v2/rpc.js +389 -0
- package/dist/src/cluster_v2/rpc.test.js +110 -0
- package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
- package/dist/src/cluster_v2/runtime.js +531 -0
- package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
- package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
- package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
- package/dist/src/cluster_v2/services/bus.js +450 -0
- package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
- package/dist/src/cluster_v2/services/discord/collector.js +569 -0
- package/dist/src/cluster_v2/services/discord/index.js +1 -0
- package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
- package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
- package/dist/src/cluster_v2/services/embedding.js +66 -0
- package/dist/src/cluster_v2/services/registry-cache.js +107 -0
- package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
- package/dist/src/cluster_v2/services/registry.js +36 -0
- package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
- package/dist/src/cluster_v2/services/twitter/index.js +1 -0
- package/dist/src/config/digest.js +78 -0
- package/dist/src/config/discord.js +143 -0
- package/dist/src/config/image-gen.js +48 -0
- package/dist/src/config/mono-pilot.js +31 -0
- package/dist/src/config/twitter.js +100 -0
- package/dist/src/extensions/cluster.js +311 -0
- package/dist/src/extensions/commands/build-memory.js +76 -0
- package/dist/src/extensions/commands/digest/backfill.js +779 -0
- package/dist/src/extensions/commands/digest/index.js +1133 -0
- package/dist/src/extensions/commands/image-model.js +214 -0
- package/dist/src/extensions/game/bus-injection.js +47 -0
- package/dist/src/extensions/game/identity.js +83 -0
- package/dist/src/extensions/game/mailbox.js +61 -0
- package/dist/src/extensions/game/system-prompt.js +134 -0
- package/dist/src/extensions/game/tools.js +28 -0
- package/dist/src/extensions/lifecycle.js +337 -0
- package/dist/src/extensions/mode-runtime.js +26 -2
- package/dist/src/extensions/mono-game.js +66 -0
- package/dist/src/extensions/mono-pilot.js +100 -18
- package/dist/src/extensions/nvim.js +47 -0
- package/dist/src/extensions/session-hints.js +1 -2
- package/dist/src/extensions/sftp.js +897 -0
- package/dist/src/extensions/status.js +676 -0
- package/dist/src/extensions/system-events.js +478 -0
- package/dist/src/extensions/system-prompt.js +24 -14
- package/dist/src/extensions/user-message.js +70 -1
- package/dist/src/lsp/client.js +235 -0
- package/dist/src/lsp/index.js +165 -0
- package/dist/src/lsp/runtime.js +67 -0
- package/dist/src/lsp/server.js +242 -0
- package/dist/src/memory/build-memory.js +103 -0
- package/dist/src/memory/config/defaults.js +55 -0
- package/dist/src/memory/config/loader.js +29 -0
- package/dist/src/memory/config/paths.js +9 -0
- package/dist/src/memory/config/resolve.js +90 -0
- package/dist/src/memory/config/types.js +1 -0
- package/dist/src/memory/embeddings/batch-runner.js +39 -0
- package/dist/src/memory/embeddings/cache.js +47 -0
- package/dist/src/memory/embeddings/chunk-limits.js +26 -0
- package/dist/src/memory/embeddings/input-limits.js +48 -0
- package/dist/src/memory/embeddings/local.js +108 -0
- package/dist/src/memory/embeddings/types.js +1 -0
- package/dist/src/memory/index-manager.js +552 -0
- package/dist/src/memory/indexing/embeddings.js +67 -0
- package/dist/src/memory/indexing/files.js +180 -0
- package/dist/src/memory/indexing/index-file.js +105 -0
- package/dist/src/memory/log.js +38 -0
- package/dist/src/memory/paths.js +15 -0
- package/dist/src/memory/runtime/index.js +299 -0
- package/dist/src/memory/runtime/thread.js +116 -0
- package/dist/src/memory/search/fts.js +57 -0
- package/dist/src/memory/search/hybrid.js +50 -0
- package/dist/src/memory/search/text.js +30 -0
- package/dist/src/memory/search/vector.js +43 -0
- package/dist/src/memory/session/content-hash.js +7 -0
- package/dist/src/memory/session/entry.js +33 -0
- package/dist/src/memory/session/flush-policy.js +34 -0
- package/dist/src/memory/session/hook.js +191 -0
- package/dist/src/memory/session/paths.js +15 -0
- package/dist/src/memory/session/session-reader.js +88 -0
- package/dist/src/memory/session/transcript/content-hash.js +7 -0
- package/dist/src/memory/session/transcript/entry.js +28 -0
- package/dist/src/memory/session/transcript/flush.js +56 -0
- package/dist/src/memory/session/transcript/paths.js +28 -0
- package/dist/src/memory/session/transcript/reader.js +112 -0
- package/dist/src/memory/session/transcript/state.js +31 -0
- package/dist/src/memory/store/schema.js +89 -0
- package/dist/src/memory/store/sqlite.js +89 -0
- package/dist/src/memory/types.js +1 -0
- package/dist/src/memory/warm.js +25 -0
- package/dist/{tools → src/tools}/README.md +28 -2
- package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
- package/dist/{tools → src/tools}/apply-patch.js +174 -104
- package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
- package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
- package/dist/src/tools/ast-grep.js +357 -0
- package/dist/src/tools/brief-write.js +122 -0
- package/dist/src/tools/bus-send.js +100 -0
- package/dist/{tools → src/tools}/call-mcp-tool.js +20 -24
- package/dist/src/tools/codex-apply-patch-description.md +52 -0
- package/dist/src/tools/codex-apply-patch.js +540 -0
- package/dist/{tools → src/tools}/delete.js +24 -0
- package/dist/src/tools/exit-plan-mode.js +83 -0
- package/dist/{tools → src/tools}/fetch-mcp-resource.js +31 -3
- package/dist/src/tools/generate-image.js +567 -0
- package/dist/{tools → src/tools}/glob.js +55 -1
- package/dist/{tools → src/tools}/list-mcp-resources.js +32 -3
- package/dist/{tools → src/tools}/list-mcp-tools.js +38 -3
- package/dist/src/tools/ls.js +48 -0
- package/dist/src/tools/lsp-diagnostics.js +67 -0
- package/dist/src/tools/lsp-symbols.js +54 -0
- package/dist/src/tools/mailbox.js +85 -0
- package/dist/src/tools/memory-get.js +90 -0
- package/dist/src/tools/memory-search.js +180 -0
- package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
- package/dist/{tools → src/tools}/read-file.js +8 -19
- package/dist/{tools → src/tools}/rg.js +10 -20
- package/dist/{tools → src/tools}/shell.js +19 -42
- package/dist/{tools → src/tools}/subagent.js +255 -6
- package/dist/{tools → src/tools}/switch-mode.js +37 -6
- package/dist/{tools → src/tools}/web-fetch.js +105 -7
- package/dist/{tools → src/tools}/web-search.js +29 -1
- package/package.json +21 -9
- package/dist/src/utils/mcp-client.js +0 -282
- /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
- /package/dist/{tools → src/tools}/rg.test.js +0 -0
- /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
- /package/dist/{tools → src/tools}/semantic-search.js +0 -0
- /package/dist/{tools → src/tools}/shell-description.md +0 -0
- /package/dist/{tools → src/tools}/subagent-description.md +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { MemoryIndexManager } from "../index-manager.js";
|
|
3
|
+
import { memoryLog } from "../log.js";
|
|
4
|
+
function post(msg) {
|
|
5
|
+
parentPort?.postMessage(msg);
|
|
6
|
+
}
|
|
7
|
+
// Embed requests are forwarded to main thread via postMessage.
|
|
8
|
+
// Each request gets a unique id; main thread replies with the result.
|
|
9
|
+
let embedNextId = 1;
|
|
10
|
+
const embedPending = new Map();
|
|
11
|
+
const embedViaMainThread = (texts) => {
|
|
12
|
+
const id = embedNextId++;
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
embedPending.set(id, { resolve, reject });
|
|
15
|
+
post({ type: "embed", id, texts });
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
let manager = null;
|
|
19
|
+
let dirtyTimer = null;
|
|
20
|
+
try {
|
|
21
|
+
const init = workerData;
|
|
22
|
+
let lastDirty = true;
|
|
23
|
+
manager = new MemoryIndexManager({
|
|
24
|
+
agentId: init.agentId,
|
|
25
|
+
workspaceDir: init.workspaceDir,
|
|
26
|
+
settings: init.settings,
|
|
27
|
+
embedFn: embedViaMainThread,
|
|
28
|
+
embedModel: init.embedModel,
|
|
29
|
+
onAutoSyncEvent: (event) => {
|
|
30
|
+
post({ type: "autoSyncEvent", event });
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
memoryLog.info("worker initialized", { agentId: init.agentId });
|
|
34
|
+
// Periodically broadcast dirty state so the proxy can cache it.
|
|
35
|
+
const DIRTY_POLL_MS = 2000;
|
|
36
|
+
dirtyTimer = setInterval(() => {
|
|
37
|
+
const dirty = manager?.isDirty?.() ?? false;
|
|
38
|
+
if (dirty !== lastDirty) {
|
|
39
|
+
lastDirty = dirty;
|
|
40
|
+
post({ type: "dirty", value: dirty });
|
|
41
|
+
}
|
|
42
|
+
}, DIRTY_POLL_MS);
|
|
43
|
+
dirtyTimer.unref();
|
|
44
|
+
parentPort?.on("message", (msg) => {
|
|
45
|
+
// Embed response from main thread
|
|
46
|
+
if (msg.type === "embedResult") {
|
|
47
|
+
const { id, data, error } = msg;
|
|
48
|
+
const pending = embedPending.get(id);
|
|
49
|
+
if (pending) {
|
|
50
|
+
embedPending.delete(id);
|
|
51
|
+
if (error)
|
|
52
|
+
pending.reject(new Error(error));
|
|
53
|
+
else
|
|
54
|
+
pending.resolve(data);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
void handleRequest(msg);
|
|
59
|
+
});
|
|
60
|
+
post({ type: "ready" });
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
// Initialization failed — let the process crash so the proxy's exit handler fires.
|
|
64
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
65
|
+
console.error(`[memory-worker] init failed: ${message}`);
|
|
66
|
+
memoryLog.error("worker init failed", { error: message });
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
async function handleRequest(req) {
|
|
70
|
+
if (!manager) {
|
|
71
|
+
post({ id: req.id, type: "error", message: "Manager not initialized" });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
let result;
|
|
76
|
+
switch (req.type) {
|
|
77
|
+
case "search":
|
|
78
|
+
result = await manager.search(req.query, req.opts);
|
|
79
|
+
break;
|
|
80
|
+
case "sync":
|
|
81
|
+
await manager.sync?.({
|
|
82
|
+
...req.opts,
|
|
83
|
+
onWorkDetected: req.notifyWorkDetected
|
|
84
|
+
? () => {
|
|
85
|
+
post({ type: "syncWorkDetected", requestId: req.id });
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
});
|
|
89
|
+
result = undefined;
|
|
90
|
+
break;
|
|
91
|
+
case "syncDirty":
|
|
92
|
+
if (manager.syncDirty) {
|
|
93
|
+
result = await manager.syncDirty();
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
await manager.sync?.({ reason: "build-dirty" });
|
|
97
|
+
result = [];
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case "close":
|
|
101
|
+
if (dirtyTimer)
|
|
102
|
+
clearInterval(dirtyTimer);
|
|
103
|
+
await manager.close?.();
|
|
104
|
+
result = undefined;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
post({ id: req.id, type: "result", data: result });
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
post({
|
|
111
|
+
id: req.id,
|
|
112
|
+
type: "error",
|
|
113
|
+
message: err instanceof Error ? err.message : String(err),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CHUNKS_TABLE, FTS_TABLE } from "../store/schema.js";
|
|
2
|
+
import { truncateUtf16Safe } from "./text.js";
|
|
3
|
+
export function buildFtsQuery(raw) {
|
|
4
|
+
const tokens = raw
|
|
5
|
+
.match(/[\p{L}\p{N}_]+/gu)
|
|
6
|
+
?.map((token) => token.trim())
|
|
7
|
+
.filter(Boolean) ?? [];
|
|
8
|
+
if (tokens.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
const quoted = tokens.map((token) => `"${token.replaceAll('"', "")}"`);
|
|
11
|
+
return quoted.join(" AND ");
|
|
12
|
+
}
|
|
13
|
+
export function bm25RankToScore(rank) {
|
|
14
|
+
const normalized = Number.isFinite(rank) ? Math.max(0, rank) : 999;
|
|
15
|
+
return 1 / (1 + normalized);
|
|
16
|
+
}
|
|
17
|
+
export function searchFts(params) {
|
|
18
|
+
if (params.limit <= 0)
|
|
19
|
+
return [];
|
|
20
|
+
const ftsQuery = buildFtsQuery(params.query);
|
|
21
|
+
if (!ftsQuery)
|
|
22
|
+
return [];
|
|
23
|
+
const modelClause = params.model ? ` AND ${FTS_TABLE}.model = ?` : "";
|
|
24
|
+
const modelParams = params.model ? [params.model] : [];
|
|
25
|
+
const agentJoin = ` JOIN ${CHUNKS_TABLE} c ON c.id = ${FTS_TABLE}.id`;
|
|
26
|
+
const agentClause = params.agentId ? " AND c.agent_id = ?" : "";
|
|
27
|
+
const agentParams = params.agentId ? [params.agentId] : [];
|
|
28
|
+
const sourceClause = params.source ? " AND c.source = ?" : "";
|
|
29
|
+
const sourceParams = params.source ? [params.source] : [];
|
|
30
|
+
const rows = params.db
|
|
31
|
+
.prepare(`SELECT ${FTS_TABLE}.id as id, ${FTS_TABLE}.path as path,
|
|
32
|
+
${FTS_TABLE}.start_line as start_line,
|
|
33
|
+
${FTS_TABLE}.end_line as end_line,
|
|
34
|
+
${FTS_TABLE}.text as text,
|
|
35
|
+
bm25(${FTS_TABLE}) AS rank, c.source as source, c.agent_id as agent_id
|
|
36
|
+
FROM ${FTS_TABLE}${agentJoin}
|
|
37
|
+
WHERE ${FTS_TABLE} MATCH ?${modelClause}${agentClause}${sourceClause}
|
|
38
|
+
ORDER BY rank ASC
|
|
39
|
+
LIMIT ?`)
|
|
40
|
+
.all(ftsQuery, ...modelParams, ...agentParams, ...sourceParams, params.limit);
|
|
41
|
+
return rows
|
|
42
|
+
.map((row) => {
|
|
43
|
+
const textScore = bm25RankToScore(row.rank);
|
|
44
|
+
return {
|
|
45
|
+
id: row.id,
|
|
46
|
+
path: row.path,
|
|
47
|
+
startLine: row.start_line,
|
|
48
|
+
endLine: row.end_line,
|
|
49
|
+
score: textScore,
|
|
50
|
+
textScore,
|
|
51
|
+
snippet: truncateUtf16Safe(row.text, params.snippetMaxChars),
|
|
52
|
+
source: (row.source === "sessions" ? "sessions" : "memory"),
|
|
53
|
+
agentId: row.agent_id,
|
|
54
|
+
};
|
|
55
|
+
})
|
|
56
|
+
.filter((row) => row.score >= params.minScore);
|
|
57
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function mergeHybridResults(params) {
|
|
2
|
+
const byId = new Map();
|
|
3
|
+
for (const entry of params.vector) {
|
|
4
|
+
byId.set(entry.id, {
|
|
5
|
+
path: entry.path,
|
|
6
|
+
startLine: entry.startLine,
|
|
7
|
+
endLine: entry.endLine,
|
|
8
|
+
snippet: entry.snippet,
|
|
9
|
+
vectorScore: entry.vectorScore,
|
|
10
|
+
textScore: 0,
|
|
11
|
+
source: entry.source,
|
|
12
|
+
agentId: entry.agentId,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
for (const entry of params.keyword) {
|
|
16
|
+
const existing = byId.get(entry.id);
|
|
17
|
+
if (existing) {
|
|
18
|
+
existing.textScore = Math.max(existing.textScore, entry.textScore);
|
|
19
|
+
if (!existing.snippet && entry.snippet) {
|
|
20
|
+
existing.snippet = entry.snippet;
|
|
21
|
+
}
|
|
22
|
+
if (!existing.agentId && entry.agentId) {
|
|
23
|
+
existing.agentId = entry.agentId;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
byId.set(entry.id, {
|
|
28
|
+
path: entry.path,
|
|
29
|
+
startLine: entry.startLine,
|
|
30
|
+
endLine: entry.endLine,
|
|
31
|
+
snippet: entry.snippet,
|
|
32
|
+
vectorScore: 0,
|
|
33
|
+
textScore: entry.textScore,
|
|
34
|
+
source: entry.source,
|
|
35
|
+
agentId: entry.agentId,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const vectorWeight = params.vectorWeight;
|
|
40
|
+
const textWeight = params.textWeight;
|
|
41
|
+
return Array.from(byId.values()).map((entry) => ({
|
|
42
|
+
path: entry.path,
|
|
43
|
+
startLine: entry.startLine,
|
|
44
|
+
endLine: entry.endLine,
|
|
45
|
+
snippet: entry.snippet,
|
|
46
|
+
source: entry.source,
|
|
47
|
+
score: entry.vectorScore * vectorWeight + entry.textScore * textWeight,
|
|
48
|
+
agentId: entry.agentId,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function isHighSurrogate(codeUnit) {
|
|
2
|
+
return codeUnit >= 0xd800 && codeUnit <= 0xdbff;
|
|
3
|
+
}
|
|
4
|
+
function isLowSurrogate(codeUnit) {
|
|
5
|
+
return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
|
|
6
|
+
}
|
|
7
|
+
function sliceUtf16Safe(input, from, to) {
|
|
8
|
+
const len = input.length;
|
|
9
|
+
let start = Math.max(0, Math.min(len, Math.floor(from)));
|
|
10
|
+
let end = Math.max(0, Math.min(len, Math.floor(to)));
|
|
11
|
+
if (start > 0 && start < len) {
|
|
12
|
+
const codeUnit = input.charCodeAt(start);
|
|
13
|
+
if (isLowSurrogate(codeUnit) && isHighSurrogate(input.charCodeAt(start - 1))) {
|
|
14
|
+
start += 1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (end > 0 && end < len) {
|
|
18
|
+
const codeUnit = input.charCodeAt(end - 1);
|
|
19
|
+
if (isHighSurrogate(codeUnit) && isLowSurrogate(input.charCodeAt(end))) {
|
|
20
|
+
end -= 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return input.slice(start, end);
|
|
24
|
+
}
|
|
25
|
+
export function truncateUtf16Safe(input, maxLen) {
|
|
26
|
+
const limit = Math.max(0, Math.floor(maxLen));
|
|
27
|
+
if (input.length <= limit)
|
|
28
|
+
return input;
|
|
29
|
+
return sliceUtf16Safe(input, 0, limit);
|
|
30
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CHUNKS_TABLE, VECTOR_TABLE } from "../store/schema.js";
|
|
2
|
+
import { truncateUtf16Safe } from "./text.js";
|
|
3
|
+
const vectorToBlob = (embedding) => Buffer.from(new Float32Array(embedding).buffer);
|
|
4
|
+
export async function searchVector(params) {
|
|
5
|
+
if (params.queryVec.length === 0 || params.limit <= 0) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
const conditions = [];
|
|
9
|
+
const paramsList = [];
|
|
10
|
+
if (params.model) {
|
|
11
|
+
conditions.push("c.model = ?");
|
|
12
|
+
paramsList.push(params.model);
|
|
13
|
+
}
|
|
14
|
+
if (params.agentId) {
|
|
15
|
+
conditions.push("c.agent_id = ?");
|
|
16
|
+
paramsList.push(params.agentId);
|
|
17
|
+
}
|
|
18
|
+
if (params.source) {
|
|
19
|
+
conditions.push("c.source = ?");
|
|
20
|
+
paramsList.push(params.source);
|
|
21
|
+
}
|
|
22
|
+
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
23
|
+
const rows = params.db
|
|
24
|
+
.prepare(`SELECT c.id, c.path, c.start_line, c.end_line, c.text,
|
|
25
|
+
c.source as source,
|
|
26
|
+
c.agent_id as agent_id,
|
|
27
|
+
vec_distance_cosine(v.embedding, ?) AS dist
|
|
28
|
+
FROM ${VECTOR_TABLE} v
|
|
29
|
+
JOIN ${CHUNKS_TABLE} c ON c.id = v.id${whereClause}
|
|
30
|
+
ORDER BY dist ASC
|
|
31
|
+
LIMIT ?`)
|
|
32
|
+
.all(vectorToBlob(params.queryVec), ...paramsList, params.limit);
|
|
33
|
+
return rows.map((row) => ({
|
|
34
|
+
id: row.id,
|
|
35
|
+
path: row.path,
|
|
36
|
+
startLine: row.start_line,
|
|
37
|
+
endLine: row.end_line,
|
|
38
|
+
vectorScore: 1 - row.dist,
|
|
39
|
+
snippet: truncateUtf16Safe(row.text, params.snippetMaxChars),
|
|
40
|
+
source: row.source === "sessions" ? "sessions" : "memory",
|
|
41
|
+
agentId: row.agent_id,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const HASH_LENGTH = 12;
|
|
3
|
+
export function buildContentHashSlug(content) {
|
|
4
|
+
const normalized = content.trim().replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
5
|
+
const hash = createHash("sha256").update(normalized).digest("hex");
|
|
6
|
+
return hash.slice(0, HASH_LENGTH);
|
|
7
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
function shortenHomePath(pathValue) {
|
|
5
|
+
const home = homedir();
|
|
6
|
+
return pathValue.startsWith(home) ? `~${pathValue.slice(home.length)}` : pathValue;
|
|
7
|
+
}
|
|
8
|
+
function formatExcerpt(messages) {
|
|
9
|
+
return messages.map((message) => `${message.role}: ${message.text}`).join("\n");
|
|
10
|
+
}
|
|
11
|
+
export function buildSessionMemoryEntry(input) {
|
|
12
|
+
const iso = input.timestamp.toISOString();
|
|
13
|
+
const [datePart, timePart] = iso.split("T");
|
|
14
|
+
const time = (timePart ?? "").split(".")[0] ?? "";
|
|
15
|
+
const lines = [
|
|
16
|
+
`# Session: ${datePart ?? ""} ${time} UTC`,
|
|
17
|
+
"",
|
|
18
|
+
`- **Reason**: ${input.reason}`,
|
|
19
|
+
`- **Session ID**: ${input.sessionId ?? "unknown"}`,
|
|
20
|
+
];
|
|
21
|
+
if (input.sessionFile) {
|
|
22
|
+
lines.push(`- **Session File**: ${shortenHomePath(input.sessionFile)}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push("");
|
|
25
|
+
if (input.messages.length > 0) {
|
|
26
|
+
lines.push("## Conversation Excerpt", "", formatExcerpt(input.messages), "");
|
|
27
|
+
}
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
}
|
|
30
|
+
export async function writeSessionMemoryFile(filePath, content) {
|
|
31
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
32
|
+
await writeFile(filePath, content, "utf-8");
|
|
33
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { loadSessionTranscriptState } from "./transcript/state.js";
|
|
2
|
+
import { readSessionTranscriptDelta } from "./transcript/reader.js";
|
|
3
|
+
import { getSessionTranscriptStatePath, normalizeSessionId } from "./transcript/paths.js";
|
|
4
|
+
function normalizeThreshold(value) {
|
|
5
|
+
if (!Number.isFinite(value))
|
|
6
|
+
return 0;
|
|
7
|
+
return Math.max(0, Math.trunc(value));
|
|
8
|
+
}
|
|
9
|
+
function hasDeltaThresholds(policy) {
|
|
10
|
+
return normalizeThreshold(policy.deltaBytes) > 0 || normalizeThreshold(policy.deltaMessages) > 0;
|
|
11
|
+
}
|
|
12
|
+
export async function evaluateSessionFlushDelta(params) {
|
|
13
|
+
const byteThreshold = normalizeThreshold(params.policy.deltaBytes);
|
|
14
|
+
const messageThreshold = normalizeThreshold(params.policy.deltaMessages);
|
|
15
|
+
const initialSessionId = normalizeSessionId(undefined, params.sessionFile);
|
|
16
|
+
const initialStatePath = getSessionTranscriptStatePath(params.agentId, initialSessionId);
|
|
17
|
+
const initialState = await loadSessionTranscriptState(initialStatePath);
|
|
18
|
+
const delta = await readSessionTranscriptDelta(params.sessionFile, initialState.lastLine);
|
|
19
|
+
const finalSessionId = normalizeSessionId(delta.sessionId, params.sessionFile);
|
|
20
|
+
const statePath = getSessionTranscriptStatePath(params.agentId, finalSessionId);
|
|
21
|
+
const state = finalSessionId === initialSessionId ? initialState : await loadSessionTranscriptState(statePath);
|
|
22
|
+
const finalDelta = finalSessionId === initialSessionId
|
|
23
|
+
? delta
|
|
24
|
+
: await readSessionTranscriptDelta(params.sessionFile, state.lastLine);
|
|
25
|
+
const bytesReached = byteThreshold > 0 && finalDelta.deltaBytes >= byteThreshold;
|
|
26
|
+
const messagesReached = messageThreshold > 0 && finalDelta.messages.length >= messageThreshold;
|
|
27
|
+
return {
|
|
28
|
+
sessionId: finalSessionId,
|
|
29
|
+
lastLine: finalDelta.lastLine,
|
|
30
|
+
deltaBytes: finalDelta.deltaBytes,
|
|
31
|
+
deltaMessages: finalDelta.messages.length,
|
|
32
|
+
shouldFlush: hasDeltaThresholds(params.policy) && (bytesReached || messagesReached),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { deriveAgentId } from "../../agents-paths.js";
|
|
3
|
+
import { loadResolvedMemorySearchConfig } from "../config/loader.js";
|
|
4
|
+
import { memoryLog } from "../log.js";
|
|
5
|
+
import { evaluateSessionFlushDelta } from "./flush-policy.js";
|
|
6
|
+
import { buildContentHashSlug } from "./content-hash.js";
|
|
7
|
+
import { buildSessionMemoryEntry, writeSessionMemoryFile } from "./entry.js";
|
|
8
|
+
import { formatSessionTimestampParts, buildMemoryFilename, getAgentMemoryDir } from "./paths.js";
|
|
9
|
+
import { readSessionExcerpt } from "./session-reader.js";
|
|
10
|
+
import { flushSessionTranscript } from "./transcript/flush.js";
|
|
11
|
+
import { publishSystemEvent } from "../../extensions/system-events.js";
|
|
12
|
+
const SESSION_EXCERPT_MAX_MESSAGES = 50;
|
|
13
|
+
async function flushSessionArtifacts(params) {
|
|
14
|
+
const agentId = deriveAgentId(params.ctx.cwd);
|
|
15
|
+
memoryLog.info("flush trigger", {
|
|
16
|
+
agentId,
|
|
17
|
+
trigger: params.trigger,
|
|
18
|
+
reason: params.reason,
|
|
19
|
+
sessionFile: params.sessionFile,
|
|
20
|
+
});
|
|
21
|
+
const memoryResult = await flushSessionMemory({
|
|
22
|
+
agentId,
|
|
23
|
+
reason: params.reason,
|
|
24
|
+
sessionFile: params.sessionFile,
|
|
25
|
+
});
|
|
26
|
+
const transcriptResult = await flushSessionTranscript({
|
|
27
|
+
agentId,
|
|
28
|
+
reason: params.reason,
|
|
29
|
+
sessionFile: params.sessionFile,
|
|
30
|
+
});
|
|
31
|
+
memoryLog.info("flush artifacts written", {
|
|
32
|
+
agentId,
|
|
33
|
+
trigger: params.trigger,
|
|
34
|
+
reason: params.reason,
|
|
35
|
+
sessionMemoryWritten: memoryResult.written,
|
|
36
|
+
sessionMemoryPath: memoryResult.filePath,
|
|
37
|
+
sessionTranscriptWritten: transcriptResult.written,
|
|
38
|
+
sessionTranscriptPath: transcriptResult.filePath,
|
|
39
|
+
sessionTranscriptLastLine: transcriptResult.lastLine,
|
|
40
|
+
});
|
|
41
|
+
if (memoryResult.written) {
|
|
42
|
+
publishSystemEvent({
|
|
43
|
+
source: "memory",
|
|
44
|
+
level: "info",
|
|
45
|
+
message: `Session memory flush persisted (${params.trigger}).`,
|
|
46
|
+
dedupeKey: `memory|session_memory_flush|${params.trigger}`,
|
|
47
|
+
toast: false,
|
|
48
|
+
ctx: params.ctx,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (transcriptResult.written) {
|
|
52
|
+
publishSystemEvent({
|
|
53
|
+
source: "memory",
|
|
54
|
+
level: "info",
|
|
55
|
+
message: `Session transcript flush persisted (${params.trigger}).`,
|
|
56
|
+
dedupeKey: `memory|session_transcript_flush|${params.trigger}`,
|
|
57
|
+
toast: false,
|
|
58
|
+
ctx: params.ctx,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function notifySessionFlushFailure(ctx, trigger, error) {
|
|
63
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
+
publishSystemEvent({
|
|
65
|
+
source: "memory",
|
|
66
|
+
level: "warning",
|
|
67
|
+
message: `Session flush failed (${trigger}): ${message}`,
|
|
68
|
+
dedupeKey: `memory|session_flush_failed|${trigger}`,
|
|
69
|
+
toast: false,
|
|
70
|
+
ctx,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function flushSessionMemory(params) {
|
|
74
|
+
const excerpt = await readSessionExcerpt(params.sessionFile, SESSION_EXCERPT_MAX_MESSAGES);
|
|
75
|
+
if (excerpt.messages.length === 0) {
|
|
76
|
+
return { written: false };
|
|
77
|
+
}
|
|
78
|
+
const timestamp = new Date();
|
|
79
|
+
const { date, timeSlug } = formatSessionTimestampParts(timestamp);
|
|
80
|
+
const hashInput = [excerpt.sessionId ?? "", ...excerpt.messages.map((msg) => `${msg.role}:${msg.text}`)].join("\n");
|
|
81
|
+
const hashSlug = buildContentHashSlug(hashInput);
|
|
82
|
+
const filePath = join(getAgentMemoryDir(params.agentId), buildMemoryFilename(date, `${timeSlug}-${hashSlug}`));
|
|
83
|
+
const content = buildSessionMemoryEntry({
|
|
84
|
+
timestamp,
|
|
85
|
+
reason: params.reason,
|
|
86
|
+
sessionId: excerpt.sessionId,
|
|
87
|
+
sessionFile: params.sessionFile,
|
|
88
|
+
messages: excerpt.messages,
|
|
89
|
+
});
|
|
90
|
+
await writeSessionMemoryFile(filePath, content);
|
|
91
|
+
return { written: true, filePath };
|
|
92
|
+
}
|
|
93
|
+
async function handleSessionSwitch(event, ctx) {
|
|
94
|
+
if (!event.previousSessionFile)
|
|
95
|
+
return;
|
|
96
|
+
const settings = await loadResolvedMemorySearchConfig();
|
|
97
|
+
if (!settings.enabled || !settings.flush.onSessionSwitch)
|
|
98
|
+
return;
|
|
99
|
+
await flushSessionArtifacts({
|
|
100
|
+
reason: event.reason,
|
|
101
|
+
sessionFile: event.previousSessionFile,
|
|
102
|
+
ctx,
|
|
103
|
+
trigger: "session-switch",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function handleSessionBeforeCompact(_event, ctx) {
|
|
107
|
+
const settings = await loadResolvedMemorySearchConfig();
|
|
108
|
+
if (!settings.enabled || !settings.flush.onSessionCompact)
|
|
109
|
+
return;
|
|
110
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
111
|
+
if (!sessionFile)
|
|
112
|
+
return;
|
|
113
|
+
await flushSessionArtifacts({
|
|
114
|
+
reason: "compaction",
|
|
115
|
+
sessionFile,
|
|
116
|
+
ctx,
|
|
117
|
+
trigger: "session-compact",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function handleTurnEnd(_event, ctx) {
|
|
121
|
+
const settings = await loadResolvedMemorySearchConfig();
|
|
122
|
+
if (!settings.enabled)
|
|
123
|
+
return;
|
|
124
|
+
const byteThreshold = settings.flush.deltaBytes;
|
|
125
|
+
const messageThreshold = settings.flush.deltaMessages;
|
|
126
|
+
if (byteThreshold <= 0 && messageThreshold <= 0)
|
|
127
|
+
return;
|
|
128
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
129
|
+
if (!sessionFile)
|
|
130
|
+
return;
|
|
131
|
+
const agentId = deriveAgentId(ctx.cwd);
|
|
132
|
+
const delta = await evaluateSessionFlushDelta({
|
|
133
|
+
agentId,
|
|
134
|
+
sessionFile,
|
|
135
|
+
policy: {
|
|
136
|
+
deltaBytes: byteThreshold,
|
|
137
|
+
deltaMessages: messageThreshold,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
memoryLog.info("flush delta evaluated", {
|
|
141
|
+
agentId,
|
|
142
|
+
sessionFile,
|
|
143
|
+
deltaBytes: delta.deltaBytes,
|
|
144
|
+
deltaMessages: delta.deltaMessages,
|
|
145
|
+
thresholdBytes: byteThreshold,
|
|
146
|
+
thresholdMessages: messageThreshold,
|
|
147
|
+
shouldFlush: delta.shouldFlush,
|
|
148
|
+
});
|
|
149
|
+
if (!delta.shouldFlush)
|
|
150
|
+
return;
|
|
151
|
+
await flushSessionArtifacts({
|
|
152
|
+
reason: "resume",
|
|
153
|
+
sessionFile,
|
|
154
|
+
ctx,
|
|
155
|
+
trigger: "delta-threshold",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
export function registerSessionMemoryHook(pi) {
|
|
159
|
+
pi.on("session_switch", async (event, ctx) => {
|
|
160
|
+
try {
|
|
161
|
+
await handleSessionSwitch(event, ctx);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.warn(`[memory] session switch flush failed: ${String(error)}`);
|
|
165
|
+
memoryLog.warn("session switch flush failed", { error: String(error) });
|
|
166
|
+
notifySessionFlushFailure(ctx, "session-switch", error);
|
|
167
|
+
// Best effort: session memory is non-critical.
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
171
|
+
try {
|
|
172
|
+
await handleSessionBeforeCompact(event, ctx);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
console.warn(`[memory] session_before_compact flush failed: ${String(error)}`);
|
|
176
|
+
memoryLog.warn("session_before_compact flush failed", { error: String(error) });
|
|
177
|
+
notifySessionFlushFailure(ctx, "session-compact", error);
|
|
178
|
+
// Best effort: session memory is non-critical.
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
182
|
+
try {
|
|
183
|
+
await handleTurnEnd(event, ctx);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.warn(`[memory] turn_end delta flush failed: ${String(error)}`);
|
|
187
|
+
memoryLog.warn("turn_end delta flush failed", { error: String(error) });
|
|
188
|
+
notifySessionFlushFailure(ctx, "delta-threshold", error);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { getAgentDir } from "../../agents-paths.js";
|
|
3
|
+
export function getAgentMemoryDir(agentId) {
|
|
4
|
+
return join(getAgentDir(agentId), "memory");
|
|
5
|
+
}
|
|
6
|
+
export function formatSessionTimestampParts(timestamp) {
|
|
7
|
+
const iso = timestamp.toISOString();
|
|
8
|
+
const [datePart, timePart] = iso.split("T");
|
|
9
|
+
const time = (timePart ?? "").split(".")[0] ?? "";
|
|
10
|
+
const timeSlug = time.replace(/:/g, "").slice(0, 4);
|
|
11
|
+
return { date: datePart ?? "", time, timeSlug };
|
|
12
|
+
}
|
|
13
|
+
export function buildMemoryFilename(date, slug) {
|
|
14
|
+
return `${date}-${slug}.md`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
function parseJsonLine(line) {
|
|
3
|
+
try {
|
|
4
|
+
const parsed = JSON.parse(line);
|
|
5
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function extractTextContent(content) {
|
|
12
|
+
if (typeof content === "string") {
|
|
13
|
+
return content;
|
|
14
|
+
}
|
|
15
|
+
if (!Array.isArray(content)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const texts = content
|
|
19
|
+
.map((item) => {
|
|
20
|
+
if (!item || typeof item !== "object")
|
|
21
|
+
return undefined;
|
|
22
|
+
const entry = item;
|
|
23
|
+
return entry.type === "text" && typeof entry.text === "string" ? entry.text : undefined;
|
|
24
|
+
})
|
|
25
|
+
.filter((text) => typeof text === "string" && text.trim().length > 0);
|
|
26
|
+
if (texts.length === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
return texts.join("\n");
|
|
29
|
+
}
|
|
30
|
+
function shouldSkipUserText(text) {
|
|
31
|
+
return text.trim().startsWith("/");
|
|
32
|
+
}
|
|
33
|
+
const USER_QUERY_OPEN = "<user_query>";
|
|
34
|
+
const USER_QUERY_CLOSE = "</user_query>";
|
|
35
|
+
function extractUserQuery(text) {
|
|
36
|
+
const start = text.indexOf(USER_QUERY_OPEN);
|
|
37
|
+
const end = text.lastIndexOf(USER_QUERY_CLOSE);
|
|
38
|
+
if (start === -1 || end === -1 || end <= start)
|
|
39
|
+
return undefined;
|
|
40
|
+
const extracted = text.slice(start + USER_QUERY_OPEN.length, end).trim();
|
|
41
|
+
return extracted.length > 0 ? extracted : undefined;
|
|
42
|
+
}
|
|
43
|
+
export async function readSessionExcerpt(sessionFile, maxMessages) {
|
|
44
|
+
const result = { messages: [] };
|
|
45
|
+
if (maxMessages <= 0)
|
|
46
|
+
return result;
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = await readFile(sessionFile, "utf-8");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const entry = parseJsonLine(line);
|
|
57
|
+
if (!entry)
|
|
58
|
+
continue;
|
|
59
|
+
if (entry.type === "session" && typeof entry.id === "string" && !result.sessionId) {
|
|
60
|
+
result.sessionId = entry.id;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (entry.type !== "message" || !entry.message)
|
|
64
|
+
continue;
|
|
65
|
+
const role = entry.message.role;
|
|
66
|
+
if (role !== "user" && role !== "assistant")
|
|
67
|
+
continue;
|
|
68
|
+
const text = extractTextContent(entry.message.content ?? "");
|
|
69
|
+
if (!text)
|
|
70
|
+
continue;
|
|
71
|
+
let normalized = text.trim();
|
|
72
|
+
if (!normalized)
|
|
73
|
+
continue;
|
|
74
|
+
if (role === "user") {
|
|
75
|
+
const extracted = extractUserQuery(normalized);
|
|
76
|
+
if (!extracted)
|
|
77
|
+
continue;
|
|
78
|
+
if (shouldSkipUserText(extracted))
|
|
79
|
+
continue;
|
|
80
|
+
normalized = extracted;
|
|
81
|
+
}
|
|
82
|
+
result.messages.push({ role, text: normalized });
|
|
83
|
+
if (result.messages.length > maxMessages) {
|
|
84
|
+
result.messages.shift();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const HASH_LENGTH = 12;
|
|
3
|
+
export function buildTranscriptContentHashSlug(content) {
|
|
4
|
+
const normalized = content.trim().replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
5
|
+
const hash = createHash("sha256").update(normalized).digest("hex");
|
|
6
|
+
return hash.slice(0, HASH_LENGTH);
|
|
7
|
+
}
|