agim-cli 1.1.10 → 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.
- package/CHANGELOG.md +155 -0
- package/dist/cli.js +78 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/approval-bus.d.ts +18 -0
- package/dist/core/approval-bus.d.ts.map +1 -1
- package/dist/core/approval-bus.js +111 -0
- package/dist/core/approval-bus.js.map +1 -1
- package/dist/core/approval-router.d.ts.map +1 -1
- package/dist/core/approval-router.js +12 -0
- package/dist/core/approval-router.js.map +1 -1
- package/dist/core/audit-log.d.ts +39 -0
- package/dist/core/audit-log.d.ts.map +1 -1
- package/dist/core/audit-log.js +124 -0
- package/dist/core/audit-log.js.map +1 -1
- package/dist/core/boot-state.d.ts +17 -0
- package/dist/core/boot-state.d.ts.map +1 -0
- package/dist/core/boot-state.js +77 -0
- package/dist/core/boot-state.js.map +1 -0
- package/dist/core/job-recovery.d.ts +41 -1
- package/dist/core/job-recovery.d.ts.map +1 -1
- package/dist/core/job-recovery.js +216 -4
- package/dist/core/job-recovery.js.map +1 -1
- package/dist/core/memory-consolidate.d.ts +12 -0
- package/dist/core/memory-consolidate.d.ts.map +1 -0
- package/dist/core/memory-consolidate.js +242 -0
- package/dist/core/memory-consolidate.js.map +1 -0
- package/dist/core/memory-distill.d.ts +30 -0
- package/dist/core/memory-distill.d.ts.map +1 -0
- package/dist/core/memory-distill.js +213 -0
- package/dist/core/memory-distill.js.map +1 -0
- package/dist/core/memory-rpc.d.ts +11 -0
- package/dist/core/memory-rpc.d.ts.map +1 -0
- package/dist/core/memory-rpc.js +94 -0
- package/dist/core/memory-rpc.js.map +1 -0
- package/dist/core/memory-vector.d.ts +44 -0
- package/dist/core/memory-vector.d.ts.map +1 -0
- package/dist/core/memory-vector.js +360 -0
- package/dist/core/memory-vector.js.map +1 -0
- package/dist/core/memory.d.ts +140 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +714 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/persona.d.ts +24 -0
- package/dist/core/persona.d.ts.map +1 -0
- package/dist/core/persona.js +80 -0
- package/dist/core/persona.js.map +1 -0
- package/dist/core/push-rpc.d.ts +26 -0
- package/dist/core/push-rpc.d.ts.map +1 -0
- package/dist/core/push-rpc.js +123 -0
- package/dist/core/push-rpc.js.map +1 -0
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +26 -1
- package/dist/core/router.js.map +1 -1
- package/dist/core/types.d.ts +41 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/index.d.ts +9 -0
- package/dist/plugins/agents/claude-code/index.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/index.js +37 -0
- package/dist/plugins/agents/claude-code/index.js.map +1 -1
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +8 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/mcp-approval-server.js +181 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -1
- package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +5 -1
- package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -1
- package/dist/plugins/messengers/telegram/telegram-adapter.js +85 -0
- package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -1
- package/dist/web/public/settings.html +106 -10
- package/dist/web/public/tasks.html +977 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +433 -6
- package/dist/web/server.js.map +1 -1
- package/dist/web/viewer-render.d.ts.map +1 -1
- package/dist/web/viewer-render.js +7 -4
- package/dist/web/viewer-render.js.map +1 -1
- package/package.json +4 -1
package/dist/web/server.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/web/server.js
CHANGED
|
@@ -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 {
|
|
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
|
|
1262
|
-
|
|
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');
|