@hanzo/bot 2026.3.8 → 2026.3.10

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.
Files changed (161) hide show
  1. package/dist/{audio-preflight-D_s-peid.js → audio-preflight-BnfuyvxO.js} +4 -4
  2. package/dist/{audio-preflight-BEc8i-bS.js → audio-preflight-CpAXC_Ct.js} +4 -4
  3. package/dist/{audio-transcription-runner-X1KzI7dF.js → audio-transcription-runner-CAOjjGxN.js} +1 -1
  4. package/dist/{audio-transcription-runner-BePCnZfw.js → audio-transcription-runner-GcMnO6sT.js} +1 -1
  5. package/dist/build-info.json +3 -3
  6. package/dist/bundled/boot-md/handler.js +6 -6
  7. package/dist/bundled/session-memory/handler.js +6 -6
  8. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  9. package/dist/{chrome-B24-8NDM.js → chrome--CFg5C_H.js} +8 -8
  10. package/dist/{chrome-C7OwLtx9.js → chrome-jCt9JCU8.js} +8 -8
  11. package/dist/{cloud-connect-CknfBF39.js → cloud-connect-6kdj8st_.js} +1 -1
  12. package/dist/{deliver-D8dBbzpu.js → deliver-BVtVDxwX.js} +1 -1
  13. package/dist/{deliver-DudaV86i.js → deliver-DmfS4khs.js} +1 -1
  14. package/dist/{deliver-runtime-C76IMU4W.js → deliver-runtime-G0G5orrZ.js} +3 -3
  15. package/dist/{deliver-runtime-qDmQqiF-.js → deliver-runtime-PxJvVUhh.js} +3 -3
  16. package/dist/{deps-send-whatsapp.runtime-Cv_awFtm.js → deps-send-whatsapp.runtime-8bLqjmui.js} +7 -7
  17. package/dist/{deps-send-whatsapp.runtime-Cq-TLsJw.js → deps-send-whatsapp.runtime-CrzuaVhC.js} +7 -7
  18. package/dist/entry.js +15 -8
  19. package/dist/extensionAPI.js +6 -6
  20. package/dist/{image-nUHQF6BX.js → image-BdZcUz8M.js} +1 -1
  21. package/dist/{image-BOybyCis.js → image-DSK1hSSV.js} +1 -1
  22. package/dist/{image-runtime-B5M_-diF.js → image-runtime-ueqmfx1a.js} +3 -3
  23. package/dist/{image-runtime-y4msd5bn.js → image-runtime-xqxW2PQA.js} +3 -3
  24. package/dist/llm-slug-generator.js +6 -6
  25. package/dist/{local-launch-C2RER-G3.js → local-launch-BJpBAIR5.js} +37 -33
  26. package/dist/{pi-embedded-BHXPs-Ix.js → pi-embedded-DBn841N-.js} +24 -24
  27. package/dist/{pi-embedded-DvWHP6Nn.js → pi-embedded-DYc6emwb.js} +24 -24
  28. package/dist/{pi-embedded-helpers-xIXwvwuE.js → pi-embedded-helpers-BtnBVL-4.js} +3 -3
  29. package/dist/{pi-embedded-helpers-Ck1qEeMH.js → pi-embedded-helpers-DLm1Mtr2.js} +3 -3
  30. package/dist/plugin-sdk/accounts-B8qv93DH.js +35 -0
  31. package/dist/plugin-sdk/accounts-D2p8t4UO.js +288 -0
  32. package/dist/plugin-sdk/accounts-D96D1U9M.js +46 -0
  33. package/dist/plugin-sdk/active-listener-Mha1rAbh.js +50 -0
  34. package/dist/plugin-sdk/api-key-rotation-D0aZfxXH.js +181 -0
  35. package/dist/plugin-sdk/audio-preflight-D3y8mHJa.js +69 -0
  36. package/dist/plugin-sdk/audio-transcription-runner-j0mQXKSH.js +2205 -0
  37. package/dist/plugin-sdk/audit-membership-runtime-D-Ni2WDc.js +58 -0
  38. package/dist/plugin-sdk/channel-activity-VpA3MxPb.js +94 -0
  39. package/dist/plugin-sdk/channel-web-BP3vDdim.js +2256 -0
  40. package/dist/plugin-sdk/chrome-5jJIDTj0.js +2447 -0
  41. package/dist/plugin-sdk/commands-registry-BVKCdwN_.js +1125 -0
  42. package/dist/plugin-sdk/config-CudVTZDi.js +18200 -0
  43. package/dist/plugin-sdk/deliver-4NrmrRKu.js +1744 -0
  44. package/dist/plugin-sdk/deliver-runtime-CB4wXMTH.js +32 -0
  45. package/dist/plugin-sdk/deps-send-discord.runtime-Di8ELKED.js +23 -0
  46. package/dist/plugin-sdk/deps-send-imessage.runtime-CWWtApbz.js +22 -0
  47. package/dist/plugin-sdk/deps-send-signal.runtime-C6BGyoIY.js +21 -0
  48. package/dist/plugin-sdk/deps-send-slack.runtime-DgtITBuc.js +19 -0
  49. package/dist/plugin-sdk/deps-send-telegram.runtime-DyY4XRxh.js +24 -0
  50. package/dist/plugin-sdk/deps-send-whatsapp.runtime-LKiQNFcF.js +57 -0
  51. package/dist/plugin-sdk/diagnostic-DCPixRez.js +319 -0
  52. package/dist/plugin-sdk/errors-D5bA02--.js +54 -0
  53. package/dist/plugin-sdk/fetch-guard-n0LVdzZL.js +156 -0
  54. package/dist/plugin-sdk/fs-safe-IQ0H7rVD.js +352 -0
  55. package/dist/plugin-sdk/image-n-R2HcNg.js +2314 -0
  56. package/dist/plugin-sdk/image-ops-BpYDXC6N.js +584 -0
  57. package/dist/plugin-sdk/image-runtime-CbCl82B8.js +25 -0
  58. package/dist/plugin-sdk/index.js +50 -50
  59. package/dist/plugin-sdk/ir-DsMX3GcS.js +1296 -0
  60. package/dist/plugin-sdk/local-roots-DR-lR22p.js +186 -0
  61. package/dist/plugin-sdk/logger-2A0UE34q.js +1163 -0
  62. package/dist/plugin-sdk/login-CxFTtHEi.js +57 -0
  63. package/dist/plugin-sdk/login-qr-BCkrf1Zx.js +320 -0
  64. package/dist/plugin-sdk/manager-ClUgSFkG.js +3943 -0
  65. package/dist/plugin-sdk/manager-runtime-MWzYCRyH.js +15 -0
  66. package/dist/plugin-sdk/outbound-Dqs8L8QW.js +212 -0
  67. package/dist/plugin-sdk/outbound-attachment-CPfpUcdI.js +19 -0
  68. package/dist/plugin-sdk/path-alias-guards-LILr7Hrs.js +43 -0
  69. package/dist/plugin-sdk/paths-CfGmXu9A.js +166 -0
  70. package/dist/plugin-sdk/pi-embedded-helpers-CeC8GbRi.js +9731 -0
  71. package/dist/plugin-sdk/pi-model-discovery-DycOMKYh.js +134 -0
  72. package/dist/plugin-sdk/pi-model-discovery-runtime-C64BYe5F.js +8 -0
  73. package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-C-HNtPSw.js +354 -0
  74. package/dist/plugin-sdk/plugins-2gQWMmUN.js +1205 -0
  75. package/dist/plugin-sdk/proxy-fetch-sX3-xzX1.js +38 -0
  76. package/dist/plugin-sdk/pw-ai-D7FTxM3C.js +1938 -0
  77. package/dist/plugin-sdk/qmd-manager-Da3Jq30m.js +1608 -0
  78. package/dist/plugin-sdk/query-expansion-B5Z0In1U.js +1014 -0
  79. package/dist/plugin-sdk/redact-85H1J7mo.js +319 -0
  80. package/dist/plugin-sdk/reply-DmCyOPxV.js +102224 -0
  81. package/dist/plugin-sdk/resolve-outbound-target-y0Sp7gsM.js +40 -0
  82. package/dist/plugin-sdk/run-with-concurrency-CFRxflYW.js +1994 -0
  83. package/dist/plugin-sdk/runtime-whatsapp-login.runtime-fgm84Rdh.js +10 -0
  84. package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-DpMuLd_h.js +19 -0
  85. package/dist/plugin-sdk/send-BQvcPd54.js +3135 -0
  86. package/dist/plugin-sdk/send-Bplfz7UW.js +540 -0
  87. package/dist/plugin-sdk/send-C8gdhoLP.js +414 -0
  88. package/dist/plugin-sdk/send-CTOVZqmi.js +2602 -0
  89. package/dist/plugin-sdk/send-Cld7xlxq.js +503 -0
  90. package/dist/plugin-sdk/session-D4k86ARy.js +169 -0
  91. package/dist/plugin-sdk/signal.js +2 -2
  92. package/dist/plugin-sdk/skill-commands-BfHvtJx2.js +353 -0
  93. package/dist/plugin-sdk/skills-BrE5Yb5o.js +1428 -0
  94. package/dist/plugin-sdk/slash-commands.runtime-UpSrdY-a.js +13 -0
  95. package/dist/plugin-sdk/slash-dispatch.runtime-C-Ymizf2.js +52 -0
  96. package/dist/plugin-sdk/slash-skill-commands.runtime-Bb9wo3w0.js +16 -0
  97. package/dist/plugin-sdk/ssrf-CbvrROKN.js +202 -0
  98. package/dist/plugin-sdk/store-8XS_isi_.js +81 -0
  99. package/dist/plugin-sdk/subagent-registry-runtime-Cl3jJKM1.js +52 -0
  100. package/dist/plugin-sdk/tables-BhfDBQ58.js +55 -0
  101. package/dist/plugin-sdk/target-errors-0DW3k-Ae.js +195 -0
  102. package/dist/plugin-sdk/thinking-DE2FCBnv.js +1209 -0
  103. package/dist/plugin-sdk/tokens-C2tJ8uXs.js +52 -0
  104. package/dist/plugin-sdk/tool-images-B1I6LEp7.js +274 -0
  105. package/dist/plugin-sdk/web-BHzDLmns.js +56 -0
  106. package/dist/plugin-sdk/whatsapp-actions-BoAH0BAS.js +80 -0
  107. package/dist/{pw-ai-DweqbnMJ.js → pw-ai-C-Sy12jT.js} +1 -1
  108. package/dist/{pw-ai-DsYmOxCp.js → pw-ai-pJMhS79V.js} +1 -1
  109. package/dist/{run-main-CgFUs81l.js → run-main-JI9-1g4u.js} +2 -2
  110. package/dist/{slash-dispatch.runtime-D28-UnsO.js → slash-dispatch.runtime-DLP2IeNv.js} +6 -6
  111. package/dist/{slash-dispatch.runtime-DzpJjr3K.js → slash-dispatch.runtime-Vp6IDoCc.js} +6 -6
  112. package/dist/{subagent-registry-runtime-a7xfwPB8.js → subagent-registry-runtime-BlAI3eqU.js} +6 -6
  113. package/dist/{subagent-registry-runtime-Bt-LYyrB.js → subagent-registry-runtime-COKZwsHd.js} +6 -6
  114. package/dist/{web-CREcqhe9.js → web-BEuMJbx-.js} +6 -6
  115. package/dist/{web-IBqHOVI2.js → web-BvId86u4.js} +6 -6
  116. package/extensions/acpx/package.json +1 -1
  117. package/extensions/bluebubbles/package.json +1 -1
  118. package/extensions/ci-fix-loop/package.json +1 -1
  119. package/extensions/continuous-learning/package.json +1 -1
  120. package/extensions/copilot-proxy/package.json +1 -1
  121. package/extensions/diagnostics-otel/package.json +1 -1
  122. package/extensions/diffs/package.json +1 -1
  123. package/extensions/discord/package.json +1 -1
  124. package/extensions/feishu/package.json +1 -1
  125. package/extensions/flow/package.json +1 -1
  126. package/extensions/google-antigravity-auth/package.json +1 -1
  127. package/extensions/google-gemini-cli-auth/package.json +1 -1
  128. package/extensions/googlechat/package.json +1 -1
  129. package/extensions/imessage/package.json +1 -1
  130. package/extensions/irc/package.json +1 -1
  131. package/extensions/line/package.json +1 -1
  132. package/extensions/llm-task/package.json +1 -1
  133. package/extensions/lobster/package.json +1 -1
  134. package/extensions/matrix/CHANGELOG.md +10 -0
  135. package/extensions/matrix/package.json +1 -1
  136. package/extensions/mattermost/package.json +1 -1
  137. package/extensions/memory-core/package.json +1 -1
  138. package/extensions/memory-lancedb/package.json +1 -1
  139. package/extensions/minimax-portal-auth/package.json +1 -1
  140. package/extensions/msteams/CHANGELOG.md +10 -0
  141. package/extensions/msteams/package.json +1 -1
  142. package/extensions/nextcloud-talk/package.json +1 -1
  143. package/extensions/nostr/CHANGELOG.md +10 -0
  144. package/extensions/nostr/package.json +1 -1
  145. package/extensions/open-prose/package.json +1 -1
  146. package/extensions/self-improvement/package.json +1 -1
  147. package/extensions/signal/package.json +1 -1
  148. package/extensions/slack/package.json +1 -1
  149. package/extensions/synology-chat/package.json +1 -1
  150. package/extensions/telegram/package.json +1 -1
  151. package/extensions/tlon/package.json +1 -1
  152. package/extensions/twitch/CHANGELOG.md +10 -0
  153. package/extensions/twitch/package.json +1 -1
  154. package/extensions/voice-call/CHANGELOG.md +10 -0
  155. package/extensions/voice-call/package.json +1 -1
  156. package/extensions/whatsapp/package.json +1 -1
  157. package/extensions/zalo/CHANGELOG.md +10 -0
  158. package/extensions/zalo/package.json +1 -1
  159. package/extensions/zalouser/CHANGELOG.md +10 -0
  160. package/extensions/zalouser/package.json +1 -1
  161. package/package.json +2 -1
