agim-cli 1.1.11 → 1.2.0

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 (73) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/dist/cli.js +78 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/approval-bus.d.ts +18 -0
  5. package/dist/core/approval-bus.d.ts.map +1 -1
  6. package/dist/core/approval-bus.js +111 -0
  7. package/dist/core/approval-bus.js.map +1 -1
  8. package/dist/core/approval-router.d.ts.map +1 -1
  9. package/dist/core/approval-router.js +12 -0
  10. package/dist/core/approval-router.js.map +1 -1
  11. package/dist/core/audit-log.d.ts +39 -0
  12. package/dist/core/audit-log.d.ts.map +1 -1
  13. package/dist/core/audit-log.js +124 -0
  14. package/dist/core/audit-log.js.map +1 -1
  15. package/dist/core/boot-state.d.ts +17 -0
  16. package/dist/core/boot-state.d.ts.map +1 -0
  17. package/dist/core/boot-state.js +77 -0
  18. package/dist/core/boot-state.js.map +1 -0
  19. package/dist/core/job-recovery.d.ts +41 -1
  20. package/dist/core/job-recovery.d.ts.map +1 -1
  21. package/dist/core/job-recovery.js +216 -4
  22. package/dist/core/job-recovery.js.map +1 -1
  23. package/dist/core/memory-consolidate.d.ts +12 -0
  24. package/dist/core/memory-consolidate.d.ts.map +1 -0
  25. package/dist/core/memory-consolidate.js +242 -0
  26. package/dist/core/memory-consolidate.js.map +1 -0
  27. package/dist/core/memory-distill.d.ts +30 -0
  28. package/dist/core/memory-distill.d.ts.map +1 -0
  29. package/dist/core/memory-distill.js +213 -0
  30. package/dist/core/memory-distill.js.map +1 -0
  31. package/dist/core/memory-rpc.d.ts +11 -0
  32. package/dist/core/memory-rpc.d.ts.map +1 -0
  33. package/dist/core/memory-rpc.js +94 -0
  34. package/dist/core/memory-rpc.js.map +1 -0
  35. package/dist/core/memory-vector.d.ts +44 -0
  36. package/dist/core/memory-vector.d.ts.map +1 -0
  37. package/dist/core/memory-vector.js +360 -0
  38. package/dist/core/memory-vector.js.map +1 -0
  39. package/dist/core/memory.d.ts +140 -0
  40. package/dist/core/memory.d.ts.map +1 -0
  41. package/dist/core/memory.js +714 -0
  42. package/dist/core/memory.js.map +1 -0
  43. package/dist/core/persona.d.ts +24 -0
  44. package/dist/core/persona.d.ts.map +1 -0
  45. package/dist/core/persona.js +80 -0
  46. package/dist/core/persona.js.map +1 -0
  47. package/dist/core/push-rpc.d.ts +26 -0
  48. package/dist/core/push-rpc.d.ts.map +1 -0
  49. package/dist/core/push-rpc.js +123 -0
  50. package/dist/core/push-rpc.js.map +1 -0
  51. package/dist/core/router.d.ts.map +1 -1
  52. package/dist/core/router.js +26 -1
  53. package/dist/core/router.js.map +1 -1
  54. package/dist/core/types.d.ts +41 -0
  55. package/dist/core/types.d.ts.map +1 -1
  56. package/dist/plugins/agents/claude-code/index.d.ts +9 -0
  57. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -1
  58. package/dist/plugins/agents/claude-code/index.js +37 -0
  59. package/dist/plugins/agents/claude-code/index.js.map +1 -1
  60. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +8 -0
  61. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -1
  62. package/dist/plugins/agents/claude-code/mcp-approval-server.js +181 -0
  63. package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -1
  64. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +5 -1
  65. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -1
  66. package/dist/plugins/messengers/telegram/telegram-adapter.js +85 -0
  67. package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -1
  68. package/dist/web/public/settings.html +106 -10
  69. package/dist/web/public/tasks.html +977 -1
  70. package/dist/web/server.d.ts.map +1 -1
  71. package/dist/web/server.js +433 -6
  72. package/dist/web/server.js.map +1 -1
  73. package/package.json +4 -1
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AA+JA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA4tB/C"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AA+JA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAgxB/C"}
@@ -563,6 +563,58 @@ export async function startWebServer(options) {
563
563
  if (url.pathname === '/api/agent-health' && req.method === 'GET') {
564
564
  return handleAgentHealth(req, res);
565
565
  }
566
+ // v1.3 — Cost & Health: aggregated metrics from the audit-log used by
567
+ // the new Cost & Health tab. summary returns daily totals + KPI cards;
568
+ // topn returns ranked groups by user / agent / platform / intent.
569
+ if (url.pathname === '/api/health/summary' && req.method === 'GET') {
570
+ return handleHealthSummary(req, res, url);
571
+ }
572
+ if (url.pathname === '/api/health/topn' && req.method === 'GET') {
573
+ return handleHealthTopN(req, res, url);
574
+ }
575
+ // v1.5 — Memory admin: enumerate users, list / delete facts, view /
576
+ // edit / delete persona, export. Backs the Memory tab in /tasks.
577
+ if (url.pathname === '/api/memory/users' && req.method === 'GET') {
578
+ return handleMemoryUsers(req, res);
579
+ }
580
+ if (url.pathname === '/api/memory/facts' && req.method === 'GET') {
581
+ return handleMemoryFacts(req, res, url);
582
+ }
583
+ if (url.pathname === '/api/memory/facts' && req.method === 'DELETE') {
584
+ return handleMemoryBulkDelete(req, res, url);
585
+ }
586
+ const memFactIdMatch = url.pathname.match(/^\/api\/memory\/facts\/(\d+)$/);
587
+ if (memFactIdMatch && req.method === 'DELETE') {
588
+ return handleMemoryDeleteOne(req, res, url, parseInt(memFactIdMatch[1], 10));
589
+ }
590
+ if (url.pathname === '/api/memory/persona' && req.method === 'GET') {
591
+ return handleMemoryPersona(req, res, url);
592
+ }
593
+ if (url.pathname === '/api/memory/persona' && req.method === 'PUT') {
594
+ return handleMemoryPersonaPut(req, res, url);
595
+ }
596
+ if (url.pathname === '/api/memory/persona' && req.method === 'DELETE') {
597
+ return handleMemoryPersonaDelete(req, res, url);
598
+ }
599
+ if (url.pathname === '/api/memory/export' && req.method === 'GET') {
600
+ return handleMemoryExport(req, res, url);
601
+ }
602
+ // v1.6 — vector backend control + index ops.
603
+ if (url.pathname === '/api/memory/vector/status' && req.method === 'GET') {
604
+ return handleVectorStatus(req, res, url);
605
+ }
606
+ if (url.pathname === '/api/memory/vector/test' && req.method === 'POST') {
607
+ return handleVectorTest(req, res);
608
+ }
609
+ if (url.pathname === '/api/memory/vector/download' && req.method === 'POST') {
610
+ return handleVectorDownload(req, res);
611
+ }
612
+ if (url.pathname === '/api/memory/vector/backfill' && req.method === 'POST') {
613
+ return handleVectorBackfill(req, res, url);
614
+ }
615
+ if (url.pathname === '/api/memory/vector/clear' && req.method === 'POST') {
616
+ return handleVectorClear(req, res, url);
617
+ }
566
618
  // PR-B: HITL approvals — global pending list + per-reqId resolve.
567
619
  if (url.pathname === '/api/approvals' && req.method === 'GET') {
568
620
  return handleListApprovals(req, res);
@@ -1191,14 +1243,21 @@ async function handleWechatQrStatus(res, url) {
1191
1243
  async function handleServiceStatus(res) {
1192
1244
  try {
1193
1245
  const { detectService, formatUptime, readWebEndpoint } = await import('../cli-ui/service.js');
1246
+ const { getBootInfo } = await import('../core/boot-state.js');
1194
1247
  const st = detectService();
1195
1248
  const web = readWebEndpoint();
1249
+ const boot = getBootInfo();
1196
1250
  sendJson(res, 200, {
1197
1251
  mode: st.mode,
1198
1252
  pid: st.pid,
1199
1253
  uptimeSec: st.uptimeSec ?? null,
1200
1254
  uptime: typeof st.uptimeSec === 'number' ? formatUptime(st.uptimeSec) : null,
1201
1255
  web,
1256
+ // v1.5 — fine-grained boot phase so the web restart-polling UI can
1257
+ // wait for full readiness instead of merely "HTTP server is up".
1258
+ bootPhase: boot.phase,
1259
+ bootMsSinceStart: boot.msSinceStart,
1260
+ bootPhaseTimings: boot.phaseTimings,
1202
1261
  });
1203
1262
  }
1204
1263
  catch (err) {
@@ -1256,13 +1315,21 @@ async function handleServiceRestart(res) {
1256
1315
  const st = detectService();
1257
1316
  if (st.mode === 'systemd') {
1258
1317
  try {
1259
- const { execSync } = await import('node:child_process');
1318
+ const { spawn } = await import('node:child_process');
1260
1319
  const unitName = existsSync('/etc/systemd/system/agim.service') ? 'agim.service' : 'im-hub.service';
1261
- execSync(`systemctl restart ${unitName}`);
1262
- sendJson(res, 200, { ok: true, mode: 'systemd' });
1320
+ // v1.5 — fire-and-forget. execSync blocked the HTTP response until
1321
+ // systemctl had finished stop+start (5-10s), making the web button
1322
+ // look frozen to the user. Detach the child so we can ACK the
1323
+ // request immediately and let systemd do its thing.
1324
+ const child = spawn('systemctl', ['restart', unitName], {
1325
+ detached: true,
1326
+ stdio: 'ignore',
1327
+ });
1328
+ child.unref();
1329
+ sendJson(res, 200, { ok: true, mode: 'systemd', restarting: true });
1263
1330
  }
1264
1331
  catch (err) {
1265
- sendJson(res, 500, { error: 'systemctl restart failed: ' + (err instanceof Error ? err.message : String(err)) });
1332
+ sendJson(res, 500, { error: 'systemctl restart spawn failed: ' + (err instanceof Error ? err.message : String(err)) });
1266
1333
  }
1267
1334
  return;
1268
1335
  }
@@ -1786,8 +1853,18 @@ const ENV_EDITABLE_KEYS = [
1786
1853
  'IMHUB_A2A_NOTIFY_MODE',
1787
1854
  'IMHUB_A2A_NOTIFY_MAX_DEPTH',
1788
1855
  'IMHUB_A2A_HEARTBEAT_MIN',
1856
+ // v1.5 — long-term memory (see src/core/memory.ts + persona.ts).
1857
+ 'IMHUB_MEMORY_ENABLED',
1858
+ // v1.6 — vector retrieval (opt-in, OFF by default).
1859
+ 'IMHUB_MEMORY_VECTOR_BACKEND',
1860
+ 'IMHUB_MEMORY_VECTOR_LOCAL_MODEL',
1861
+ 'IMHUB_MEMORY_VECTOR_OPENAI_BASE_URL',
1862
+ 'IMHUB_MEMORY_VECTOR_OPENAI_MODEL',
1863
+ 'IMHUB_MEMORY_VECTOR_OPENAI_API_KEY',
1864
+ 'IMHUB_MEMORY_VECTOR_BATCH_SIZE',
1865
+ 'IMHUB_MEMORY_VECTOR_HYBRID_WEIGHT',
1789
1866
  ];
1790
- const SECRET_KEYS = new Set(['IMHUB_SMTP_PASS', 'IMHUB_BAIDU_MAP_AK']);
1867
+ const SECRET_KEYS = new Set(['IMHUB_SMTP_PASS', 'IMHUB_BAIDU_MAP_AK', 'IMHUB_MEMORY_VECTOR_OPENAI_API_KEY']);
1791
1868
  function maskSecret(v) {
1792
1869
  if (!v)
1793
1870
  return '';
@@ -1827,8 +1904,14 @@ async function handlePutEnv(req, res) {
1827
1904
  for (const [k, v] of Object.entries(updates)) {
1828
1905
  if (!ENV_EDITABLE_KEYS.includes(k))
1829
1906
  continue;
1830
- if (v === null || typeof v === 'string')
1907
+ if (v === null || typeof v === 'string') {
1908
+ // Defense in depth: if the client echoes a masked secret value
1909
+ // (e.g. "abcd****wxyz") back into PUT, treat it as "no change" and
1910
+ // skip the update so the real value isn't overwritten by the mask.
1911
+ if (SECRET_KEYS.has(k) && typeof v === 'string' && isMasked(v))
1912
+ continue;
1831
1913
  safe[k] = v;
1914
+ }
1832
1915
  }
1833
1916
  if (Object.keys(safe).length === 0) {
1834
1917
  sendJson(res, 400, { error: 'no editable keys in updates' });
@@ -1954,6 +2037,350 @@ async function handleAudit(_req, res, url) {
1954
2037
  * No persistence — pure read of in-memory state. Cheap to call (<1 ms for
1955
2038
  * a typical agent fleet) so the page is happy to poll on a 5 s tick.
1956
2039
  */
2040
+ async function handleHealthSummary(_req, res, url) {
2041
+ try {
2042
+ const days = Math.min(Math.max(parseInt(url.searchParams.get('days') || '7', 10) || 7, 1), 365);
2043
+ const { getHealthSummary } = await import('../core/audit-log.js');
2044
+ sendJson(res, 200, getHealthSummary(days));
2045
+ }
2046
+ catch (err) {
2047
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2048
+ }
2049
+ }
2050
+ async function handleHealthTopN(_req, res, url) {
2051
+ try {
2052
+ const dimRaw = (url.searchParams.get('dim') || 'user').toLowerCase();
2053
+ const metricRaw = (url.searchParams.get('by') || 'cost').toLowerCase();
2054
+ const days = Math.min(Math.max(parseInt(url.searchParams.get('days') || '7', 10) || 7, 1), 365);
2055
+ const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') || '10', 10) || 10, 1), 100);
2056
+ const validDims = new Set(['user', 'agent', 'platform', 'intent']);
2057
+ const validMetrics = new Set(['cost', 'calls', 'errors', 'avg_latency']);
2058
+ if (!validDims.has(dimRaw)) {
2059
+ sendJson(res, 400, { error: `dim must be one of ${[...validDims].join(',')}` });
2060
+ return;
2061
+ }
2062
+ if (!validMetrics.has(metricRaw)) {
2063
+ sendJson(res, 400, { error: `by must be one of ${[...validMetrics].join(',')}` });
2064
+ return;
2065
+ }
2066
+ const { getTopN } = await import('../core/audit-log.js');
2067
+ const items = getTopN(dimRaw, metricRaw, days, limit);
2068
+ sendJson(res, 200, { dim: dimRaw, by: metricRaw, days, items });
2069
+ }
2070
+ catch (err) {
2071
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2072
+ }
2073
+ }
2074
+ // ─── v1.5 Memory admin handlers ──────────────────────────────────────
2075
+ function readUserKey(url) {
2076
+ const k = url.searchParams.get('user_key');
2077
+ if (!k || !k.includes(':'))
2078
+ return null;
2079
+ return k;
2080
+ }
2081
+ async function handleMemoryUsers(_req, res) {
2082
+ try {
2083
+ const { listUsers } = await import('../core/memory.js');
2084
+ sendJson(res, 200, { users: listUsers() });
2085
+ }
2086
+ catch (err) {
2087
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2088
+ }
2089
+ }
2090
+ async function handleMemoryFacts(_req, res, url) {
2091
+ try {
2092
+ const user_key = readUserKey(url);
2093
+ if (!user_key) {
2094
+ sendJson(res, 400, { error: 'user_key required (platform:userId)' });
2095
+ return;
2096
+ }
2097
+ const { listFacts } = await import('../core/memory.js');
2098
+ const query = url.searchParams.get('query') || '';
2099
+ const category = url.searchParams.get('category') || '';
2100
+ const limit = parseInt(url.searchParams.get('limit') || '50', 10) || 50;
2101
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10) || 0;
2102
+ const validCats = new Set(['fact', 'preference', 'goal', 'history', 'profile']);
2103
+ const cat = validCats.has(category) ? category : undefined;
2104
+ sendJson(res, 200, listFacts({ user_key, query, category: cat, limit, offset }));
2105
+ }
2106
+ catch (err) {
2107
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2108
+ }
2109
+ }
2110
+ async function handleMemoryDeleteOne(req, res, url, id) {
2111
+ try {
2112
+ const user_key = readUserKey(url);
2113
+ if (!user_key) {
2114
+ sendJson(res, 400, { error: 'user_key required' });
2115
+ return;
2116
+ }
2117
+ const { deleteFact } = await import('../core/memory.js');
2118
+ const ok = deleteFact(id, user_key);
2119
+ sendJson(res, ok ? 200 : 404, { ok, id });
2120
+ }
2121
+ catch (err) {
2122
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2123
+ }
2124
+ void req;
2125
+ }
2126
+ async function handleMemoryBulkDelete(req, res, url) {
2127
+ try {
2128
+ const user_key = readUserKey(url);
2129
+ if (!user_key) {
2130
+ sendJson(res, 400, { error: 'user_key required' });
2131
+ return;
2132
+ }
2133
+ const body = await readBody(req, res);
2134
+ const parsed = body ? JSON.parse(body) : {};
2135
+ const { bulkDeleteFacts } = await import('../core/memory.js');
2136
+ const idsRaw = parsed.ids;
2137
+ const ids = Array.isArray(idsRaw) ? idsRaw.filter((n) => typeof n === 'number') : undefined;
2138
+ const category = typeof parsed.category === 'string' ? parsed.category : undefined;
2139
+ const max_confidence = typeof parsed.max_confidence === 'number' ? parsed.max_confidence : undefined;
2140
+ const confirm_clear = parsed.confirm_clear === true;
2141
+ const validCats = new Set(['fact', 'preference', 'goal', 'history', 'profile']);
2142
+ const deleted = bulkDeleteFacts({
2143
+ user_key,
2144
+ ids: ids && ids.length > 0 ? ids : undefined,
2145
+ category: category && validCats.has(category) ? category : undefined,
2146
+ max_confidence,
2147
+ confirm_clear,
2148
+ });
2149
+ sendJson(res, 200, { deleted });
2150
+ }
2151
+ catch (err) {
2152
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2153
+ }
2154
+ }
2155
+ async function handleMemoryPersona(_req, res, url) {
2156
+ try {
2157
+ const user_key = readUserKey(url);
2158
+ if (!user_key) {
2159
+ sendJson(res, 400, { error: 'user_key required' });
2160
+ return;
2161
+ }
2162
+ const { getPersona } = await import('../core/memory.js');
2163
+ const p = getPersona(user_key);
2164
+ if (!p) {
2165
+ sendJson(res, 404, { error: 'no persona for user_key', user_key });
2166
+ return;
2167
+ }
2168
+ sendJson(res, 200, p);
2169
+ }
2170
+ catch (err) {
2171
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2172
+ }
2173
+ }
2174
+ async function handleMemoryPersonaPut(req, res, url) {
2175
+ try {
2176
+ const user_key = readUserKey(url);
2177
+ if (!user_key) {
2178
+ sendJson(res, 400, { error: 'user_key required' });
2179
+ return;
2180
+ }
2181
+ const body = await readBody(req, res);
2182
+ const parsed = body ? JSON.parse(body) : {};
2183
+ const summary = typeof parsed.summary === 'string' ? parsed.summary.trim() : '';
2184
+ if (!summary) {
2185
+ sendJson(res, 400, { error: 'summary required (non-empty string)' });
2186
+ return;
2187
+ }
2188
+ const { upsertPersona } = await import('../core/memory.js');
2189
+ const ok = upsertPersona(user_key, summary);
2190
+ sendJson(res, ok ? 200 : 500, { ok });
2191
+ }
2192
+ catch (err) {
2193
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2194
+ }
2195
+ }
2196
+ async function handleMemoryPersonaDelete(_req, res, url) {
2197
+ try {
2198
+ const user_key = readUserKey(url);
2199
+ if (!user_key) {
2200
+ sendJson(res, 400, { error: 'user_key required' });
2201
+ return;
2202
+ }
2203
+ const { deletePersona } = await import('../core/memory.js');
2204
+ const ok = deletePersona(user_key);
2205
+ sendJson(res, ok ? 200 : 404, { ok });
2206
+ }
2207
+ catch (err) {
2208
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2209
+ }
2210
+ }
2211
+ async function handleMemoryExport(_req, res, url) {
2212
+ try {
2213
+ const user_key = readUserKey(url);
2214
+ if (!user_key) {
2215
+ sendJson(res, 400, { error: 'user_key required' });
2216
+ return;
2217
+ }
2218
+ const { exportUserMemory } = await import('../core/memory.js');
2219
+ const data = exportUserMemory(user_key);
2220
+ res.writeHead(200, {
2221
+ 'Content-Type': 'application/json; charset=utf-8',
2222
+ 'Content-Disposition': `attachment; filename="agim-memory-${user_key.replace(/[^a-zA-Z0-9_-]+/g, '_')}.json"`,
2223
+ });
2224
+ res.end(JSON.stringify(data, null, 2));
2225
+ }
2226
+ catch (err) {
2227
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2228
+ }
2229
+ }
2230
+ // ─── v1.6 vector backend handlers ────────────────────────────────────
2231
+ async function handleVectorStatus(_req, res, url) {
2232
+ try {
2233
+ const { getActiveBackend } = await import('../core/memory-vector.js');
2234
+ const { getVectorCoverage } = await import('../core/memory.js');
2235
+ const backend = getActiveBackend();
2236
+ const userKeyParam = url.searchParams.get('user_key') || undefined;
2237
+ const coverage = getVectorCoverage(userKeyParam);
2238
+ sendJson(res, 200, {
2239
+ status: backend.describe(),
2240
+ coverage,
2241
+ // Backfill / download jobs share an in-memory map for progress polling.
2242
+ jobs: pickPublicJobs(),
2243
+ });
2244
+ }
2245
+ catch (err) {
2246
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2247
+ }
2248
+ }
2249
+ async function handleVectorTest(_req, res) {
2250
+ try {
2251
+ const { testActiveBackend } = await import('../core/memory-vector.js');
2252
+ const r = await testActiveBackend();
2253
+ sendJson(res, r.ok ? 200 : 400, r);
2254
+ }
2255
+ catch (err) {
2256
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2257
+ }
2258
+ }
2259
+ const vectorJobs = new Map();
2260
+ function pickPublicJobs() {
2261
+ const all = Array.from(vectorJobs.values()).sort((a, b) => b.startedAt - a.startedAt);
2262
+ return all.slice(0, 5);
2263
+ }
2264
+ function newJob(kind) {
2265
+ const id = `${kind}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
2266
+ const j = {
2267
+ id, kind, startedAt: Date.now(), finishedAt: null,
2268
+ phase: 'running', message: 'started', details: {},
2269
+ };
2270
+ vectorJobs.set(id, j);
2271
+ // P2 cleanup: bound the map size — keep up to 100 entries, oldest first.
2272
+ if (vectorJobs.size > 100) {
2273
+ const oldest = Array.from(vectorJobs.values())
2274
+ .sort((a, b) => a.startedAt - b.startedAt)[0];
2275
+ if (oldest)
2276
+ vectorJobs.delete(oldest.id);
2277
+ }
2278
+ return j;
2279
+ }
2280
+ function findRunningJob(kind) {
2281
+ for (const j of vectorJobs.values()) {
2282
+ if (j.kind === kind && j.phase === 'running')
2283
+ return j;
2284
+ }
2285
+ return undefined;
2286
+ }
2287
+ function finishJob(id, ok, message, details = {}) {
2288
+ const j = vectorJobs.get(id);
2289
+ if (!j)
2290
+ return;
2291
+ j.finishedAt = Date.now();
2292
+ j.phase = ok ? 'done' : 'failed';
2293
+ j.message = message;
2294
+ j.details = details;
2295
+ }
2296
+ async function handleVectorDownload(_req, res) {
2297
+ try {
2298
+ const { triggerLocalDownload, getActiveBackend } = await import('../core/memory-vector.js');
2299
+ const b = getActiveBackend();
2300
+ if (b.name !== 'local') {
2301
+ sendJson(res, 400, { error: `backend is "${b.name}", switch to "local" first` });
2302
+ return;
2303
+ }
2304
+ if (b.ready) {
2305
+ sendJson(res, 200, { ok: true, alreadyReady: true });
2306
+ return;
2307
+ }
2308
+ // P1-10: dedupe concurrent download attempts. Two browser tabs (or a
2309
+ // double-click) would otherwise spawn parallel triggerLocalDownload
2310
+ // calls that race on the same model cache directory.
2311
+ const inflight = findRunningJob('download');
2312
+ if (inflight) {
2313
+ sendJson(res, 202, { ok: true, jobId: inflight.id, alreadyRunning: true });
2314
+ return;
2315
+ }
2316
+ const job = newJob('download');
2317
+ // Kick off async; respond immediately so the UI doesn't block.
2318
+ void (async () => {
2319
+ try {
2320
+ const r = await triggerLocalDownload();
2321
+ if (r.ok)
2322
+ finishJob(job.id, true, 'model ready', { modelId: b.modelId, dims: b.dims });
2323
+ else
2324
+ finishJob(job.id, false, r.error || 'unknown error', {});
2325
+ }
2326
+ catch (err) {
2327
+ finishJob(job.id, false, err instanceof Error ? err.message : String(err), {});
2328
+ }
2329
+ })();
2330
+ sendJson(res, 202, { ok: true, jobId: job.id, hint: 'poll /api/memory/vector/status for progress' });
2331
+ }
2332
+ catch (err) {
2333
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2334
+ }
2335
+ }
2336
+ async function handleVectorBackfill(req, res, url) {
2337
+ try {
2338
+ const { backfillEmbeddings } = await import('../core/memory.js');
2339
+ const { getActiveBackend } = await import('../core/memory-vector.js');
2340
+ const b = getActiveBackend();
2341
+ if (!b.ready) {
2342
+ sendJson(res, 400, { error: `vector backend not ready (${b.name})` });
2343
+ return;
2344
+ }
2345
+ const user_key = url.searchParams.get('user_key') || undefined;
2346
+ const body = await readBody(req, res).catch(() => '');
2347
+ const parsed = body ? JSON.parse(body) : {};
2348
+ const maxRows = typeof parsed.max_rows === 'number' ? parsed.max_rows : undefined;
2349
+ // P1-10: dedupe concurrent backfill — backfill is idempotent at the
2350
+ // UPDATE layer, but running two passes wastes inference budget +
2351
+ // makes the progress display jump around.
2352
+ const inflight = findRunningJob('backfill');
2353
+ if (inflight) {
2354
+ sendJson(res, 202, { ok: true, jobId: inflight.id, alreadyRunning: true });
2355
+ return;
2356
+ }
2357
+ const job = newJob('backfill');
2358
+ void (async () => {
2359
+ try {
2360
+ const r = await backfillEmbeddings({ user_key, maxRows });
2361
+ finishJob(job.id, !r.errored, r.errored || `${r.succeeded}/${r.processed} ok (failed ${r.failed})`, r);
2362
+ }
2363
+ catch (err) {
2364
+ finishJob(job.id, false, err instanceof Error ? err.message : String(err), {});
2365
+ }
2366
+ })();
2367
+ sendJson(res, 202, { ok: true, jobId: job.id, hint: 'poll /api/memory/vector/status for progress' });
2368
+ }
2369
+ catch (err) {
2370
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2371
+ }
2372
+ }
2373
+ async function handleVectorClear(_req, res, url) {
2374
+ try {
2375
+ const { clearEmbeddings } = await import('../core/memory.js');
2376
+ const user_key = url.searchParams.get('user_key') || undefined;
2377
+ const cleared = clearEmbeddings(user_key);
2378
+ sendJson(res, 200, { ok: true, cleared });
2379
+ }
2380
+ catch (err) {
2381
+ sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
2382
+ }
2383
+ }
1957
2384
  async function handleAgentHealth(_req, res) {
1958
2385
  try {
1959
2386
  const { circuitBreaker } = await import('../core/circuit-breaker.js');