@poolzin/pool-bot 2026.2.10 → 2026.2.17
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/CHANGELOG.md +24 -0
- package/dist/agents/auth-profiles/usage.js +22 -0
- package/dist/agents/auth-profiles.js +1 -1
- package/dist/agents/bash-tools.exec.js +4 -6
- package/dist/agents/glob-pattern.js +42 -0
- package/dist/agents/memory-search.js +33 -0
- package/dist/agents/model-fallback.js +59 -8
- package/dist/agents/pi-tools.before-tool-call.js +145 -4
- package/dist/agents/pi-tools.js +27 -9
- package/dist/agents/pi-tools.policy.js +85 -92
- package/dist/agents/pi-tools.schema.js +54 -27
- package/dist/agents/sandbox/validate-sandbox-security.js +157 -0
- package/dist/agents/sandbox-tool-policy.js +26 -0
- package/dist/agents/sanitize-for-prompt.js +18 -0
- package/dist/agents/session-write-lock.js +203 -39
- package/dist/agents/system-prompt.js +52 -10
- package/dist/agents/tool-loop-detection.js +466 -0
- package/dist/agents/tool-policy.js +6 -0
- package/dist/auto-reply/reply/post-compaction-audit.js +96 -0
- package/dist/auto-reply/reply/post-compaction-context.js +98 -0
- package/dist/build-info.json +3 -3
- package/dist/config/zod-schema.agent-defaults.js +14 -0
- package/dist/config/zod-schema.agent-runtime.js +14 -0
- package/dist/infra/path-safety.js +16 -0
- package/dist/logging/diagnostic-session-state.js +73 -0
- package/dist/logging/diagnostic.js +22 -0
- package/dist/memory/embeddings.js +36 -9
- package/dist/memory/hybrid.js +24 -5
- package/dist/memory/manager.js +76 -28
- package/dist/memory/mmr.js +164 -0
- package/dist/memory/query-expansion.js +331 -0
- package/dist/memory/temporal-decay.js +119 -0
- package/dist/process/kill-tree.js +98 -0
- package/dist/shared/pid-alive.js +12 -0
- package/dist/shared/process-scoped-map.js +10 -0
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
|
@@ -125,6 +125,20 @@ export const AgentDefaultsSchema = z
|
|
|
125
125
|
subagents: z
|
|
126
126
|
.object({
|
|
127
127
|
maxConcurrent: z.number().int().positive().optional(),
|
|
128
|
+
maxSpawnDepth: z
|
|
129
|
+
.number()
|
|
130
|
+
.int()
|
|
131
|
+
.min(1)
|
|
132
|
+
.max(5)
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Maximum nesting depth for sub-agent spawning. 1 = no nesting (default), 2 = sub-agents can spawn sub-sub-agents."),
|
|
135
|
+
maxChildrenPerAgent: z
|
|
136
|
+
.number()
|
|
137
|
+
.int()
|
|
138
|
+
.min(1)
|
|
139
|
+
.max(20)
|
|
140
|
+
.optional()
|
|
141
|
+
.describe("Maximum active children a single requester session may spawn. Default: 5."),
|
|
128
142
|
archiveAfterMinutes: z.number().int().positive().optional(),
|
|
129
143
|
model: z
|
|
130
144
|
.union([
|
|
@@ -372,6 +372,20 @@ export const MemorySearchSchema = z
|
|
|
372
372
|
vectorWeight: z.number().min(0).max(1).optional(),
|
|
373
373
|
textWeight: z.number().min(0).max(1).optional(),
|
|
374
374
|
candidateMultiplier: z.number().int().positive().optional(),
|
|
375
|
+
mmr: z
|
|
376
|
+
.object({
|
|
377
|
+
enabled: z.boolean().optional(),
|
|
378
|
+
lambda: z.number().min(0).max(1).optional(),
|
|
379
|
+
})
|
|
380
|
+
.strict()
|
|
381
|
+
.optional(),
|
|
382
|
+
temporalDecay: z
|
|
383
|
+
.object({
|
|
384
|
+
enabled: z.boolean().optional(),
|
|
385
|
+
halfLifeDays: z.number().int().positive().optional(),
|
|
386
|
+
})
|
|
387
|
+
.strict()
|
|
388
|
+
.optional(),
|
|
375
389
|
})
|
|
376
390
|
.strict()
|
|
377
391
|
.optional(),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function resolveSafeBaseDir(rootDir) {
|
|
3
|
+
const resolved = path.resolve(rootDir);
|
|
4
|
+
return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
|
|
5
|
+
}
|
|
6
|
+
export function isWithinDir(rootDir, targetPath) {
|
|
7
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
8
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
9
|
+
// Windows paths are effectively case-insensitive; normalize to avoid false negatives.
|
|
10
|
+
if (process.platform === "win32") {
|
|
11
|
+
const relative = path.win32.relative(resolvedRoot.toLowerCase(), resolvedTarget.toLowerCase());
|
|
12
|
+
return relative === "" || (!relative.startsWith("..") && !path.win32.isAbsolute(relative));
|
|
13
|
+
}
|
|
14
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
15
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
16
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const diagnosticSessionStates = new Map();
|
|
2
|
+
const SESSION_STATE_TTL_MS = 30 * 60 * 1000;
|
|
3
|
+
const SESSION_STATE_PRUNE_INTERVAL_MS = 60 * 1000;
|
|
4
|
+
const SESSION_STATE_MAX_ENTRIES = 2000;
|
|
5
|
+
let lastSessionPruneAt = 0;
|
|
6
|
+
export function pruneDiagnosticSessionStates(now = Date.now(), force = false) {
|
|
7
|
+
const shouldPruneForSize = diagnosticSessionStates.size > SESSION_STATE_MAX_ENTRIES;
|
|
8
|
+
if (!force && !shouldPruneForSize && now - lastSessionPruneAt < SESSION_STATE_PRUNE_INTERVAL_MS) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
lastSessionPruneAt = now;
|
|
12
|
+
for (const [key, state] of diagnosticSessionStates.entries()) {
|
|
13
|
+
const ageMs = now - state.lastActivity;
|
|
14
|
+
const isIdle = state.state === "idle";
|
|
15
|
+
if (isIdle && state.queueDepth <= 0 && ageMs > SESSION_STATE_TTL_MS) {
|
|
16
|
+
diagnosticSessionStates.delete(key);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (diagnosticSessionStates.size <= SESSION_STATE_MAX_ENTRIES) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const excess = diagnosticSessionStates.size - SESSION_STATE_MAX_ENTRIES;
|
|
23
|
+
const ordered = Array.from(diagnosticSessionStates.entries()).toSorted((a, b) => a[1].lastActivity - b[1].lastActivity);
|
|
24
|
+
for (let i = 0; i < excess; i += 1) {
|
|
25
|
+
const key = ordered[i]?.[0];
|
|
26
|
+
if (!key) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
diagnosticSessionStates.delete(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function resolveSessionKey({ sessionKey, sessionId }) {
|
|
33
|
+
return sessionKey ?? sessionId ?? "unknown";
|
|
34
|
+
}
|
|
35
|
+
function findStateBySessionId(sessionId) {
|
|
36
|
+
for (const state of diagnosticSessionStates.values()) {
|
|
37
|
+
if (state.sessionId === sessionId) {
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
export function getDiagnosticSessionState(ref) {
|
|
44
|
+
pruneDiagnosticSessionStates();
|
|
45
|
+
const key = resolveSessionKey(ref);
|
|
46
|
+
const existing = diagnosticSessionStates.get(key) ?? (ref.sessionId && findStateBySessionId(ref.sessionId));
|
|
47
|
+
if (existing) {
|
|
48
|
+
if (ref.sessionId) {
|
|
49
|
+
existing.sessionId = ref.sessionId;
|
|
50
|
+
}
|
|
51
|
+
if (ref.sessionKey) {
|
|
52
|
+
existing.sessionKey = ref.sessionKey;
|
|
53
|
+
}
|
|
54
|
+
return existing;
|
|
55
|
+
}
|
|
56
|
+
const created = {
|
|
57
|
+
sessionId: ref.sessionId,
|
|
58
|
+
sessionKey: ref.sessionKey,
|
|
59
|
+
lastActivity: Date.now(),
|
|
60
|
+
state: "idle",
|
|
61
|
+
queueDepth: 0,
|
|
62
|
+
};
|
|
63
|
+
diagnosticSessionStates.set(key, created);
|
|
64
|
+
pruneDiagnosticSessionStates(Date.now(), true);
|
|
65
|
+
return created;
|
|
66
|
+
}
|
|
67
|
+
export function getDiagnosticSessionStateCountForTest() {
|
|
68
|
+
return diagnosticSessionStates.size;
|
|
69
|
+
}
|
|
70
|
+
export function resetDiagnosticSessionStateForTest() {
|
|
71
|
+
diagnosticSessionStates.clear();
|
|
72
|
+
lastSessionPruneAt = 0;
|
|
73
|
+
}
|
|
@@ -165,6 +165,28 @@ export function logLaneDequeue(lane, waitMs, queueSize) {
|
|
|
165
165
|
});
|
|
166
166
|
markActivity();
|
|
167
167
|
}
|
|
168
|
+
export function logToolLoopAction(params) {
|
|
169
|
+
const payload = `tool loop: sessionId=${params.sessionId ?? "unknown"} sessionKey=${params.sessionKey ?? "unknown"} tool=${params.toolName} level=${params.level} action=${params.action} detector=${params.detector} count=${params.count}${params.pairedToolName ? ` pairedTool=${params.pairedToolName}` : ""} message="${params.message}"`;
|
|
170
|
+
if (params.level === "critical") {
|
|
171
|
+
diag.error(payload);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
diag.warn(payload);
|
|
175
|
+
}
|
|
176
|
+
emitDiagnosticEvent({
|
|
177
|
+
type: "tool.loop",
|
|
178
|
+
sessionId: params.sessionId,
|
|
179
|
+
sessionKey: params.sessionKey,
|
|
180
|
+
toolName: params.toolName,
|
|
181
|
+
level: params.level,
|
|
182
|
+
action: params.action,
|
|
183
|
+
detector: params.detector,
|
|
184
|
+
count: params.count,
|
|
185
|
+
message: params.message,
|
|
186
|
+
pairedToolName: params.pairedToolName,
|
|
187
|
+
});
|
|
188
|
+
markActivity();
|
|
189
|
+
}
|
|
168
190
|
export function logRunAttempt(params) {
|
|
169
191
|
diag.debug(`run attempt: sessionId=${params.sessionId ?? "unknown"} sessionKey=${params.sessionKey ?? "unknown"} runId=${params.runId} attempt=${params.attempt}`);
|
|
170
192
|
emitDiagnosticEvent({
|
|
@@ -13,6 +13,7 @@ function sanitizeAndNormalizeEmbedding(vec) {
|
|
|
13
13
|
}
|
|
14
14
|
return sanitized.map((value) => value / magnitude);
|
|
15
15
|
}
|
|
16
|
+
const REMOTE_EMBEDDING_PROVIDER_IDS = ["openai", "gemini", "voyage"];
|
|
16
17
|
export const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf";
|
|
17
18
|
function canAutoSelectLocal(options) {
|
|
18
19
|
const modelPath = options.local?.modelPath?.trim();
|
|
@@ -105,7 +106,7 @@ export async function createEmbeddingProvider(options) {
|
|
|
105
106
|
localError = formatLocalSetupError(err);
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
|
-
for (const provider of
|
|
109
|
+
for (const provider of REMOTE_EMBEDDING_PROVIDER_IDS) {
|
|
109
110
|
try {
|
|
110
111
|
const result = await createProvider(provider);
|
|
111
112
|
return { ...result, requestedProvider };
|
|
@@ -116,14 +117,18 @@ export async function createEmbeddingProvider(options) {
|
|
|
116
117
|
missingKeyErrors.push(message);
|
|
117
118
|
continue;
|
|
118
119
|
}
|
|
120
|
+
// Non-auth errors (e.g., network) are still fatal
|
|
119
121
|
throw new Error(message, { cause: err });
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
// All providers failed due to missing API keys - return null provider for FTS-only mode
|
|
122
125
|
const details = [...missingKeyErrors, localError].filter(Boolean);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
const reason = details.length > 0 ? details.join("\n\n") : "No embeddings provider available.";
|
|
127
|
+
return {
|
|
128
|
+
provider: null,
|
|
129
|
+
requestedProvider,
|
|
130
|
+
providerUnavailableReason: reason,
|
|
131
|
+
};
|
|
127
132
|
}
|
|
128
133
|
try {
|
|
129
134
|
const primary = await createProvider(requestedProvider);
|
|
@@ -142,15 +147,38 @@ export async function createEmbeddingProvider(options) {
|
|
|
142
147
|
};
|
|
143
148
|
}
|
|
144
149
|
catch (fallbackErr) {
|
|
145
|
-
|
|
150
|
+
// Both primary and fallback failed - check if it's auth-related
|
|
151
|
+
const fallbackReason = formatErrorMessage(fallbackErr);
|
|
152
|
+
const combinedReason = `${reason}\n\nFallback to ${fallback} failed: ${fallbackReason}`;
|
|
153
|
+
if (isMissingApiKeyError(primaryErr) && isMissingApiKeyError(fallbackErr)) {
|
|
154
|
+
// Both failed due to missing API keys - return null for FTS-only mode
|
|
155
|
+
return {
|
|
156
|
+
provider: null,
|
|
157
|
+
requestedProvider,
|
|
158
|
+
fallbackFrom: requestedProvider,
|
|
159
|
+
fallbackReason: reason,
|
|
160
|
+
providerUnavailableReason: combinedReason,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Non-auth errors are still fatal
|
|
164
|
+
throw new Error(combinedReason, { cause: fallbackErr });
|
|
146
165
|
}
|
|
147
166
|
}
|
|
167
|
+
// No fallback configured - check if we should degrade to FTS-only
|
|
168
|
+
if (isMissingApiKeyError(primaryErr)) {
|
|
169
|
+
return {
|
|
170
|
+
provider: null,
|
|
171
|
+
requestedProvider,
|
|
172
|
+
providerUnavailableReason: reason,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
148
175
|
throw new Error(reason, { cause: primaryErr });
|
|
149
176
|
}
|
|
150
177
|
}
|
|
151
178
|
function isNodeLlamaCppMissing(err) {
|
|
152
|
-
if (!(err instanceof Error))
|
|
179
|
+
if (!(err instanceof Error)) {
|
|
153
180
|
return false;
|
|
181
|
+
}
|
|
154
182
|
const code = err.code;
|
|
155
183
|
if (code === "ERR_MODULE_NOT_FOUND") {
|
|
156
184
|
return err.message.includes("node-llama-cpp");
|
|
@@ -174,8 +202,7 @@ function formatLocalSetupError(err) {
|
|
|
174
202
|
? "2) Reinstall Poolbot (this should install node-llama-cpp): npm i -g poolbot@latest"
|
|
175
203
|
: null,
|
|
176
204
|
"3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp",
|
|
177
|
-
|
|
178
|
-
'Or set agents.defaults.memorySearch.provider = "voyage" (remote).',
|
|
205
|
+
...REMOTE_EMBEDDING_PROVIDER_IDS.map((provider) => `Or set agents.defaults.memorySearch.provider = "${provider}" (remote).`),
|
|
179
206
|
]
|
|
180
207
|
.filter(Boolean)
|
|
181
208
|
.join("\n");
|
package/dist/memory/hybrid.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import { applyMMRToHybridResults, DEFAULT_MMR_CONFIG } from "./mmr.js";
|
|
2
|
+
import { applyTemporalDecayToHybridResults, DEFAULT_TEMPORAL_DECAY_CONFIG, } from "./temporal-decay.js";
|
|
3
|
+
export { DEFAULT_MMR_CONFIG };
|
|
4
|
+
export { DEFAULT_TEMPORAL_DECAY_CONFIG };
|
|
1
5
|
export function buildFtsQuery(raw) {
|
|
2
6
|
const tokens = raw
|
|
3
|
-
.match(/[
|
|
7
|
+
.match(/[\p{L}\p{N}_]+/gu)
|
|
4
8
|
?.map((t) => t.trim())
|
|
5
9
|
.filter(Boolean) ?? [];
|
|
6
|
-
if (tokens.length === 0)
|
|
10
|
+
if (tokens.length === 0) {
|
|
7
11
|
return null;
|
|
12
|
+
}
|
|
8
13
|
const quoted = tokens.map((t) => `"${t.replaceAll('"', "")}"`);
|
|
9
14
|
return quoted.join(" AND ");
|
|
10
15
|
}
|
|
@@ -12,7 +17,7 @@ export function bm25RankToScore(rank) {
|
|
|
12
17
|
const normalized = Number.isFinite(rank) ? Math.max(0, rank) : 999;
|
|
13
18
|
return 1 / (1 + normalized);
|
|
14
19
|
}
|
|
15
|
-
export function mergeHybridResults(params) {
|
|
20
|
+
export async function mergeHybridResults(params) {
|
|
16
21
|
const byId = new Map();
|
|
17
22
|
for (const r of params.vector) {
|
|
18
23
|
byId.set(r.id, {
|
|
@@ -30,8 +35,9 @@ export function mergeHybridResults(params) {
|
|
|
30
35
|
const existing = byId.get(r.id);
|
|
31
36
|
if (existing) {
|
|
32
37
|
existing.textScore = r.textScore;
|
|
33
|
-
if (r.snippet && r.snippet.length > 0)
|
|
38
|
+
if (r.snippet && r.snippet.length > 0) {
|
|
34
39
|
existing.snippet = r.snippet;
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
else {
|
|
37
43
|
byId.set(r.id, {
|
|
@@ -57,5 +63,18 @@ export function mergeHybridResults(params) {
|
|
|
57
63
|
source: entry.source,
|
|
58
64
|
};
|
|
59
65
|
});
|
|
60
|
-
|
|
66
|
+
const temporalDecayConfig = { ...DEFAULT_TEMPORAL_DECAY_CONFIG, ...params.temporalDecay };
|
|
67
|
+
const decayed = await applyTemporalDecayToHybridResults({
|
|
68
|
+
results: merged,
|
|
69
|
+
temporalDecay: temporalDecayConfig,
|
|
70
|
+
workspaceDir: params.workspaceDir,
|
|
71
|
+
nowMs: params.nowMs,
|
|
72
|
+
});
|
|
73
|
+
const sorted = decayed.toSorted((a, b) => b.score - a.score);
|
|
74
|
+
// Apply MMR re-ranking if enabled
|
|
75
|
+
const mmrConfig = { ...DEFAULT_MMR_CONFIG, ...params.mmr };
|
|
76
|
+
if (mmrConfig.enabled) {
|
|
77
|
+
return applyMMRToHybridResults(sorted, mmrConfig);
|
|
78
|
+
}
|
|
79
|
+
return sorted;
|
|
61
80
|
}
|
package/dist/memory/manager.js
CHANGED
|
@@ -20,6 +20,7 @@ import { buildFileEntry, chunkMarkdown, ensureDir, hashText, isMemoryPath, listM
|
|
|
20
20
|
import { bm25RankToScore, buildFtsQuery, mergeHybridResults } from "./hybrid.js";
|
|
21
21
|
import { searchKeyword, searchVector } from "./manager-search.js";
|
|
22
22
|
import { ensureMemoryIndexSchema } from "./memory-schema.js";
|
|
23
|
+
import { extractKeywords } from "./query-expansion.js";
|
|
23
24
|
import { requireNodeSqlite } from "./sqlite.js";
|
|
24
25
|
import { loadSqliteVecExtension } from "./sqlite-vec.js";
|
|
25
26
|
const META_KEY = "memory_index_meta_v1";
|
|
@@ -54,6 +55,7 @@ export class MemoryIndexManager {
|
|
|
54
55
|
requestedProvider;
|
|
55
56
|
fallbackFrom;
|
|
56
57
|
fallbackReason;
|
|
58
|
+
providerUnavailableReason;
|
|
57
59
|
openAi;
|
|
58
60
|
gemini;
|
|
59
61
|
voyage;
|
|
@@ -108,6 +110,7 @@ export class MemoryIndexManager {
|
|
|
108
110
|
workspaceDir,
|
|
109
111
|
settings,
|
|
110
112
|
providerResult,
|
|
113
|
+
purpose: params.purpose,
|
|
111
114
|
});
|
|
112
115
|
INDEX_CACHE.set(key, manager);
|
|
113
116
|
return manager;
|
|
@@ -122,6 +125,7 @@ export class MemoryIndexManager {
|
|
|
122
125
|
this.requestedProvider = params.providerResult.requestedProvider;
|
|
123
126
|
this.fallbackFrom = params.providerResult.fallbackFrom;
|
|
124
127
|
this.fallbackReason = params.providerResult.fallbackReason;
|
|
128
|
+
this.providerUnavailableReason = params.providerResult.providerUnavailableReason;
|
|
125
129
|
this.openAi = params.providerResult.openAi;
|
|
126
130
|
this.gemini = params.providerResult.gemini;
|
|
127
131
|
this.voyage = params.providerResult.voyage;
|
|
@@ -146,7 +150,8 @@ export class MemoryIndexManager {
|
|
|
146
150
|
this.ensureWatcher();
|
|
147
151
|
this.ensureSessionListener();
|
|
148
152
|
this.ensureIntervalSync();
|
|
149
|
-
|
|
153
|
+
const statusOnly = params.purpose === "status";
|
|
154
|
+
this.dirty = this.sources.has("memory") && (statusOnly ? !meta : true);
|
|
150
155
|
this.batch = this.resolveBatchConfig();
|
|
151
156
|
}
|
|
152
157
|
async warmSession(sessionKey) {
|
|
@@ -175,6 +180,30 @@ export class MemoryIndexManager {
|
|
|
175
180
|
const maxResults = opts?.maxResults ?? this.settings.query.maxResults;
|
|
176
181
|
const hybrid = this.settings.query.hybrid;
|
|
177
182
|
const candidates = Math.min(200, Math.max(1, Math.floor(maxResults * hybrid.candidateMultiplier)));
|
|
183
|
+
// FTS-only mode: no embedding provider available
|
|
184
|
+
if (!this.provider) {
|
|
185
|
+
if (!this.fts.enabled || !this.fts.available) {
|
|
186
|
+
log.warn("memory search: no provider and FTS unavailable");
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const keywords = extractKeywords(cleaned);
|
|
190
|
+
const searchTerms = keywords.length > 0 ? keywords : [cleaned];
|
|
191
|
+
const resultSets = await Promise.all(searchTerms.map((term) => this.searchKeyword(term, candidates).catch(() => [])));
|
|
192
|
+
const seenIds = new Map();
|
|
193
|
+
for (const results of resultSets) {
|
|
194
|
+
for (const result of results) {
|
|
195
|
+
const existing = seenIds.get(result.id);
|
|
196
|
+
if (!existing || result.score > existing.score) {
|
|
197
|
+
seenIds.set(result.id, result);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const merged = [...seenIds.values()]
|
|
202
|
+
.toSorted((a, b) => b.score - a.score)
|
|
203
|
+
.filter((entry) => entry.score >= minScore)
|
|
204
|
+
.slice(0, maxResults);
|
|
205
|
+
return merged;
|
|
206
|
+
}
|
|
178
207
|
const keywordResults = hybrid.enabled
|
|
179
208
|
? await this.searchKeyword(cleaned, candidates).catch(() => [])
|
|
180
209
|
: [];
|
|
@@ -186,15 +215,19 @@ export class MemoryIndexManager {
|
|
|
186
215
|
if (!hybrid.enabled) {
|
|
187
216
|
return vectorResults.filter((entry) => entry.score >= minScore).slice(0, maxResults);
|
|
188
217
|
}
|
|
189
|
-
const merged = this.mergeHybridResults({
|
|
218
|
+
const merged = await this.mergeHybridResults({
|
|
190
219
|
vector: vectorResults,
|
|
191
220
|
keyword: keywordResults,
|
|
192
221
|
vectorWeight: hybrid.vectorWeight,
|
|
193
222
|
textWeight: hybrid.textWeight,
|
|
223
|
+
mmr: hybrid.mmr,
|
|
224
|
+
temporalDecay: hybrid.temporalDecay,
|
|
194
225
|
});
|
|
195
226
|
return merged.filter((entry) => entry.score >= minScore).slice(0, maxResults);
|
|
196
227
|
}
|
|
197
228
|
async searchVector(queryVec, limit) {
|
|
229
|
+
if (!this.provider)
|
|
230
|
+
return [];
|
|
198
231
|
const results = await searchVector({
|
|
199
232
|
db: this.db,
|
|
200
233
|
vectorTable: VECTOR_TABLE,
|
|
@@ -218,7 +251,7 @@ export class MemoryIndexManager {
|
|
|
218
251
|
const results = await searchKeyword({
|
|
219
252
|
db: this.db,
|
|
220
253
|
ftsTable: FTS_TABLE,
|
|
221
|
-
providerModel: this.provider
|
|
254
|
+
providerModel: this.provider?.model ?? "fts-only",
|
|
222
255
|
query,
|
|
223
256
|
limit,
|
|
224
257
|
snippetMaxChars: SNIPPET_MAX_CHARS,
|
|
@@ -228,8 +261,8 @@ export class MemoryIndexManager {
|
|
|
228
261
|
});
|
|
229
262
|
return results.map((entry) => entry);
|
|
230
263
|
}
|
|
231
|
-
mergeHybridResults(params) {
|
|
232
|
-
const merged = mergeHybridResults({
|
|
264
|
+
async mergeHybridResults(params) {
|
|
265
|
+
const merged = await mergeHybridResults({
|
|
233
266
|
vector: params.vector.map((r) => ({
|
|
234
267
|
id: r.id,
|
|
235
268
|
path: r.path,
|
|
@@ -250,6 +283,9 @@ export class MemoryIndexManager {
|
|
|
250
283
|
})),
|
|
251
284
|
vectorWeight: params.vectorWeight,
|
|
252
285
|
textWeight: params.textWeight,
|
|
286
|
+
workspaceDir: this.workspaceDir,
|
|
287
|
+
mmr: params.mmr,
|
|
288
|
+
temporalDecay: params.temporalDecay,
|
|
253
289
|
});
|
|
254
290
|
return merged.map((entry) => entry);
|
|
255
291
|
}
|
|
@@ -359,8 +395,8 @@ export class MemoryIndexManager {
|
|
|
359
395
|
dirty: this.dirty || this.sessionsDirty,
|
|
360
396
|
workspaceDir: this.workspaceDir,
|
|
361
397
|
dbPath: this.settings.store.path,
|
|
362
|
-
provider: this.provider
|
|
363
|
-
model: this.provider
|
|
398
|
+
provider: this.provider?.id ?? "none",
|
|
399
|
+
model: this.provider?.model,
|
|
364
400
|
requestedProvider: this.requestedProvider,
|
|
365
401
|
sources: Array.from(this.sources),
|
|
366
402
|
extraPaths: this.settings.extraPaths,
|
|
@@ -882,7 +918,7 @@ export class MemoryIndexManager {
|
|
|
882
918
|
try {
|
|
883
919
|
this.db
|
|
884
920
|
.prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`)
|
|
885
|
-
.run(stale.path, "memory", this.provider
|
|
921
|
+
.run(stale.path, "memory", this.provider?.model ?? "fts-only");
|
|
886
922
|
}
|
|
887
923
|
catch { }
|
|
888
924
|
}
|
|
@@ -976,7 +1012,7 @@ export class MemoryIndexManager {
|
|
|
976
1012
|
try {
|
|
977
1013
|
this.db
|
|
978
1014
|
.prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`)
|
|
979
|
-
.run(stale.path, "sessions", this.provider
|
|
1015
|
+
.run(stale.path, "sessions", this.provider?.model ?? "fts-only");
|
|
980
1016
|
}
|
|
981
1017
|
catch { }
|
|
982
1018
|
}
|
|
@@ -1015,8 +1051,8 @@ export class MemoryIndexManager {
|
|
|
1015
1051
|
const meta = this.readMeta();
|
|
1016
1052
|
const needsFullReindex = params?.force ||
|
|
1017
1053
|
!meta ||
|
|
1018
|
-
meta.model !== this.provider
|
|
1019
|
-
meta.provider !== this.provider
|
|
1054
|
+
meta.model !== this.provider?.model ||
|
|
1055
|
+
meta.provider !== this.provider?.id ||
|
|
1020
1056
|
meta.providerKey !== this.providerKey ||
|
|
1021
1057
|
meta.chunkTokens !== this.settings.chunking.tokens ||
|
|
1022
1058
|
meta.chunkOverlap !== this.settings.chunking.overlap ||
|
|
@@ -1068,9 +1104,9 @@ export class MemoryIndexManager {
|
|
|
1068
1104
|
resolveBatchConfig() {
|
|
1069
1105
|
const batch = this.settings.remote?.batch;
|
|
1070
1106
|
const enabled = Boolean(batch?.enabled &&
|
|
1071
|
-
((this.openAi && this.provider
|
|
1072
|
-
(this.gemini && this.provider
|
|
1073
|
-
(this.voyage && this.provider
|
|
1107
|
+
((this.openAi && this.provider?.id === "openai") ||
|
|
1108
|
+
(this.gemini && this.provider?.id === "gemini") ||
|
|
1109
|
+
(this.voyage && this.provider?.id === "voyage")));
|
|
1074
1110
|
return {
|
|
1075
1111
|
enabled,
|
|
1076
1112
|
wait: batch?.wait ?? true,
|
|
@@ -1081,7 +1117,7 @@ export class MemoryIndexManager {
|
|
|
1081
1117
|
}
|
|
1082
1118
|
async activateFallbackProvider(reason) {
|
|
1083
1119
|
const fallback = this.settings.fallback;
|
|
1084
|
-
if (!fallback || fallback === "none" || fallback === this.provider.id)
|
|
1120
|
+
if (!fallback || fallback === "none" || !this.provider || fallback === this.provider.id)
|
|
1085
1121
|
return false;
|
|
1086
1122
|
if (this.fallbackFrom)
|
|
1087
1123
|
return false;
|
|
@@ -1170,8 +1206,8 @@ export class MemoryIndexManager {
|
|
|
1170
1206
|
this.sessionsDirty = false;
|
|
1171
1207
|
}
|
|
1172
1208
|
nextMeta = {
|
|
1173
|
-
model: this.provider
|
|
1174
|
-
provider: this.provider
|
|
1209
|
+
model: this.provider?.model ?? "fts-only",
|
|
1210
|
+
provider: this.provider?.id ?? "none",
|
|
1175
1211
|
providerKey: this.providerKey,
|
|
1176
1212
|
chunkTokens: this.settings.chunking.tokens,
|
|
1177
1213
|
chunkOverlap: this.settings.chunking.overlap,
|
|
@@ -1371,7 +1407,11 @@ export class MemoryIndexManager {
|
|
|
1371
1407
|
if (unique.length === 0)
|
|
1372
1408
|
return new Map();
|
|
1373
1409
|
const out = new Map();
|
|
1374
|
-
const baseParams = [
|
|
1410
|
+
const baseParams = [
|
|
1411
|
+
this.provider?.id ?? "none",
|
|
1412
|
+
this.provider?.model ?? "fts-only",
|
|
1413
|
+
this.providerKey,
|
|
1414
|
+
];
|
|
1375
1415
|
const batchSize = 400;
|
|
1376
1416
|
for (let start = 0; start < unique.length; start += batchSize) {
|
|
1377
1417
|
const batch = unique.slice(start, start + batchSize);
|
|
@@ -1387,7 +1427,7 @@ export class MemoryIndexManager {
|
|
|
1387
1427
|
return out;
|
|
1388
1428
|
}
|
|
1389
1429
|
upsertEmbeddingCache(entries) {
|
|
1390
|
-
if (!this.cache.enabled)
|
|
1430
|
+
if (!this.cache.enabled || !this.provider)
|
|
1391
1431
|
return;
|
|
1392
1432
|
if (entries.length === 0)
|
|
1393
1433
|
return;
|
|
@@ -1461,6 +1501,9 @@ export class MemoryIndexManager {
|
|
|
1461
1501
|
return embeddings;
|
|
1462
1502
|
}
|
|
1463
1503
|
computeProviderKey() {
|
|
1504
|
+
if (!this.provider) {
|
|
1505
|
+
return hashText(JSON.stringify({ provider: "none", model: "fts-only" }));
|
|
1506
|
+
}
|
|
1464
1507
|
if (this.provider.id === "openai" && this.openAi) {
|
|
1465
1508
|
const entries = Object.entries(this.openAi.headers)
|
|
1466
1509
|
.filter(([key]) => key.toLowerCase() !== "authorization")
|
|
@@ -1491,13 +1534,13 @@ export class MemoryIndexManager {
|
|
|
1491
1534
|
return hashText(JSON.stringify({ provider: this.provider.id, model: this.provider.model }));
|
|
1492
1535
|
}
|
|
1493
1536
|
async embedChunksWithBatch(chunks, entry, source) {
|
|
1494
|
-
if (this.provider
|
|
1537
|
+
if (this.provider?.id === "openai" && this.openAi) {
|
|
1495
1538
|
return this.embedChunksWithOpenAiBatch(chunks, entry, source);
|
|
1496
1539
|
}
|
|
1497
|
-
if (this.provider
|
|
1540
|
+
if (this.provider?.id === "gemini" && this.gemini) {
|
|
1498
1541
|
return this.embedChunksWithGeminiBatch(chunks, entry, source);
|
|
1499
1542
|
}
|
|
1500
|
-
if (this.provider
|
|
1543
|
+
if (this.provider?.id === "voyage" && this.voyage) {
|
|
1501
1544
|
return this.embedChunksWithVoyageBatch(chunks, entry, source);
|
|
1502
1545
|
}
|
|
1503
1546
|
return this.embedChunksInBatches(chunks);
|
|
@@ -1602,7 +1645,7 @@ export class MemoryIndexManager {
|
|
|
1602
1645
|
method: "POST",
|
|
1603
1646
|
url: OPENAI_BATCH_ENDPOINT,
|
|
1604
1647
|
body: {
|
|
1605
|
-
model: this.openAi?.model ?? this.provider
|
|
1648
|
+
model: this.openAi?.model ?? this.provider?.model ?? "fts-only",
|
|
1606
1649
|
input: chunk.text,
|
|
1607
1650
|
},
|
|
1608
1651
|
});
|
|
@@ -1700,6 +1743,8 @@ export class MemoryIndexManager {
|
|
|
1700
1743
|
async embedBatchWithRetry(texts) {
|
|
1701
1744
|
if (texts.length === 0)
|
|
1702
1745
|
return [];
|
|
1746
|
+
if (!this.provider)
|
|
1747
|
+
throw new Error("embedding provider unavailable");
|
|
1703
1748
|
let attempt = 0;
|
|
1704
1749
|
let delayMs = EMBEDDING_RETRY_BASE_DELAY_MS;
|
|
1705
1750
|
while (true) {
|
|
@@ -1729,13 +1774,15 @@ export class MemoryIndexManager {
|
|
|
1729
1774
|
return /(rate[_ ]limit|too many requests|429|resource has been exhausted|5\d\d|cloudflare)/i.test(message);
|
|
1730
1775
|
}
|
|
1731
1776
|
resolveEmbeddingTimeout(kind) {
|
|
1732
|
-
const isLocal = this.provider
|
|
1777
|
+
const isLocal = this.provider?.id === "local";
|
|
1733
1778
|
if (kind === "query") {
|
|
1734
1779
|
return isLocal ? EMBEDDING_QUERY_TIMEOUT_LOCAL_MS : EMBEDDING_QUERY_TIMEOUT_REMOTE_MS;
|
|
1735
1780
|
}
|
|
1736
1781
|
return isLocal ? EMBEDDING_BATCH_TIMEOUT_LOCAL_MS : EMBEDDING_BATCH_TIMEOUT_REMOTE_MS;
|
|
1737
1782
|
}
|
|
1738
1783
|
async embedQueryWithTimeout(text) {
|
|
1784
|
+
if (!this.provider)
|
|
1785
|
+
throw new Error("embedding provider unavailable");
|
|
1739
1786
|
const timeoutMs = this.resolveEmbeddingTimeout("query");
|
|
1740
1787
|
log.debug("memory embeddings: query start", { provider: this.provider.id, timeoutMs });
|
|
1741
1788
|
return await this.withTimeout(this.provider.embedQuery(text), timeoutMs, `memory embeddings query timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
@@ -1873,7 +1920,7 @@ export class MemoryIndexManager {
|
|
|
1873
1920
|
try {
|
|
1874
1921
|
this.db
|
|
1875
1922
|
.prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`)
|
|
1876
|
-
.run(entry.path, options.source, this.provider
|
|
1923
|
+
.run(entry.path, options.source, this.provider?.model ?? "fts-only");
|
|
1877
1924
|
}
|
|
1878
1925
|
catch { }
|
|
1879
1926
|
}
|
|
@@ -1883,7 +1930,8 @@ export class MemoryIndexManager {
|
|
|
1883
1930
|
for (let i = 0; i < chunks.length; i++) {
|
|
1884
1931
|
const chunk = chunks[i];
|
|
1885
1932
|
const embedding = embeddings[i] ?? [];
|
|
1886
|
-
const
|
|
1933
|
+
const providerModel = this.provider?.model ?? "fts-only";
|
|
1934
|
+
const id = hashText(`${options.source}:${entry.path}:${chunk.startLine}:${chunk.endLine}:${chunk.hash}:${providerModel}`);
|
|
1887
1935
|
this.db
|
|
1888
1936
|
.prepare(`INSERT INTO chunks (id, path, source, start_line, end_line, hash, model, text, embedding, updated_at)
|
|
1889
1937
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -1893,7 +1941,7 @@ export class MemoryIndexManager {
|
|
|
1893
1941
|
text=excluded.text,
|
|
1894
1942
|
embedding=excluded.embedding,
|
|
1895
1943
|
updated_at=excluded.updated_at`)
|
|
1896
|
-
.run(id, entry.path, options.source, chunk.startLine, chunk.endLine, chunk.hash,
|
|
1944
|
+
.run(id, entry.path, options.source, chunk.startLine, chunk.endLine, chunk.hash, providerModel, chunk.text, JSON.stringify(embedding), now);
|
|
1897
1945
|
if (vectorReady && embedding.length > 0) {
|
|
1898
1946
|
try {
|
|
1899
1947
|
this.db.prepare(`DELETE FROM ${VECTOR_TABLE} WHERE id = ?`).run(id);
|
|
@@ -1907,7 +1955,7 @@ export class MemoryIndexManager {
|
|
|
1907
1955
|
this.db
|
|
1908
1956
|
.prepare(`INSERT INTO ${FTS_TABLE} (text, id, path, source, model, start_line, end_line)\n` +
|
|
1909
1957
|
` VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
1910
|
-
.run(chunk.text, id, entry.path, options.source,
|
|
1958
|
+
.run(chunk.text, id, entry.path, options.source, providerModel, chunk.startLine, chunk.endLine);
|
|
1911
1959
|
}
|
|
1912
1960
|
}
|
|
1913
1961
|
this.db
|