@@ -0,0 +1,1608 @@
1
+ import { c as resolveAgentWorkspaceDir, mt as parseAgentSessionKey } from "./run-with-concurrency-CFRxflYW.js";
2
+ import { c as resolveStateDir } from "./paths-MKyEVmEb.js";
3
+ import { a as createSubsystemLogger } from "./logger-2A0UE34q.js";
4
+ import "./paths-CfGmXu9A.js";
5
+ import "./redact-85H1J7mo.js";
6
+ import "./path-alias-guards-LILr7Hrs.js";
7
+ import { l as writeFileWithinRoot } from "./fs-safe-IQ0H7rVD.js";
8
+ import { i as resolveWindowsSpawnProgram, n as materializeWindowsSpawnProgram } from "./windows-spawn-E2JqbJ-S.js";
9
+ import { a as listSessionFilesForAgent, i as buildSessionEntry, r as requireNodeSqlite, t as extractKeywords, v as isFileMissingError, y as statRegularFile } from "./query-expansion-B5Z0In1U.js";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ import fs from "node:fs/promises";
13
+ import { spawn } from "node:child_process";
14
+ import readline from "node:readline";
15
+
16
+ //#region src/memory/qmd-scope.ts
17
+ function isQmdScopeAllowed(scope, sessionKey) {
18
+ if (!scope) return true;
19
+ const parsed = parseQmdSessionScope(sessionKey);
20
+ const channel = parsed.channel;
21
+ const chatType = parsed.chatType;
22
+ const normalizedKey = parsed.normalizedKey ?? "";
23
+ const rawKey = sessionKey?.trim().toLowerCase() ?? "";
24
+ for (const rule of scope.rules ?? []) {
25
+ if (!rule) continue;
26
+ const match = rule.match ?? {};
27
+ if (match.channel && match.channel !== channel) continue;
28
+ if (match.chatType && match.chatType !== chatType) continue;
29
+ const normalizedPrefix = match.keyPrefix?.trim().toLowerCase() || void 0;
30
+ const rawPrefix = match.rawKeyPrefix?.trim().toLowerCase() || void 0;
31
+ if (rawPrefix && !rawKey.startsWith(rawPrefix)) continue;
32
+ if (normalizedPrefix) {
33
+ if (normalizedPrefix.startsWith("agent:")) {
34
+ if (!rawKey.startsWith(normalizedPrefix)) continue;
35
+ } else if (!normalizedKey.startsWith(normalizedPrefix)) continue;
36
+ }
37
+ return rule.action === "allow";
38
+ }
39
+ return (scope.default ?? "allow") === "allow";
40
+ }
41
+ function deriveQmdScopeChannel(key) {
42
+ return parseQmdSessionScope(key).channel;
43
+ }
44
+ function deriveQmdScopeChatType(key) {
45
+ return parseQmdSessionScope(key).chatType;
46
+ }
47
+ function parseQmdSessionScope(key) {
48
+ const normalized = normalizeQmdSessionKey(key);
49
+ if (!normalized) return {};
50
+ const parts = normalized.split(":").filter(Boolean);
51
+ let chatType;
52
+ if (parts.length >= 2 && (parts[1] === "group" || parts[1] === "channel" || parts[1] === "direct" || parts[1] === "dm")) {
53
+ if (parts.includes("group")) chatType = "group";
54
+ else if (parts.includes("channel")) chatType = "channel";
55
+ return {
56
+ normalizedKey: normalized,
57
+ channel: parts[0]?.toLowerCase(),
58
+ chatType: chatType ?? "direct"
59
+ };
60
+ }
61
+ if (normalized.includes(":group:")) return {
62
+ normalizedKey: normalized,
63
+ chatType: "group"
64
+ };
65
+ if (normalized.includes(":channel:")) return {
66
+ normalizedKey: normalized,
67
+ chatType: "channel"
68
+ };
69
+ return {
70
+ normalizedKey: normalized,
71
+ chatType: "direct"
72
+ };
73
+ }
74
+ function normalizeQmdSessionKey(key) {
75
+ if (!key) return;
76
+ const trimmed = key.trim();
77
+ if (!trimmed) return;
78
+ const normalized = (parseAgentSessionKey(trimmed)?.rest ?? trimmed).toLowerCase();
79
+ if (normalized.startsWith("subagent:")) return;
80
+ return normalized;
81
+ }
82
+
83
+ //#endregion
84
+ //#region src/memory/qmd-query-parser.ts
85
+ const log$1 = createSubsystemLogger("memory");
86
+ function parseQmdQueryJson(stdout, stderr) {
87
+ const trimmedStdout = stdout.trim();
88
+ const trimmedStderr = stderr.trim();
89
+ const stdoutIsMarker = trimmedStdout.length > 0 && isQmdNoResultsOutput(trimmedStdout);
90
+ const stderrIsMarker = trimmedStderr.length > 0 && isQmdNoResultsOutput(trimmedStderr);
91
+ if (stdoutIsMarker || !trimmedStdout && stderrIsMarker) return [];
92
+ if (!trimmedStdout) {
93
+ const message = `stdout empty${trimmedStderr ? ` (stderr: ${summarizeQmdStderr(trimmedStderr)})` : ""}`;
94
+ log$1.warn(`qmd query returned invalid JSON: ${message}`);
95
+ throw new Error(`qmd query returned invalid JSON: ${message}`);
96
+ }
97
+ try {
98
+ const parsed = parseQmdQueryResultArray(trimmedStdout);
99
+ if (parsed !== null) return parsed;
100
+ const noisyPayload = extractFirstJsonArray(trimmedStdout);
101
+ if (!noisyPayload) throw new Error("qmd query JSON response was not an array");
102
+ const fallback = parseQmdQueryResultArray(noisyPayload);
103
+ if (fallback !== null) return fallback;
104
+ throw new Error("qmd query JSON response was not an array");
105
+ } catch (err) {
106
+ const message = err instanceof Error ? err.message : String(err);
107
+ log$1.warn(`qmd query returned invalid JSON: ${message}`);
108
+ throw new Error(`qmd query returned invalid JSON: ${message}`, { cause: err });
109
+ }
110
+ }
111
+ function isQmdNoResultsOutput(raw) {
112
+ return raw.split(/\r?\n/).map((line) => line.trim().toLowerCase().replace(/\s+/g, " ")).filter((line) => line.length > 0).some((line) => isQmdNoResultsLine(line));
113
+ }
114
+ function isQmdNoResultsLine(line) {
115
+ if (line === "no results found" || line === "no results found.") return true;
116
+ return /^(?:\[[^\]]+\]\s*)?(?:(?:warn(?:ing)?|info|error|qmd)\s*:\s*)+no results found\.?$/.test(line);
117
+ }
118
+ function summarizeQmdStderr(raw) {
119
+ return raw.length <= 120 ? raw : `${raw.slice(0, 117)}...`;
120
+ }
121
+ function parseQmdQueryResultArray(raw) {
122
+ try {
123
+ const parsed = JSON.parse(raw);
124
+ if (!Array.isArray(parsed)) return null;
125
+ return parsed;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ function extractFirstJsonArray(raw) {
131
+ const start = raw.indexOf("[");
132
+ if (start < 0) return null;
133
+ let depth = 0;
134
+ let inString = false;
135
+ let escaped = false;
136
+ for (let i = start; i < raw.length; i += 1) {
137
+ const char = raw[i];
138
+ if (char === void 0) break;
139
+ if (inString) {
140
+ if (escaped) {
141
+ escaped = false;
142
+ continue;
143
+ }
144
+ if (char === "\\") escaped = true;
145
+ else if (char === "\"") inString = false;
146
+ continue;
147
+ }
148
+ if (char === "\"") {
149
+ inString = true;
150
+ continue;
151
+ }
152
+ if (char === "[") depth += 1;
153
+ else if (char === "]") {
154
+ depth -= 1;
155
+ if (depth === 0) return raw.slice(start, i + 1);
156
+ }
157
+ }
158
+ return null;
159
+ }
160
+
161
+ //#endregion
162
+ //#region src/memory/qmd-manager.ts
163
+ const log = createSubsystemLogger("memory");
164
+ const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
165
+ const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
166
+ const MAX_QMD_OUTPUT_CHARS = 2e5;
167
+ const NUL_MARKER_RE = /(?:\^@|\\0|\\x00|\\u0000|null\s*byte|nul\s*byte)/i;
168
+ const QMD_EMBED_BACKOFF_BASE_MS = 6e4;
169
+ const QMD_EMBED_BACKOFF_MAX_MS = 3600 * 1e3;
170
+ const HAN_SCRIPT_RE = /[\u3400-\u9fff]/u;
171
+ const QMD_BM25_HAN_KEYWORD_LIMIT = 12;
172
+ let qmdEmbedQueueTail = Promise.resolve();
173
+ function resolveWindowsCommandShim(command) {
174
+ if (process.platform !== "win32") return command;
175
+ const trimmed = command.trim();
176
+ if (!trimmed) return command;
177
+ const ext = path.extname(trimmed).toLowerCase();
178
+ if (ext === ".cmd" || ext === ".exe" || ext === ".bat") return command;
179
+ const base = path.basename(trimmed).toLowerCase();
180
+ if (base === "qmd" || base === "mcporter") return `${trimmed}.cmd`;
181
+ return command;
182
+ }
183
+ function resolveSpawnInvocation(params) {
184
+ return materializeWindowsSpawnProgram(resolveWindowsSpawnProgram({
185
+ command: resolveWindowsCommandShim(params.command),
186
+ platform: process.platform,
187
+ env: params.env,
188
+ execPath: process.execPath,
189
+ packageName: params.packageName,
190
+ allowShellFallback: true
191
+ }), params.args);
192
+ }
193
+ function isWindowsCmdSpawnEinval(err, command) {
194
+ if (process.platform !== "win32") return false;
195
+ if (err?.code !== "EINVAL") return false;
196
+ return /(^|[\\/])mcporter\.cmd$/i.test(command);
197
+ }
198
+ function hasHanScript(value) {
199
+ return HAN_SCRIPT_RE.test(value);
200
+ }
201
+ function normalizeHanBm25Query(query) {
202
+ const trimmed = query.trim();
203
+ if (!trimmed || !hasHanScript(trimmed)) return trimmed;
204
+ const keywords = extractKeywords(trimmed);
205
+ const normalizedKeywords = [];
206
+ const seen = /* @__PURE__ */ new Set();
207
+ for (const keyword of keywords) {
208
+ const token = keyword.trim();
209
+ if (!token || seen.has(token)) continue;
210
+ const includesHan = hasHanScript(token);
211
+ if (includesHan && Array.from(token).length < 2) continue;
212
+ if (!includesHan && token.length < 2) continue;
213
+ seen.add(token);
214
+ normalizedKeywords.push(token);
215
+ if (normalizedKeywords.length >= QMD_BM25_HAN_KEYWORD_LIMIT) break;
216
+ }
217
+ return normalizedKeywords.length > 0 ? normalizedKeywords.join(" ") : trimmed;
218
+ }
219
+ async function runWithQmdEmbedLock(task) {
220
+ const previous = qmdEmbedQueueTail;
221
+ let release;
222
+ qmdEmbedQueueTail = new Promise((resolve) => {
223
+ release = resolve;
224
+ });
225
+ await previous.catch(() => void 0);
226
+ try {
227
+ return await task();
228
+ } finally {
229
+ release?.();
230
+ }
231
+ }
232
+ var QmdMemoryManager = class QmdMemoryManager {
233
+ static async create(params) {
234
+ const resolved = params.resolved.qmd;
235
+ if (!resolved) return null;
236
+ const manager = new QmdMemoryManager({
237
+ cfg: params.cfg,
238
+ agentId: params.agentId,
239
+ resolved
240
+ });
241
+ await manager.initialize(params.mode ?? "full");
242
+ return manager;
243
+ }
244
+ constructor(params) {
245
+ this.collectionRoots = /* @__PURE__ */ new Map();
246
+ this.sources = /* @__PURE__ */ new Set();
247
+ this.docPathCache = /* @__PURE__ */ new Map();
248
+ this.exportedSessionState = /* @__PURE__ */ new Map();
249
+ this.maxQmdOutputChars = MAX_QMD_OUTPUT_CHARS;
250
+ this.updateTimer = null;
251
+ this.pendingUpdate = null;
252
+ this.queuedForcedUpdate = null;
253
+ this.queuedForcedRuns = 0;
254
+ this.closed = false;
255
+ this.db = null;
256
+ this.lastUpdateAt = null;
257
+ this.lastEmbedAt = null;
258
+ this.embedBackoffUntil = null;
259
+ this.embedFailureCount = 0;
260
+ this.attemptedNullByteCollectionRepair = false;
261
+ this.attemptedDuplicateDocumentRepair = false;
262
+ this.cfg = params.cfg;
263
+ this.agentId = params.agentId;
264
+ this.qmd = params.resolved;
265
+ this.workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
266
+ this.stateDir = resolveStateDir(process.env, os.homedir);
267
+ this.agentStateDir = path.join(this.stateDir, "agents", this.agentId);
268
+ this.qmdDir = path.join(this.agentStateDir, "qmd");
269
+ this.xdgConfigHome = path.join(this.qmdDir, "xdg-config");
270
+ this.xdgCacheHome = path.join(this.qmdDir, "xdg-cache");
271
+ this.indexPath = path.join(this.xdgCacheHome, "qmd", "index.sqlite");
272
+ this.env = {
273
+ ...process.env,
274
+ XDG_CONFIG_HOME: this.xdgConfigHome,
275
+ QMD_CONFIG_DIR: this.xdgConfigHome,
276
+ XDG_CACHE_HOME: this.xdgCacheHome,
277
+ NO_COLOR: "1"
278
+ };
279
+ this.sessionExporter = this.qmd.sessions.enabled ? {
280
+ dir: this.qmd.sessions.exportDir ?? path.join(this.qmdDir, "sessions"),
281
+ retentionMs: this.qmd.sessions.retentionDays ? this.qmd.sessions.retentionDays * 24 * 60 * 60 * 1e3 : void 0,
282
+ collectionName: this.pickSessionCollectionName()
283
+ } : null;
284
+ if (this.sessionExporter) this.qmd.collections = [...this.qmd.collections, {
285
+ name: this.sessionExporter.collectionName,
286
+ path: this.sessionExporter.dir,
287
+ pattern: "**/*.md",
288
+ kind: "sessions"
289
+ }];
290
+ this.managedCollectionNames = this.computeManagedCollectionNames();
291
+ }
292
+ async initialize(mode) {
293
+ this.bootstrapCollections();
294
+ if (mode === "status") return;
295
+ await fs.mkdir(this.xdgConfigHome, { recursive: true });
296
+ await fs.mkdir(this.xdgCacheHome, { recursive: true });
297
+ await fs.mkdir(path.dirname(this.indexPath), { recursive: true });
298
+ if (this.sessionExporter) await fs.mkdir(this.sessionExporter.dir, { recursive: true });
299
+ await this.symlinkSharedModels();
300
+ await this.ensureCollections();
301
+ if (this.qmd.update.onBoot) {
302
+ const bootRun = this.runUpdate("boot", true);
303
+ if (this.qmd.update.waitForBootSync) await bootRun.catch((err) => {
304
+ log.warn(`qmd boot update failed: ${String(err)}`);
305
+ });
306
+ else bootRun.catch((err) => {
307
+ log.warn(`qmd boot update failed: ${String(err)}`);
308
+ });
309
+ }
310
+ if (this.qmd.update.intervalMs > 0) this.updateTimer = setInterval(() => {
311
+ this.runUpdate("interval").catch((err) => {
312
+ log.warn(`qmd update failed (${String(err)})`);
313
+ });
314
+ }, this.qmd.update.intervalMs);
315
+ }
316
+ bootstrapCollections() {
317
+ this.collectionRoots.clear();
318
+ this.sources.clear();
319
+ for (const collection of this.qmd.collections) {
320
+ const kind = collection.kind === "sessions" ? "sessions" : "memory";
321
+ this.collectionRoots.set(collection.name, {
322
+ path: collection.path,
323
+ kind
324
+ });
325
+ this.sources.add(kind);
326
+ }
327
+ }
328
+ async ensureCollections() {
329
+ const existing = await this.listCollectionsBestEffort();
330
+ await this.migrateLegacyUnscopedCollections(existing);
331
+ for (const collection of this.qmd.collections) {
332
+ const listed = existing.get(collection.name);
333
+ if (listed && !this.shouldRebindCollection(collection, listed)) continue;
334
+ if (listed) try {
335
+ await this.removeCollection(collection.name);
336
+ } catch (err) {
337
+ const message = err instanceof Error ? err.message : String(err);
338
+ if (!this.isCollectionMissingError(message)) log.warn(`qmd collection remove failed for ${collection.name}: ${message}`);
339
+ }
340
+ try {
341
+ await this.ensureCollectionPath(collection);
342
+ await this.addCollection(collection.path, collection.name, collection.pattern);
343
+ existing.set(collection.name, {
344
+ path: collection.path,
345
+ pattern: collection.pattern
346
+ });
347
+ } catch (err) {
348
+ const message = err instanceof Error ? err.message : String(err);
349
+ if (this.isCollectionAlreadyExistsError(message)) {
350
+ if (!await this.tryRebindConflictingCollection({
351
+ collection,
352
+ existing,
353
+ addErrorMessage: message
354
+ })) log.warn(`qmd collection add skipped for ${collection.name}: ${message}`);
355
+ continue;
356
+ }
357
+ log.warn(`qmd collection add failed for ${collection.name}: ${message}`);
358
+ }
359
+ }
360
+ }
361
+ async listCollectionsBestEffort() {
362
+ const existing = /* @__PURE__ */ new Map();
363
+ try {
364
+ const result = await this.runQmd([
365
+ "collection",
366
+ "list",
367
+ "--json"
368
+ ], { timeoutMs: this.qmd.update.commandTimeoutMs });
369
+ const parsed = this.parseListedCollections(result.stdout);
370
+ for (const [name, details] of parsed) existing.set(name, details);
371
+ } catch {}
372
+ return existing;
373
+ }
374
+ findCollectionByPathPattern(collection, listed) {
375
+ for (const [name, details] of listed) {
376
+ if (!details.path || typeof details.pattern !== "string") continue;
377
+ if (!this.pathsMatch(details.path, collection.path)) continue;
378
+ if (details.pattern !== collection.pattern) continue;
379
+ return name;
380
+ }
381
+ return null;
382
+ }
383
+ async tryRebindConflictingCollection(params) {
384
+ const { collection, existing, addErrorMessage } = params;
385
+ let conflictName = this.findCollectionByPathPattern(collection, existing);
386
+ if (!conflictName) {
387
+ const refreshed = await this.listCollectionsBestEffort();
388
+ existing.clear();
389
+ for (const [name, details] of refreshed) existing.set(name, details);
390
+ conflictName = this.findCollectionByPathPattern(collection, existing);
391
+ }
392
+ if (!conflictName) return false;
393
+ if (conflictName === collection.name) {
394
+ existing.set(collection.name, {
395
+ path: collection.path,
396
+ pattern: collection.pattern
397
+ });
398
+ return true;
399
+ }
400
+ log.warn(`qmd collection add conflict for ${collection.name}: path+pattern already bound by ${conflictName}; rebinding`);
401
+ try {
402
+ await this.removeCollection(conflictName);
403
+ existing.delete(conflictName);
404
+ } catch (removeErr) {
405
+ const removeMessage = removeErr instanceof Error ? removeErr.message : String(removeErr);
406
+ if (!this.isCollectionMissingError(removeMessage)) log.warn(`qmd collection remove failed for ${conflictName}: ${removeMessage}`);
407
+ return false;
408
+ }
409
+ try {
410
+ await this.addCollection(collection.path, collection.name, collection.pattern);
411
+ existing.set(collection.name, {
412
+ path: collection.path,
413
+ pattern: collection.pattern
414
+ });
415
+ return true;
416
+ } catch (retryErr) {
417
+ const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
418
+ log.warn(`qmd collection add failed for ${collection.name} after rebinding ${conflictName}: ${retryMessage} (initial: ${addErrorMessage})`);
419
+ return false;
420
+ }
421
+ }
422
+ async migrateLegacyUnscopedCollections(existing) {
423
+ for (const collection of this.qmd.collections) {
424
+ if (existing.has(collection.name)) continue;
425
+ const legacyName = this.deriveLegacyCollectionName(collection.name);
426
+ if (!legacyName) continue;
427
+ const listedLegacy = existing.get(legacyName);
428
+ if (!listedLegacy) continue;
429
+ if (!this.canMigrateLegacyCollection(collection, listedLegacy)) {
430
+ log.debug(`qmd legacy collection migration skipped for ${legacyName} (path/pattern mismatch)`);
431
+ continue;
432
+ }
433
+ try {
434
+ await this.removeCollection(legacyName);
435
+ existing.delete(legacyName);
436
+ } catch (err) {
437
+ const message = err instanceof Error ? err.message : String(err);
438
+ if (!this.isCollectionMissingError(message)) log.warn(`qmd collection remove failed for ${legacyName}: ${message}`);
439
+ }
440
+ }
441
+ }
442
+ deriveLegacyCollectionName(scopedName) {
443
+ const agentSuffix = `-${this.sanitizeCollectionNameSegment(this.agentId)}`;
444
+ if (!scopedName.endsWith(agentSuffix)) return null;
445
+ return scopedName.slice(0, -agentSuffix.length).trim() || null;
446
+ }
447
+ canMigrateLegacyCollection(collection, listedLegacy) {
448
+ if (listedLegacy.path && !this.pathsMatch(listedLegacy.path, collection.path)) return false;
449
+ if (typeof listedLegacy.pattern === "string" && listedLegacy.pattern !== collection.pattern) return false;
450
+ return true;
451
+ }
452
+ async ensureCollectionPath(collection) {
453
+ if (!this.isDirectoryGlobPattern(collection.pattern)) return;
454
+ await fs.mkdir(collection.path, { recursive: true });
455
+ }
456
+ isDirectoryGlobPattern(pattern) {
457
+ return pattern.includes("*") || pattern.includes("?") || pattern.includes("[");
458
+ }
459
+ isCollectionAlreadyExistsError(message) {
460
+ const lower = message.toLowerCase();
461
+ return lower.includes("already exists") || lower.includes("exists");
462
+ }
463
+ isCollectionMissingError(message) {
464
+ const lower = message.toLowerCase();
465
+ return lower.includes("not found") || lower.includes("does not exist") || lower.includes("missing");
466
+ }
467
+ isMissingCollectionSearchError(err) {
468
+ const message = err instanceof Error ? err.message : String(err);
469
+ return this.isCollectionMissingError(message) && message.toLowerCase().includes("collection");
470
+ }
471
+ async tryRepairMissingCollectionSearch(err) {
472
+ if (!this.isMissingCollectionSearchError(err)) return false;
473
+ log.warn("qmd search failed because a managed collection is missing; repairing collections and retrying once");
474
+ await this.ensureCollections();
475
+ return true;
476
+ }
477
+ async addCollection(pathArg, name, pattern) {
478
+ await this.runQmd([
479
+ "collection",
480
+ "add",
481
+ pathArg,
482
+ "--name",
483
+ name,
484
+ "--mask",
485
+ pattern
486
+ ], { timeoutMs: this.qmd.update.commandTimeoutMs });
487
+ }
488
+ async removeCollection(name) {
489
+ await this.runQmd([
490
+ "collection",
491
+ "remove",
492
+ name
493
+ ], { timeoutMs: this.qmd.update.commandTimeoutMs });
494
+ }
495
+ parseListedCollections(output) {
496
+ const listed = /* @__PURE__ */ new Map();
497
+ const trimmed = output.trim();
498
+ if (!trimmed) return listed;
499
+ try {
500
+ const parsed = JSON.parse(trimmed);
501
+ if (Array.isArray(parsed)) {
502
+ for (const entry of parsed) {
503
+ if (typeof entry === "string") {
504
+ listed.set(entry, {});
505
+ continue;
506
+ }
507
+ if (!entry || typeof entry !== "object") continue;
508
+ const name = entry.name;
509
+ if (typeof name !== "string") continue;
510
+ const listedPath = entry.path;
511
+ const listedPattern = entry.pattern;
512
+ const listedMask = entry.mask;
513
+ listed.set(name, {
514
+ path: typeof listedPath === "string" ? listedPath : void 0,
515
+ pattern: typeof listedPattern === "string" ? listedPattern : typeof listedMask === "string" ? listedMask : void 0
516
+ });
517
+ }
518
+ return listed;
519
+ }
520
+ } catch {}
521
+ let currentName = null;
522
+ for (const rawLine of output.split(/\r?\n/)) {
523
+ const line = rawLine.trimEnd();
524
+ if (!line.trim()) {
525
+ currentName = null;
526
+ continue;
527
+ }
528
+ const collectionLine = /^\s*([a-z0-9._-]+)\s+\(qmd:\/\/[^)]+\)\s*$/i.exec(line);
529
+ if (collectionLine) {
530
+ currentName = collectionLine[1];
531
+ if (!listed.has(currentName)) listed.set(currentName, {});
532
+ continue;
533
+ }
534
+ if (/^\s*collections\b/i.test(line)) continue;
535
+ const bareNameLine = /^\s*([a-z0-9._-]+)\s*$/i.exec(line);
536
+ if (bareNameLine && !line.includes(":")) {
537
+ currentName = bareNameLine[1];
538
+ if (!listed.has(currentName)) listed.set(currentName, {});
539
+ continue;
540
+ }
541
+ if (!currentName) continue;
542
+ const patternLine = /^\s*(?:pattern|mask)\s*:\s*(.+?)\s*$/i.exec(line);
543
+ if (patternLine) {
544
+ const existing = listed.get(currentName) ?? {};
545
+ existing.pattern = patternLine[1].trim();
546
+ listed.set(currentName, existing);
547
+ continue;
548
+ }
549
+ const pathLine = /^\s*path\s*:\s*(.+?)\s*$/i.exec(line);
550
+ if (pathLine) {
551
+ const existing = listed.get(currentName) ?? {};
552
+ existing.path = pathLine[1].trim();
553
+ listed.set(currentName, existing);
554
+ }
555
+ }
556
+ return listed;
557
+ }
558
+ shouldRebindCollection(collection, listed) {
559
+ if (!listed.path) return false;
560
+ if (!this.pathsMatch(listed.path, collection.path)) return true;
561
+ if (typeof listed.pattern === "string" && listed.pattern !== collection.pattern) return true;
562
+ return false;
563
+ }
564
+ pathsMatch(left, right) {
565
+ const normalize = (value) => {
566
+ const resolved = path.isAbsolute(value) ? path.resolve(value) : path.resolve(this.workspaceDir, value);
567
+ const normalized = path.normalize(resolved);
568
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
569
+ };
570
+ return normalize(left) === normalize(right);
571
+ }
572
+ shouldRepairNullByteCollectionError(err) {
573
+ const message = err instanceof Error ? err.message : String(err);
574
+ const lower = message.toLowerCase();
575
+ return (lower.includes("enotdir") || lower.includes("not a directory")) && NUL_MARKER_RE.test(message);
576
+ }
577
+ shouldRepairDuplicateDocumentConstraint(err) {
578
+ const lower = (err instanceof Error ? err.message : String(err)).toLowerCase();
579
+ return lower.includes("unique constraint failed") && lower.includes("documents.collection") && lower.includes("documents.path");
580
+ }
581
+ async rebuildManagedCollectionsForRepair(reason) {
582
+ for (const collection of this.qmd.collections) {
583
+ try {
584
+ await this.removeCollection(collection.name);
585
+ } catch (removeErr) {
586
+ const removeMessage = removeErr instanceof Error ? removeErr.message : String(removeErr);
587
+ if (!this.isCollectionMissingError(removeMessage)) log.warn(`qmd collection remove failed for ${collection.name}: ${removeMessage}`);
588
+ }
589
+ try {
590
+ await this.addCollection(collection.path, collection.name, collection.pattern);
591
+ } catch (addErr) {
592
+ const addMessage = addErr instanceof Error ? addErr.message : String(addErr);
593
+ if (!this.isCollectionAlreadyExistsError(addMessage)) log.warn(`qmd collection add failed for ${collection.name}: ${addMessage}`);
594
+ }
595
+ }
596
+ log.warn(`qmd managed collections rebuilt for update repair (${reason})`);
597
+ }
598
+ async tryRepairNullByteCollections(err, reason) {
599
+ if (this.attemptedNullByteCollectionRepair) return false;
600
+ if (!this.shouldRepairNullByteCollectionError(err)) return false;
601
+ this.attemptedNullByteCollectionRepair = true;
602
+ log.warn(`qmd update failed with suspected null-byte collection metadata (${reason}); rebuilding managed collections and retrying once`);
603
+ await this.rebuildManagedCollectionsForRepair(`null-byte metadata (${reason})`);
604
+ return true;
605
+ }
606
+ async tryRepairDuplicateDocumentConstraint(err, reason) {
607
+ if (this.attemptedDuplicateDocumentRepair) return false;
608
+ if (!this.shouldRepairDuplicateDocumentConstraint(err)) return false;
609
+ this.attemptedDuplicateDocumentRepair = true;
610
+ log.warn(`qmd update failed with duplicate document constraint (${reason}); rebuilding managed collections and retrying once`);
611
+ await this.rebuildManagedCollectionsForRepair(`duplicate-document constraint (${reason})`);
612
+ return true;
613
+ }
614
+ async search(query, opts) {
615
+ if (!this.isScopeAllowed(opts?.sessionKey)) {
616
+ this.logScopeDenied(opts?.sessionKey);
617
+ return [];
618
+ }
619
+ const trimmed = query.trim();
620
+ if (!trimmed) return [];
621
+ await this.waitForPendingUpdateBeforeSearch();
622
+ const limit = Math.min(this.qmd.limits.maxResults, opts?.maxResults ?? this.qmd.limits.maxResults);
623
+ const collectionNames = this.listManagedCollectionNames();
624
+ if (collectionNames.length === 0) {
625
+ log.warn("qmd query skipped: no managed collections configured");
626
+ return [];
627
+ }
628
+ const qmdSearchCommand = this.qmd.searchMode;
629
+ const mcporterEnabled = this.qmd.mcporter.enabled;
630
+ const runSearchAttempt = async (allowMissingCollectionRepair) => {
631
+ try {
632
+ if (mcporterEnabled) {
633
+ const tool = qmdSearchCommand === "search" ? "search" : qmdSearchCommand === "vsearch" ? "vector_search" : "deep_search";
634
+ const minScore = opts?.minScore ?? 0;
635
+ if (collectionNames.length > 1) return await this.runMcporterAcrossCollections({
636
+ tool,
637
+ query: trimmed,
638
+ limit,
639
+ minScore,
640
+ collectionNames
641
+ });
642
+ return await this.runQmdSearchViaMcporter({
643
+ mcporter: this.qmd.mcporter,
644
+ tool,
645
+ query: trimmed,
646
+ limit,
647
+ minScore,
648
+ collection: collectionNames[0],
649
+ timeoutMs: this.qmd.limits.timeoutMs
650
+ });
651
+ }
652
+ if (collectionNames.length > 1) return await this.runQueryAcrossCollections(trimmed, limit, collectionNames, qmdSearchCommand);
653
+ const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit);
654
+ args.push(...this.buildCollectionFilterArgs(collectionNames));
655
+ const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
656
+ return parseQmdQueryJson(result.stdout, result.stderr);
657
+ } catch (err) {
658
+ if (allowMissingCollectionRepair && this.isMissingCollectionSearchError(err)) throw err;
659
+ if (!mcporterEnabled && qmdSearchCommand !== "query" && this.isUnsupportedQmdOptionError(err)) {
660
+ log.warn(`qmd ${qmdSearchCommand} does not support configured flags; retrying search with qmd query`);
661
+ try {
662
+ if (collectionNames.length > 1) return await this.runQueryAcrossCollections(trimmed, limit, collectionNames, "query");
663
+ const fallbackArgs = this.buildSearchArgs("query", trimmed, limit);
664
+ fallbackArgs.push(...this.buildCollectionFilterArgs(collectionNames));
665
+ const fallback = await this.runQmd(fallbackArgs, { timeoutMs: this.qmd.limits.timeoutMs });
666
+ return parseQmdQueryJson(fallback.stdout, fallback.stderr);
667
+ } catch (fallbackErr) {
668
+ log.warn(`qmd query fallback failed: ${String(fallbackErr)}`);
669
+ throw fallbackErr instanceof Error ? fallbackErr : new Error(String(fallbackErr));
670
+ }
671
+ }
672
+ const label = mcporterEnabled ? "mcporter/qmd" : `qmd ${qmdSearchCommand}`;
673
+ log.warn(`${label} failed: ${String(err)}`);
674
+ throw err instanceof Error ? err : new Error(String(err));
675
+ }
676
+ };
677
+ let parsed;
678
+ try {
679
+ parsed = await runSearchAttempt(true);
680
+ } catch (err) {
681
+ if (!await this.tryRepairMissingCollectionSearch(err)) throw err instanceof Error ? err : new Error(String(err));
682
+ parsed = await runSearchAttempt(false);
683
+ }
684
+ const results = [];
685
+ for (const entry of parsed) {
686
+ const docHints = this.normalizeDocHints({
687
+ preferredCollection: entry.collection,
688
+ preferredFile: entry.file
689
+ });
690
+ const doc = await this.resolveDocLocation(entry.docid, docHints);
691
+ if (!doc) continue;
692
+ const snippet = entry.snippet?.slice(0, this.qmd.limits.maxSnippetChars) ?? "";
693
+ const lines = this.extractSnippetLines(snippet);
694
+ const score = typeof entry.score === "number" ? entry.score : 0;
695
+ if (score < (opts?.minScore ?? 0)) continue;
696
+ results.push({
697
+ path: doc.rel,
698
+ startLine: lines.startLine,
699
+ endLine: lines.endLine,
700
+ score,
701
+ snippet,
702
+ source: doc.source
703
+ });
704
+ }
705
+ return this.clampResultsByInjectedChars(this.diversifyResultsBySource(results, limit));
706
+ }
707
+ async sync(params) {
708
+ if (params?.progress) params.progress({
709
+ completed: 0,
710
+ total: 1,
711
+ label: "Updating QMD index…"
712
+ });
713
+ await this.runUpdate(params?.reason ?? "manual", params?.force);
714
+ if (params?.progress) params.progress({
715
+ completed: 1,
716
+ total: 1,
717
+ label: "QMD index updated"
718
+ });
719
+ }
720
+ async readFile(params) {
721
+ const relPath = params.relPath?.trim();
722
+ if (!relPath) throw new Error("path required");
723
+ const absPath = this.resolveReadPath(relPath);
724
+ if (!absPath.endsWith(".md")) throw new Error("path required");
725
+ if ((await statRegularFile(absPath)).missing) return {
726
+ text: "",
727
+ path: relPath
728
+ };
729
+ if (params.from !== void 0 || params.lines !== void 0) {
730
+ const partial = await this.readPartialText(absPath, params.from, params.lines);
731
+ if (partial.missing) return {
732
+ text: "",
733
+ path: relPath
734
+ };
735
+ return {
736
+ text: partial.text,
737
+ path: relPath
738
+ };
739
+ }
740
+ const full = await this.readFullText(absPath);
741
+ if (full.missing) return {
742
+ text: "",
743
+ path: relPath
744
+ };
745
+ if (!params.from && !params.lines) return {
746
+ text: full.text,
747
+ path: relPath
748
+ };
749
+ const lines = full.text.split("\n");
750
+ const start = Math.max(1, params.from ?? 1);
751
+ const count = Math.max(1, params.lines ?? lines.length);
752
+ return {
753
+ text: lines.slice(start - 1, start - 1 + count).join("\n"),
754
+ path: relPath
755
+ };
756
+ }
757
+ status() {
758
+ const counts = this.readCounts();
759
+ return {
760
+ backend: "qmd",
761
+ provider: "qmd",
762
+ model: "qmd",
763
+ requestedProvider: "qmd",
764
+ files: counts.totalDocuments,
765
+ chunks: counts.totalDocuments,
766
+ dirty: false,
767
+ workspaceDir: this.workspaceDir,
768
+ dbPath: this.indexPath,
769
+ sources: Array.from(this.sources),
770
+ sourceCounts: counts.sourceCounts,
771
+ vector: {
772
+ enabled: true,
773
+ available: true
774
+ },
775
+ batch: {
776
+ enabled: false,
777
+ failures: 0,
778
+ limit: 0,
779
+ wait: false,
780
+ concurrency: 0,
781
+ pollIntervalMs: 0,
782
+ timeoutMs: 0
783
+ },
784
+ custom: { qmd: {
785
+ collections: this.qmd.collections.length,
786
+ lastUpdateAt: this.lastUpdateAt
787
+ } }
788
+ };
789
+ }
790
+ async probeEmbeddingAvailability() {
791
+ return { ok: true };
792
+ }
793
+ async probeVectorAvailability() {
794
+ return true;
795
+ }
796
+ async close() {
797
+ if (this.closed) return;
798
+ this.closed = true;
799
+ if (this.updateTimer) {
800
+ clearInterval(this.updateTimer);
801
+ this.updateTimer = null;
802
+ }
803
+ this.queuedForcedRuns = 0;
804
+ await this.pendingUpdate?.catch(() => void 0);
805
+ await this.queuedForcedUpdate?.catch(() => void 0);
806
+ if (this.db) {
807
+ this.db.close();
808
+ this.db = null;
809
+ }
810
+ }
811
+ async runUpdate(reason, force, opts) {
812
+ if (this.closed) return;
813
+ if (this.pendingUpdate) {
814
+ if (force) return this.enqueueForcedUpdate(reason);
815
+ return this.pendingUpdate;
816
+ }
817
+ if (this.queuedForcedUpdate && !opts?.fromForcedQueue) {
818
+ if (force) return this.enqueueForcedUpdate(reason);
819
+ return this.queuedForcedUpdate;
820
+ }
821
+ if (this.shouldSkipUpdate(force)) return;
822
+ const run = async () => {
823
+ if (this.sessionExporter) await this.exportSessions();
824
+ await this.runQmdUpdateWithRetry(reason);
825
+ if (this.shouldRunEmbed(force)) try {
826
+ await runWithQmdEmbedLock(async () => {
827
+ await this.runQmd(["embed"], {
828
+ timeoutMs: this.qmd.update.embedTimeoutMs,
829
+ discardOutput: true
830
+ });
831
+ });
832
+ this.lastEmbedAt = Date.now();
833
+ this.embedBackoffUntil = null;
834
+ this.embedFailureCount = 0;
835
+ } catch (err) {
836
+ this.noteEmbedFailure(reason, err);
837
+ }
838
+ this.lastUpdateAt = Date.now();
839
+ this.docPathCache.clear();
840
+ };
841
+ this.pendingUpdate = run().finally(() => {
842
+ this.pendingUpdate = null;
843
+ });
844
+ await this.pendingUpdate;
845
+ }
846
+ async runQmdUpdateWithRetry(reason) {
847
+ const maxAttempts = reason === "boot" || reason.startsWith("boot:") ? 3 : 1;
848
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
849
+ await this.runQmdUpdateOnce(reason);
850
+ return;
851
+ } catch (err) {
852
+ if (attempt >= maxAttempts || !this.isRetryableUpdateError(err)) throw err;
853
+ const delayMs = 500 * 2 ** (attempt - 1);
854
+ log.warn(`qmd update retry ${attempt}/${maxAttempts - 1} after failure (${reason}): ${String(err)}`);
855
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
856
+ }
857
+ }
858
+ async runQmdUpdateOnce(reason) {
859
+ try {
860
+ await this.runQmd(["update"], {
861
+ timeoutMs: this.qmd.update.updateTimeoutMs,
862
+ discardOutput: true
863
+ });
864
+ } catch (err) {
865
+ if (!await this.tryRepairNullByteCollections(err, reason) && !await this.tryRepairDuplicateDocumentConstraint(err, reason)) throw err;
866
+ await this.runQmd(["update"], {
867
+ timeoutMs: this.qmd.update.updateTimeoutMs,
868
+ discardOutput: true
869
+ });
870
+ }
871
+ }
872
+ isRetryableUpdateError(err) {
873
+ if (this.isSqliteBusyError(err)) return true;
874
+ return (err instanceof Error ? err.message : String(err)).toLowerCase().includes("timed out");
875
+ }
876
+ shouldRunEmbed(force) {
877
+ if (this.qmd.searchMode === "search") return false;
878
+ const now = Date.now();
879
+ if (this.embedBackoffUntil !== null && now < this.embedBackoffUntil) return false;
880
+ const embedIntervalMs = this.qmd.update.embedIntervalMs;
881
+ return Boolean(force) || this.lastEmbedAt === null || embedIntervalMs > 0 && now - this.lastEmbedAt > embedIntervalMs;
882
+ }
883
+ noteEmbedFailure(reason, err) {
884
+ this.embedFailureCount += 1;
885
+ const delayMs = Math.min(QMD_EMBED_BACKOFF_MAX_MS, QMD_EMBED_BACKOFF_BASE_MS * 2 ** Math.max(0, this.embedFailureCount - 1));
886
+ this.embedBackoffUntil = Date.now() + delayMs;
887
+ log.warn(`qmd embed failed (${reason}): ${String(err)}; backing off for ${Math.ceil(delayMs / 1e3)}s`);
888
+ }
889
+ enqueueForcedUpdate(reason) {
890
+ this.queuedForcedRuns += 1;
891
+ if (!this.queuedForcedUpdate) this.queuedForcedUpdate = this.drainForcedUpdates(reason).finally(() => {
892
+ this.queuedForcedUpdate = null;
893
+ });
894
+ return this.queuedForcedUpdate;
895
+ }
896
+ async drainForcedUpdates(reason) {
897
+ await this.pendingUpdate?.catch(() => void 0);
898
+ while (!this.closed && this.queuedForcedRuns > 0) {
899
+ this.queuedForcedRuns -= 1;
900
+ await this.runUpdate(`${reason}:queued`, true, { fromForcedQueue: true });
901
+ }
902
+ }
903
+ /**
904
+ * Symlink the default QMD models directory into our custom XDG_CACHE_HOME so
905
+ * that the pre-installed ML models (~/.cache/qmd/models/) are reused rather
906
+ * than re-downloaded for every agent. If the default models directory does
907
+ * not exist, or a models directory/symlink already exists in the target, this
908
+ * is a no-op.
909
+ */
910
+ async symlinkSharedModels() {
911
+ const defaultCacheHome = process.env.XDG_CACHE_HOME || (process.platform === "win32" ? process.env.LOCALAPPDATA : void 0) || path.join(os.homedir(), ".cache");
912
+ const defaultModelsDir = path.join(defaultCacheHome, "qmd", "models");
913
+ const targetModelsDir = path.join(this.xdgCacheHome, "qmd", "models");
914
+ try {
915
+ if (!(await fs.stat(defaultModelsDir).catch((err) => {
916
+ if (err.code === "ENOENT") return null;
917
+ throw err;
918
+ }))?.isDirectory()) return;
919
+ try {
920
+ await fs.lstat(targetModelsDir);
921
+ return;
922
+ } catch {}
923
+ try {
924
+ await fs.symlink(defaultModelsDir, targetModelsDir, "dir");
925
+ } catch (symlinkErr) {
926
+ const code = symlinkErr.code;
927
+ if (process.platform === "win32" && (code === "EPERM" || code === "ENOTSUP")) await fs.symlink(defaultModelsDir, targetModelsDir, "junction");
928
+ else throw symlinkErr;
929
+ }
930
+ log.debug(`symlinked qmd models: ${defaultModelsDir} → ${targetModelsDir}`);
931
+ } catch (err) {
932
+ log.warn(`failed to symlink qmd models directory: ${String(err)}`);
933
+ }
934
+ }
935
+ async runQmd(args, opts) {
936
+ return await new Promise((resolve, reject) => {
937
+ const spawnInvocation = resolveSpawnInvocation({
938
+ command: this.qmd.command,
939
+ args,
940
+ env: this.env,
941
+ packageName: "qmd"
942
+ });
943
+ const child = spawn(spawnInvocation.command, spawnInvocation.argv, {
944
+ env: this.env,
945
+ cwd: this.workspaceDir,
946
+ shell: spawnInvocation.shell,
947
+ windowsHide: spawnInvocation.windowsHide
948
+ });
949
+ let stdout = "";
950
+ let stderr = "";
951
+ let stdoutTruncated = false;
952
+ let stderrTruncated = false;
953
+ const discard = opts?.discardOutput === true;
954
+ const timer = opts?.timeoutMs ? setTimeout(() => {
955
+ child.kill("SIGKILL");
956
+ reject(/* @__PURE__ */ new Error(`qmd ${args.join(" ")} timed out after ${opts.timeoutMs}ms`));
957
+ }, opts.timeoutMs) : null;
958
+ child.stdout.on("data", (data) => {
959
+ if (discard) return;
960
+ const next = appendOutputWithCap(stdout, data.toString("utf8"), this.maxQmdOutputChars);
961
+ stdout = next.text;
962
+ stdoutTruncated = stdoutTruncated || next.truncated;
963
+ });
964
+ child.stderr.on("data", (data) => {
965
+ const next = appendOutputWithCap(stderr, data.toString("utf8"), this.maxQmdOutputChars);
966
+ stderr = next.text;
967
+ stderrTruncated = stderrTruncated || next.truncated;
968
+ });
969
+ child.on("error", (err) => {
970
+ if (timer) clearTimeout(timer);
971
+ reject(err);
972
+ });
973
+ child.on("close", (code) => {
974
+ if (timer) clearTimeout(timer);
975
+ if (!discard && (stdoutTruncated || stderrTruncated)) {
976
+ reject(/* @__PURE__ */ new Error(`qmd ${args.join(" ")} produced too much output (limit ${this.maxQmdOutputChars} chars)`));
977
+ return;
978
+ }
979
+ if (code === 0) resolve({
980
+ stdout,
981
+ stderr
982
+ });
983
+ else reject(/* @__PURE__ */ new Error(`qmd ${args.join(" ")} failed (code ${code}): ${stderr || stdout}`));
984
+ });
985
+ });
986
+ }
987
+ async ensureMcporterDaemonStarted(mcporter) {
988
+ if (!mcporter.enabled) return;
989
+ if (!mcporter.startDaemon) {
990
+ const g = globalThis;
991
+ if (!g.__openclawMcporterColdStartWarned) {
992
+ g.__openclawMcporterColdStartWarned = true;
993
+ log.warn("mcporter qmd bridge enabled but startDaemon=false; each query may cold-start QMD MCP. Consider setting memory.qmd.mcporter.startDaemon=true to keep it warm.");
994
+ }
995
+ return;
996
+ }
997
+ const g = globalThis;
998
+ if (!g.__openclawMcporterDaemonStart) g.__openclawMcporterDaemonStart = (async () => {
999
+ try {
1000
+ await this.runMcporter(["daemon", "start"], { timeoutMs: 1e4 });
1001
+ } catch (err) {
1002
+ log.warn(`mcporter daemon start failed: ${String(err)}`);
1003
+ delete g.__openclawMcporterDaemonStart;
1004
+ }
1005
+ })();
1006
+ await g.__openclawMcporterDaemonStart;
1007
+ }
1008
+ async runMcporter(args, opts) {
1009
+ const runWithInvocation = async (spawnInvocation) => await new Promise((resolve, reject) => {
1010
+ const commandSummary = `${spawnInvocation.command} ${spawnInvocation.argv.join(" ")}`;
1011
+ const child = spawn(spawnInvocation.command, spawnInvocation.argv, {
1012
+ env: this.env,
1013
+ cwd: this.workspaceDir,
1014
+ shell: spawnInvocation.shell,
1015
+ windowsHide: spawnInvocation.windowsHide
1016
+ });
1017
+ let stdout = "";
1018
+ let stderr = "";
1019
+ let stdoutTruncated = false;
1020
+ let stderrTruncated = false;
1021
+ const timer = opts?.timeoutMs ? setTimeout(() => {
1022
+ child.kill("SIGKILL");
1023
+ reject(/* @__PURE__ */ new Error(`mcporter ${args.join(" ")} timed out after ${opts.timeoutMs}ms`));
1024
+ }, opts.timeoutMs) : null;
1025
+ child.stdout.on("data", (data) => {
1026
+ const next = appendOutputWithCap(stdout, data.toString("utf8"), this.maxQmdOutputChars);
1027
+ stdout = next.text;
1028
+ stdoutTruncated = stdoutTruncated || next.truncated;
1029
+ });
1030
+ child.stderr.on("data", (data) => {
1031
+ const next = appendOutputWithCap(stderr, data.toString("utf8"), this.maxQmdOutputChars);
1032
+ stderr = next.text;
1033
+ stderrTruncated = stderrTruncated || next.truncated;
1034
+ });
1035
+ child.on("error", (err) => {
1036
+ if (timer) clearTimeout(timer);
1037
+ reject(err);
1038
+ });
1039
+ child.on("close", (code) => {
1040
+ if (timer) clearTimeout(timer);
1041
+ if (stdoutTruncated || stderrTruncated) {
1042
+ reject(/* @__PURE__ */ new Error(`mcporter ${args.join(" ")} produced too much output (limit ${this.maxQmdOutputChars} chars)`));
1043
+ return;
1044
+ }
1045
+ if (code === 0) resolve({
1046
+ stdout,
1047
+ stderr
1048
+ });
1049
+ else reject(/* @__PURE__ */ new Error(`${commandSummary} failed (code ${code}): ${stderr || stdout}`));
1050
+ });
1051
+ });
1052
+ const primaryInvocation = resolveSpawnInvocation({
1053
+ command: "mcporter",
1054
+ args,
1055
+ env: this.env,
1056
+ packageName: "mcporter"
1057
+ });
1058
+ try {
1059
+ return await runWithInvocation(primaryInvocation);
1060
+ } catch (err) {
1061
+ if (!isWindowsCmdSpawnEinval(err, primaryInvocation.command)) throw err;
1062
+ log.warn("mcporter.cmd spawn returned EINVAL on Windows; retrying with bare mcporter");
1063
+ return await runWithInvocation({
1064
+ command: "mcporter",
1065
+ argv: args,
1066
+ shell: true,
1067
+ windowsHide: true
1068
+ });
1069
+ }
1070
+ }
1071
+ async runQmdSearchViaMcporter(params) {
1072
+ await this.ensureMcporterDaemonStarted(params.mcporter);
1073
+ const selector = `${params.mcporter.serverName}.${params.tool}`;
1074
+ const callArgs = {
1075
+ query: params.query,
1076
+ limit: params.limit,
1077
+ minScore: params.minScore
1078
+ };
1079
+ if (params.collection) callArgs.collection = params.collection;
1080
+ const result = await this.runMcporter([
1081
+ "call",
1082
+ selector,
1083
+ "--args",
1084
+ JSON.stringify(callArgs),
1085
+ "--output",
1086
+ "json",
1087
+ "--timeout",
1088
+ String(Math.max(0, params.timeoutMs))
1089
+ ], { timeoutMs: Math.max(params.timeoutMs + 2e3, 5e3) });
1090
+ const parsedUnknown = JSON.parse(result.stdout);
1091
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1092
+ const structured = isRecord(parsedUnknown) && isRecord(parsedUnknown.structuredContent) ? parsedUnknown.structuredContent : parsedUnknown;
1093
+ const results = isRecord(structured) && Array.isArray(structured.results) ? structured.results : Array.isArray(structured) ? structured : [];
1094
+ const out = [];
1095
+ for (const item of results) {
1096
+ if (!isRecord(item)) continue;
1097
+ const docidRaw = item.docid;
1098
+ const docid = typeof docidRaw === "string" ? docidRaw.replace(/^#/, "").trim() : "";
1099
+ if (!docid) continue;
1100
+ const scoreRaw = item.score;
1101
+ const score = typeof scoreRaw === "number" ? scoreRaw : Number(scoreRaw);
1102
+ const snippet = typeof item.snippet === "string" ? item.snippet : "";
1103
+ out.push({
1104
+ docid,
1105
+ score: Number.isFinite(score) ? score : 0,
1106
+ snippet
1107
+ });
1108
+ }
1109
+ return out;
1110
+ }
1111
+ async readPartialText(absPath, from, lines) {
1112
+ const start = Math.max(1, from ?? 1);
1113
+ const count = Math.max(1, lines ?? Number.POSITIVE_INFINITY);
1114
+ let handle;
1115
+ try {
1116
+ handle = await fs.open(absPath);
1117
+ } catch (err) {
1118
+ if (isFileMissingError(err)) return { missing: true };
1119
+ throw err;
1120
+ }
1121
+ const stream = handle.createReadStream({ encoding: "utf-8" });
1122
+ const rl = readline.createInterface({
1123
+ input: stream,
1124
+ crlfDelay: Infinity
1125
+ });
1126
+ const selected = [];
1127
+ let index = 0;
1128
+ try {
1129
+ for await (const line of rl) {
1130
+ index += 1;
1131
+ if (index < start) continue;
1132
+ if (selected.length >= count) break;
1133
+ selected.push(line);
1134
+ }
1135
+ } finally {
1136
+ rl.close();
1137
+ await handle.close();
1138
+ }
1139
+ return {
1140
+ missing: false,
1141
+ text: selected.slice(0, count).join("\n")
1142
+ };
1143
+ }
1144
+ async readFullText(absPath) {
1145
+ try {
1146
+ return {
1147
+ missing: false,
1148
+ text: await fs.readFile(absPath, "utf-8")
1149
+ };
1150
+ } catch (err) {
1151
+ if (isFileMissingError(err)) return { missing: true };
1152
+ throw err;
1153
+ }
1154
+ }
1155
+ ensureDb() {
1156
+ if (this.db) return this.db;
1157
+ const { DatabaseSync } = requireNodeSqlite();
1158
+ this.db = new DatabaseSync(this.indexPath, { readOnly: true });
1159
+ this.db.exec("PRAGMA busy_timeout = 1");
1160
+ return this.db;
1161
+ }
1162
+ async exportSessions() {
1163
+ if (!this.sessionExporter) return;
1164
+ const exportDir = this.sessionExporter.dir;
1165
+ await fs.mkdir(exportDir, { recursive: true });
1166
+ const files = await listSessionFilesForAgent(this.agentId);
1167
+ const keep = /* @__PURE__ */ new Set();
1168
+ const tracked = /* @__PURE__ */ new Set();
1169
+ const cutoff = this.sessionExporter.retentionMs ? Date.now() - this.sessionExporter.retentionMs : null;
1170
+ for (const sessionFile of files) {
1171
+ const entry = await buildSessionEntry(sessionFile);
1172
+ if (!entry) continue;
1173
+ if (cutoff && entry.mtimeMs < cutoff) continue;
1174
+ const targetName = `${path.basename(sessionFile, ".jsonl")}.md`;
1175
+ const target = path.join(exportDir, targetName);
1176
+ tracked.add(sessionFile);
1177
+ const state = this.exportedSessionState.get(sessionFile);
1178
+ if (!state || state.hash !== entry.hash || state.mtimeMs !== entry.mtimeMs) await writeFileWithinRoot({
1179
+ rootDir: exportDir,
1180
+ relativePath: targetName,
1181
+ data: this.renderSessionMarkdown(entry),
1182
+ encoding: "utf-8"
1183
+ });
1184
+ this.exportedSessionState.set(sessionFile, {
1185
+ hash: entry.hash,
1186
+ mtimeMs: entry.mtimeMs,
1187
+ target
1188
+ });
1189
+ keep.add(target);
1190
+ }
1191
+ const exported = await fs.readdir(exportDir).catch(() => []);
1192
+ for (const name of exported) {
1193
+ if (!name.endsWith(".md")) continue;
1194
+ const full = path.join(exportDir, name);
1195
+ if (!keep.has(full)) await fs.rm(full, { force: true });
1196
+ }
1197
+ for (const [sessionFile, state] of this.exportedSessionState) if (!tracked.has(sessionFile) || !state.target.startsWith(exportDir + path.sep)) this.exportedSessionState.delete(sessionFile);
1198
+ }
1199
+ renderSessionMarkdown(entry) {
1200
+ return `${`# Session ${path.basename(entry.absPath, path.extname(entry.absPath))}`}\n\n${entry.content?.trim().length ? entry.content.trim() : "(empty)"}\n`;
1201
+ }
1202
+ pickSessionCollectionName() {
1203
+ const existing = new Set(this.qmd.collections.map((collection) => collection.name));
1204
+ const base = `sessions-${this.sanitizeCollectionNameSegment(this.agentId)}`;
1205
+ if (!existing.has(base)) return base;
1206
+ let counter = 2;
1207
+ let candidate = `${base}-${counter}`;
1208
+ while (existing.has(candidate)) {
1209
+ counter += 1;
1210
+ candidate = `${base}-${counter}`;
1211
+ }
1212
+ return candidate;
1213
+ }
1214
+ sanitizeCollectionNameSegment(input) {
1215
+ return input.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
1216
+ }
1217
+ async resolveDocLocation(docid, hints) {
1218
+ const normalizedHints = this.normalizeDocHints(hints);
1219
+ if (!docid) return this.resolveDocLocationFromHints(normalizedHints);
1220
+ const normalized = docid.startsWith("#") ? docid.slice(1) : docid;
1221
+ if (!normalized) return null;
1222
+ const cacheKey = `${normalizedHints.preferredCollection ?? "*"}:${normalized}`;
1223
+ const cached = this.docPathCache.get(cacheKey);
1224
+ if (cached) return cached;
1225
+ const db = this.ensureDb();
1226
+ let rows = [];
1227
+ try {
1228
+ rows = db.prepare("SELECT collection, path FROM documents WHERE hash = ? AND active = 1").all(normalized);
1229
+ if (rows.length === 0) rows = db.prepare("SELECT collection, path FROM documents WHERE hash LIKE ? AND active = 1").all(`${normalized}%`);
1230
+ } catch (err) {
1231
+ if (this.isSqliteBusyError(err)) {
1232
+ log.debug(`qmd index is busy while resolving doc path: ${String(err)}`);
1233
+ throw this.createQmdBusyError(err);
1234
+ }
1235
+ throw err;
1236
+ }
1237
+ if (rows.length === 0) return null;
1238
+ const location = this.pickDocLocation(rows, normalizedHints);
1239
+ if (!location) return null;
1240
+ this.docPathCache.set(cacheKey, location);
1241
+ return location;
1242
+ }
1243
+ resolveDocLocationFromHints(hints) {
1244
+ if (!hints.preferredCollection || !hints.preferredFile) return null;
1245
+ const collectionRelativePath = this.toCollectionRelativePath(hints.preferredCollection, hints.preferredFile);
1246
+ if (!collectionRelativePath) return null;
1247
+ return this.toDocLocation(hints.preferredCollection, collectionRelativePath);
1248
+ }
1249
+ normalizeDocHints(hints) {
1250
+ const preferredCollection = hints?.preferredCollection?.trim();
1251
+ const preferredFile = hints?.preferredFile?.trim();
1252
+ if (!preferredFile) return preferredCollection ? { preferredCollection } : {};
1253
+ const parsedQmdFile = this.parseQmdFileUri(preferredFile);
1254
+ return {
1255
+ preferredCollection: parsedQmdFile?.collection ?? preferredCollection,
1256
+ preferredFile: parsedQmdFile?.collectionRelativePath ?? preferredFile
1257
+ };
1258
+ }
1259
+ parseQmdFileUri(fileRef) {
1260
+ if (!fileRef.toLowerCase().startsWith("qmd://")) return null;
1261
+ try {
1262
+ const parsed = new URL(fileRef);
1263
+ const collection = decodeURIComponent(parsed.hostname).trim();
1264
+ const pathname = decodeURIComponent(parsed.pathname).replace(/^\/+/, "").trim();
1265
+ if (!collection && !pathname) return null;
1266
+ return {
1267
+ collection: collection || void 0,
1268
+ collectionRelativePath: pathname || void 0
1269
+ };
1270
+ } catch {
1271
+ return null;
1272
+ }
1273
+ }
1274
+ toCollectionRelativePath(collection, filePath) {
1275
+ const root = this.collectionRoots.get(collection);
1276
+ if (!root) return null;
1277
+ const trimmedFilePath = filePath.trim();
1278
+ if (!trimmedFilePath) return null;
1279
+ const normalizedInput = path.normalize(trimmedFilePath);
1280
+ const absolutePath = path.isAbsolute(normalizedInput) ? normalizedInput : path.resolve(root.path, normalizedInput);
1281
+ if (!this.isWithinRoot(root.path, absolutePath)) return null;
1282
+ const relative = path.relative(root.path, absolutePath);
1283
+ if (!relative || relative === ".") return null;
1284
+ return relative.replace(/\\/g, "/");
1285
+ }
1286
+ pickDocLocation(rows, hints) {
1287
+ if (hints?.preferredCollection) for (const row of rows) {
1288
+ if (row.collection !== hints.preferredCollection) continue;
1289
+ const location = this.toDocLocation(row.collection, row.path);
1290
+ if (location) return location;
1291
+ }
1292
+ if (hints?.preferredFile) {
1293
+ const preferred = path.normalize(hints.preferredFile);
1294
+ for (const row of rows) {
1295
+ const rowPath = path.normalize(row.path);
1296
+ if (rowPath !== preferred && !rowPath.endsWith(path.sep + preferred)) continue;
1297
+ const location = this.toDocLocation(row.collection, row.path);
1298
+ if (location) return location;
1299
+ }
1300
+ }
1301
+ for (const row of rows) {
1302
+ const location = this.toDocLocation(row.collection, row.path);
1303
+ if (location) return location;
1304
+ }
1305
+ return null;
1306
+ }
1307
+ extractSnippetLines(snippet) {
1308
+ const match = SNIPPET_HEADER_RE.exec(snippet);
1309
+ if (match) {
1310
+ const start = Number(match[1]);
1311
+ const count = Number(match[2]);
1312
+ if (Number.isFinite(start) && Number.isFinite(count)) return {
1313
+ startLine: start,
1314
+ endLine: start + count - 1
1315
+ };
1316
+ }
1317
+ return {
1318
+ startLine: 1,
1319
+ endLine: snippet.split("\n").length
1320
+ };
1321
+ }
1322
+ readCounts() {
1323
+ try {
1324
+ const rows = this.ensureDb().prepare("SELECT collection, COUNT(*) as c FROM documents WHERE active = 1 GROUP BY collection").all();
1325
+ const bySource = /* @__PURE__ */ new Map();
1326
+ for (const source of this.sources) bySource.set(source, {
1327
+ files: 0,
1328
+ chunks: 0
1329
+ });
1330
+ let total = 0;
1331
+ for (const row of rows) {
1332
+ const source = this.collectionRoots.get(row.collection)?.kind ?? "memory";
1333
+ const entry = bySource.get(source) ?? {
1334
+ files: 0,
1335
+ chunks: 0
1336
+ };
1337
+ entry.files += row.c ?? 0;
1338
+ entry.chunks += row.c ?? 0;
1339
+ bySource.set(source, entry);
1340
+ total += row.c ?? 0;
1341
+ }
1342
+ return {
1343
+ totalDocuments: total,
1344
+ sourceCounts: Array.from(bySource.entries()).map(([source, value]) => ({
1345
+ source,
1346
+ files: value.files,
1347
+ chunks: value.chunks
1348
+ }))
1349
+ };
1350
+ } catch (err) {
1351
+ log.warn(`failed to read qmd index stats: ${String(err)}`);
1352
+ return {
1353
+ totalDocuments: 0,
1354
+ sourceCounts: Array.from(this.sources).map((source) => ({
1355
+ source,
1356
+ files: 0,
1357
+ chunks: 0
1358
+ }))
1359
+ };
1360
+ }
1361
+ }
1362
+ logScopeDenied(sessionKey) {
1363
+ const channel = deriveQmdScopeChannel(sessionKey) ?? "unknown";
1364
+ const chatType = deriveQmdScopeChatType(sessionKey) ?? "unknown";
1365
+ const key = sessionKey?.trim() || "<none>";
1366
+ log.warn(`qmd search denied by scope (channel=${channel}, chatType=${chatType}, session=${key})`);
1367
+ }
1368
+ isScopeAllowed(sessionKey) {
1369
+ return isQmdScopeAllowed(this.qmd.scope, sessionKey);
1370
+ }
1371
+ toDocLocation(collection, collectionRelativePath) {
1372
+ const root = this.collectionRoots.get(collection);
1373
+ if (!root) return null;
1374
+ const normalizedRelative = collectionRelativePath.replace(/\\/g, "/");
1375
+ const absPath = path.normalize(path.resolve(root.path, collectionRelativePath));
1376
+ const relativeToWorkspace = path.relative(this.workspaceDir, absPath);
1377
+ return {
1378
+ rel: this.buildSearchPath(collection, normalizedRelative, relativeToWorkspace, absPath),
1379
+ abs: absPath,
1380
+ source: root.kind
1381
+ };
1382
+ }
1383
+ buildSearchPath(collection, collectionRelativePath, relativeToWorkspace, absPath) {
1384
+ if (this.isInsideWorkspace(relativeToWorkspace)) {
1385
+ const normalized = relativeToWorkspace.replace(/\\/g, "/");
1386
+ if (!normalized) return path.basename(absPath);
1387
+ return normalized;
1388
+ }
1389
+ return `qmd/${collection}/${collectionRelativePath.replace(/^\/+/, "")}`;
1390
+ }
1391
+ isInsideWorkspace(relativePath) {
1392
+ if (!relativePath) return true;
1393
+ if (relativePath.startsWith("..")) return false;
1394
+ if (relativePath.startsWith(`..${path.sep}`)) return false;
1395
+ return !path.isAbsolute(relativePath);
1396
+ }
1397
+ resolveReadPath(relPath) {
1398
+ if (relPath.startsWith("qmd/")) {
1399
+ const [, collection, ...rest] = relPath.split("/");
1400
+ if (!collection || rest.length === 0) throw new Error("invalid qmd path");
1401
+ const root = this.collectionRoots.get(collection);
1402
+ if (!root) throw new Error(`unknown qmd collection: ${collection}`);
1403
+ const joined = rest.join("/");
1404
+ const resolved = path.resolve(root.path, joined);
1405
+ if (!this.isWithinRoot(root.path, resolved)) throw new Error("qmd path escapes collection");
1406
+ return resolved;
1407
+ }
1408
+ const absPath = path.resolve(this.workspaceDir, relPath);
1409
+ if (!this.isWithinWorkspace(absPath)) throw new Error("path escapes workspace");
1410
+ return absPath;
1411
+ }
1412
+ isWithinWorkspace(absPath) {
1413
+ const normalizedWorkspace = this.workspaceDir.endsWith(path.sep) ? this.workspaceDir : `${this.workspaceDir}${path.sep}`;
1414
+ if (absPath === this.workspaceDir) return true;
1415
+ return (absPath.endsWith(path.sep) ? absPath : `${absPath}${path.sep}`).startsWith(normalizedWorkspace);
1416
+ }
1417
+ isWithinRoot(root, candidate) {
1418
+ const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
1419
+ if (candidate === root) return true;
1420
+ return (candidate.endsWith(path.sep) ? candidate : `${candidate}${path.sep}`).startsWith(normalizedRoot);
1421
+ }
1422
+ clampResultsByInjectedChars(results) {
1423
+ const budget = this.qmd.limits.maxInjectedChars;
1424
+ if (!budget || budget <= 0) return results;
1425
+ let remaining = budget;
1426
+ const clamped = [];
1427
+ for (const entry of results) {
1428
+ if (remaining <= 0) break;
1429
+ const snippet = entry.snippet ?? "";
1430
+ if (snippet.length <= remaining) {
1431
+ clamped.push(entry);
1432
+ remaining -= snippet.length;
1433
+ } else {
1434
+ const trimmed = snippet.slice(0, Math.max(0, remaining));
1435
+ clamped.push({
1436
+ ...entry,
1437
+ snippet: trimmed
1438
+ });
1439
+ break;
1440
+ }
1441
+ }
1442
+ return clamped;
1443
+ }
1444
+ diversifyResultsBySource(results, limit) {
1445
+ const target = Math.max(0, limit);
1446
+ if (target <= 0) return [];
1447
+ if (results.length <= 1) return results.slice(0, target);
1448
+ const bySource = /* @__PURE__ */ new Map();
1449
+ for (const entry of results) {
1450
+ const list = bySource.get(entry.source) ?? [];
1451
+ list.push(entry);
1452
+ bySource.set(entry.source, list);
1453
+ }
1454
+ const hasSessions = bySource.has("sessions");
1455
+ const hasMemory = bySource.has("memory");
1456
+ if (!hasSessions || !hasMemory) return results.slice(0, target);
1457
+ const sourceOrder = Array.from(bySource.entries()).toSorted((a, b) => (b[1][0]?.score ?? 0) - (a[1][0]?.score ?? 0)).map(([source]) => source);
1458
+ const diversified = [];
1459
+ while (diversified.length < target) {
1460
+ let emitted = false;
1461
+ for (const source of sourceOrder) {
1462
+ const next = bySource.get(source)?.shift();
1463
+ if (!next) continue;
1464
+ diversified.push(next);
1465
+ emitted = true;
1466
+ if (diversified.length >= target) break;
1467
+ }
1468
+ if (!emitted) break;
1469
+ }
1470
+ return diversified;
1471
+ }
1472
+ shouldSkipUpdate(force) {
1473
+ if (force) return false;
1474
+ const debounceMs = this.qmd.update.debounceMs;
1475
+ if (debounceMs <= 0) return false;
1476
+ if (!this.lastUpdateAt) return false;
1477
+ return Date.now() - this.lastUpdateAt < debounceMs;
1478
+ }
1479
+ isSqliteBusyError(err) {
1480
+ const normalized = (err instanceof Error ? err.message : String(err)).toLowerCase();
1481
+ return normalized.includes("sqlite_busy") || normalized.includes("database is locked");
1482
+ }
1483
+ isUnsupportedQmdOptionError(err) {
1484
+ const normalized = (err instanceof Error ? err.message : String(err)).toLowerCase();
1485
+ return normalized.includes("unknown flag") || normalized.includes("unknown option") || normalized.includes("unrecognized option") || normalized.includes("flag provided but not defined") || normalized.includes("unexpected argument");
1486
+ }
1487
+ createQmdBusyError(err) {
1488
+ const message = err instanceof Error ? err.message : String(err);
1489
+ return /* @__PURE__ */ new Error(`qmd index busy while reading results: ${message}`);
1490
+ }
1491
+ async waitForPendingUpdateBeforeSearch() {
1492
+ const pending = this.pendingUpdate;
1493
+ if (!pending) return;
1494
+ await Promise.race([pending.catch(() => void 0), new Promise((resolve) => setTimeout(resolve, SEARCH_PENDING_UPDATE_WAIT_MS))]);
1495
+ }
1496
+ async runQueryAcrossCollections(query, limit, collectionNames, command) {
1497
+ log.debug(`qmd ${command} multi-collection workaround active (${collectionNames.length} collections)`);
1498
+ const bestByResultKey = /* @__PURE__ */ new Map();
1499
+ for (const collectionName of collectionNames) {
1500
+ const args = this.buildSearchArgs(command, query, limit);
1501
+ args.push("-c", collectionName);
1502
+ const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
1503
+ const parsed = parseQmdQueryJson(result.stdout, result.stderr);
1504
+ for (const entry of parsed) {
1505
+ const normalizedHints = this.normalizeDocHints({
1506
+ preferredCollection: entry.collection ?? collectionName,
1507
+ preferredFile: entry.file
1508
+ });
1509
+ const normalizedDocId = typeof entry.docid === "string" && entry.docid.trim().length > 0 ? entry.docid : void 0;
1510
+ const withCollection = {
1511
+ ...entry,
1512
+ docid: normalizedDocId,
1513
+ collection: normalizedHints.preferredCollection ?? entry.collection ?? collectionName,
1514
+ file: normalizedHints.preferredFile ?? entry.file
1515
+ };
1516
+ const resultKey = this.buildQmdResultKey(withCollection);
1517
+ if (!resultKey) continue;
1518
+ const prev = bestByResultKey.get(resultKey);
1519
+ const prevScore = typeof prev?.score === "number" ? prev.score : Number.NEGATIVE_INFINITY;
1520
+ const nextScore = typeof withCollection.score === "number" ? withCollection.score : Number.NEGATIVE_INFINITY;
1521
+ if (!prev || nextScore > prevScore) bestByResultKey.set(resultKey, withCollection);
1522
+ }
1523
+ }
1524
+ return [...bestByResultKey.values()].toSorted((a, b) => (b.score ?? 0) - (a.score ?? 0));
1525
+ }
1526
+ buildQmdResultKey(entry) {
1527
+ if (typeof entry.docid === "string" && entry.docid.trim().length > 0) return `docid:${entry.docid}`;
1528
+ const hints = this.normalizeDocHints({
1529
+ preferredCollection: entry.collection,
1530
+ preferredFile: entry.file
1531
+ });
1532
+ if (!hints.preferredCollection || !hints.preferredFile) return null;
1533
+ const collectionRelativePath = this.toCollectionRelativePath(hints.preferredCollection, hints.preferredFile);
1534
+ if (!collectionRelativePath) return null;
1535
+ return `file:${hints.preferredCollection}:${collectionRelativePath}`;
1536
+ }
1537
+ async runMcporterAcrossCollections(params) {
1538
+ const bestByDocId = /* @__PURE__ */ new Map();
1539
+ for (const collectionName of params.collectionNames) {
1540
+ const parsed = await this.runQmdSearchViaMcporter({
1541
+ mcporter: this.qmd.mcporter,
1542
+ tool: params.tool,
1543
+ query: params.query,
1544
+ limit: params.limit,
1545
+ minScore: params.minScore,
1546
+ collection: collectionName,
1547
+ timeoutMs: this.qmd.limits.timeoutMs
1548
+ });
1549
+ for (const entry of parsed) {
1550
+ if (typeof entry.docid !== "string" || !entry.docid.trim()) continue;
1551
+ const prev = bestByDocId.get(entry.docid);
1552
+ const prevScore = typeof prev?.score === "number" ? prev.score : Number.NEGATIVE_INFINITY;
1553
+ const nextScore = typeof entry.score === "number" ? entry.score : Number.NEGATIVE_INFINITY;
1554
+ if (!prev || nextScore > prevScore) bestByDocId.set(entry.docid, entry);
1555
+ }
1556
+ }
1557
+ return [...bestByDocId.values()].toSorted((a, b) => (b.score ?? 0) - (a.score ?? 0));
1558
+ }
1559
+ listManagedCollectionNames() {
1560
+ return this.managedCollectionNames;
1561
+ }
1562
+ computeManagedCollectionNames() {
1563
+ const seen = /* @__PURE__ */ new Set();
1564
+ const names = [];
1565
+ for (const collection of this.qmd.collections) {
1566
+ const name = collection.name?.trim();
1567
+ if (!name || seen.has(name)) continue;
1568
+ seen.add(name);
1569
+ names.push(name);
1570
+ }
1571
+ return names;
1572
+ }
1573
+ buildCollectionFilterArgs(collectionNames) {
1574
+ if (collectionNames.length === 0) return [];
1575
+ return collectionNames.filter(Boolean).flatMap((name) => ["-c", name]);
1576
+ }
1577
+ buildSearchArgs(command, query, limit) {
1578
+ const normalizedQuery = command === "search" ? normalizeHanBm25Query(query) : query;
1579
+ if (command === "query") return [
1580
+ "query",
1581
+ normalizedQuery,
1582
+ "--json",
1583
+ "-n",
1584
+ String(limit)
1585
+ ];
1586
+ return [
1587
+ command,
1588
+ normalizedQuery,
1589
+ "--json",
1590
+ "-n",
1591
+ String(limit)
1592
+ ];
1593
+ }
1594
+ };
1595
+ function appendOutputWithCap(current, chunk, maxChars) {
1596
+ const appended = current + chunk;
1597
+ if (appended.length <= maxChars) return {
1598
+ text: appended,
1599
+ truncated: false
1600
+ };
1601
+ return {
1602
+ text: appended.slice(-maxChars),
1603
+ truncated: true
1604
+ };
1605
+ }
1606
+
1607
+ //#endregion
1608
+ export { QmdMemoryManager };