@algosuite/vo-mcp 0.2.0-beta.29 → 0.2.0-beta.30
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/dist/autostart-cli.js +62 -46
- package/dist/autostart-cli.js.map +2 -2
- package/dist/cli.js +887 -223
- package/dist/cli.js.map +4 -4
- package/dist/index.js +826 -192
- package/dist/index.js.map +4 -4
- package/dist/install-cli.js +56 -42
- package/dist/install-cli.js.map +3 -3
- package/dist/runner-cli.js +568 -308
- package/dist/runner-cli.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -318,13 +318,191 @@ var init_safe_memory_file = __esm({
|
|
|
318
318
|
}
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
+
// src/tools/memory/bounded-sync.ts
|
|
322
|
+
function createSyncDeadline(budgetMs = SYNC_DEADLINE_MS, now = Date.now) {
|
|
323
|
+
const startedAt = now();
|
|
324
|
+
return {
|
|
325
|
+
check() {
|
|
326
|
+
const elapsed = now() - startedAt;
|
|
327
|
+
if (elapsed > budgetMs) throw new SyncDeadlineExceededError(elapsed, budgetMs);
|
|
328
|
+
},
|
|
329
|
+
remainingMs() {
|
|
330
|
+
return Math.max(0, budgetMs - (now() - startedAt));
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
async function withRequestTimeout(url, run, budgetMs = REQUEST_TIMEOUT_MS) {
|
|
335
|
+
let timer;
|
|
336
|
+
try {
|
|
337
|
+
return await Promise.race([
|
|
338
|
+
run(),
|
|
339
|
+
new Promise((_resolve, reject) => {
|
|
340
|
+
timer = setTimeout(() => reject(new RequestTimeoutError(url, budgetMs)), budgetMs);
|
|
341
|
+
timer.unref?.();
|
|
342
|
+
})
|
|
343
|
+
]);
|
|
344
|
+
} finally {
|
|
345
|
+
if (timer) clearTimeout(timer);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
349
|
+
const results = new Array(items.length);
|
|
350
|
+
const width = Math.max(1, Math.min(limit, items.length));
|
|
351
|
+
let next = 0;
|
|
352
|
+
async function worker() {
|
|
353
|
+
for (; ; ) {
|
|
354
|
+
const index = next++;
|
|
355
|
+
if (index >= items.length) return;
|
|
356
|
+
try {
|
|
357
|
+
results[index] = { ok: true, value: await fn(items[index], index) };
|
|
358
|
+
} catch (error) {
|
|
359
|
+
results[index] = { ok: false, error };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
await Promise.all(Array.from({ length: width }, () => worker()));
|
|
364
|
+
return results;
|
|
365
|
+
}
|
|
366
|
+
var REQUEST_TIMEOUT_MS, SYNC_DEADLINE_MS, PUSH_CONCURRENCY, SyncDeadlineExceededError, RequestTimeoutError;
|
|
367
|
+
var init_bounded_sync = __esm({
|
|
368
|
+
"src/tools/memory/bounded-sync.ts"() {
|
|
369
|
+
"use strict";
|
|
370
|
+
REQUEST_TIMEOUT_MS = 15e3;
|
|
371
|
+
SYNC_DEADLINE_MS = 12e4;
|
|
372
|
+
PUSH_CONCURRENCY = 6;
|
|
373
|
+
SyncDeadlineExceededError = class extends Error {
|
|
374
|
+
constructor(elapsedMs, budgetMs) {
|
|
375
|
+
super(
|
|
376
|
+
`memory sync exceeded its ${budgetMs}ms deadline after ${elapsedMs}ms \u2014 aborting so the lock is released instead of held indefinitely`
|
|
377
|
+
);
|
|
378
|
+
this.name = "SyncDeadlineExceededError";
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
RequestTimeoutError = class extends Error {
|
|
382
|
+
constructor(url, budgetMs) {
|
|
383
|
+
super(`memory sync request to ${url} exceeded ${budgetMs}ms`);
|
|
384
|
+
this.name = "RequestTimeoutError";
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// src/tools/memory/memory-push-cache.ts
|
|
391
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
392
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
393
|
+
import { join as join9 } from "node:path";
|
|
394
|
+
function sha256(content) {
|
|
395
|
+
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
396
|
+
}
|
|
397
|
+
function statePath(memoryDir) {
|
|
398
|
+
return join9(memoryDir, MEMORY_SYNC_STATE_FILE);
|
|
399
|
+
}
|
|
400
|
+
function readPushCache(memoryDir, controlPlaneUrl) {
|
|
401
|
+
const empty = { controlPlaneUrl, entries: /* @__PURE__ */ new Map(), knowledgeSweptAtMs: null };
|
|
402
|
+
let raw;
|
|
403
|
+
try {
|
|
404
|
+
raw = readFileSync10(statePath(memoryDir), "utf8");
|
|
405
|
+
} catch {
|
|
406
|
+
return empty;
|
|
407
|
+
}
|
|
408
|
+
let parsed;
|
|
409
|
+
try {
|
|
410
|
+
parsed = JSON.parse(raw);
|
|
411
|
+
} catch {
|
|
412
|
+
return empty;
|
|
413
|
+
}
|
|
414
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return empty;
|
|
415
|
+
const obj = parsed;
|
|
416
|
+
if (obj["version"] !== STATE_VERSION) return empty;
|
|
417
|
+
if (obj["controlPlaneUrl"] !== controlPlaneUrl) return empty;
|
|
418
|
+
const files = obj["entries"];
|
|
419
|
+
if (typeof files !== "object" || files === null || Array.isArray(files)) return empty;
|
|
420
|
+
const entries = /* @__PURE__ */ new Map();
|
|
421
|
+
for (const [name, value] of Object.entries(files)) {
|
|
422
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
423
|
+
const row = value;
|
|
424
|
+
const memoryHash = typeof row["memoryHash"] === "string" ? row["memoryHash"] : void 0;
|
|
425
|
+
const knowledgeHash = typeof row["knowledgeHash"] === "string" ? row["knowledgeHash"] : void 0;
|
|
426
|
+
if (memoryHash === void 0 && knowledgeHash === void 0) continue;
|
|
427
|
+
entries.set(name, {
|
|
428
|
+
...memoryHash !== void 0 ? { memoryHash } : {},
|
|
429
|
+
...knowledgeHash !== void 0 ? { knowledgeHash } : {}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const sweptAt = obj["knowledgeSweptAtMs"];
|
|
433
|
+
return {
|
|
434
|
+
controlPlaneUrl,
|
|
435
|
+
entries,
|
|
436
|
+
// An unreadable/absent sweep stamp reads as NEVER SWEPT, which forces a full
|
|
437
|
+
// sweep — the fail-closed direction (more upserts, never fewer).
|
|
438
|
+
knowledgeSweptAtMs: typeof sweptAt === "number" && Number.isFinite(sweptAt) ? sweptAt : null
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function writePushCache(memoryDir, cache) {
|
|
442
|
+
const entries = {};
|
|
443
|
+
for (const [name, value] of cache.entries) entries[name] = value;
|
|
444
|
+
try {
|
|
445
|
+
writeFileSync5(
|
|
446
|
+
statePath(memoryDir),
|
|
447
|
+
`${JSON.stringify(
|
|
448
|
+
{
|
|
449
|
+
version: STATE_VERSION,
|
|
450
|
+
controlPlaneUrl: cache.controlPlaneUrl,
|
|
451
|
+
knowledgeSweptAtMs: cache.knowledgeSweptAtMs,
|
|
452
|
+
entries
|
|
453
|
+
},
|
|
454
|
+
null,
|
|
455
|
+
2
|
|
456
|
+
)}
|
|
457
|
+
`,
|
|
458
|
+
"utf8"
|
|
459
|
+
);
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function recordMemoryPush(cache, fileName, payloadHash) {
|
|
464
|
+
cache.entries.set(fileName, { ...cache.entries.get(fileName), memoryHash: payloadHash });
|
|
465
|
+
}
|
|
466
|
+
function recordKnowledgePush(cache, fileName, contentHash) {
|
|
467
|
+
cache.entries.set(fileName, { ...cache.entries.get(fileName), knowledgeHash: contentHash });
|
|
468
|
+
}
|
|
469
|
+
function pruneMissing(cache, presentFileNames) {
|
|
470
|
+
const present = new Set(presentFileNames);
|
|
471
|
+
for (const name of [...cache.entries.keys()]) {
|
|
472
|
+
if (!present.has(name)) cache.entries.delete(name);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function needsMemoryPush(cache, fileName, payloadHash, serverHasEntry) {
|
|
476
|
+
if (!serverHasEntry) return true;
|
|
477
|
+
return cache.entries.get(fileName)?.memoryHash !== payloadHash;
|
|
478
|
+
}
|
|
479
|
+
function knowledgeSweepDue(cache, nowMs = Date.now()) {
|
|
480
|
+
const swept = cache.knowledgeSweptAtMs;
|
|
481
|
+
if (swept === null || !Number.isFinite(swept)) return true;
|
|
482
|
+
const age = nowMs - swept;
|
|
483
|
+
return !(age >= 0 && age < KNOWLEDGE_FULL_SWEEP_MS);
|
|
484
|
+
}
|
|
485
|
+
function needsKnowledgePush(cache, fileName, contentHash, sweepDue = false) {
|
|
486
|
+
if (sweepDue) return true;
|
|
487
|
+
return cache.entries.get(fileName)?.knowledgeHash !== contentHash;
|
|
488
|
+
}
|
|
489
|
+
var MEMORY_SYNC_STATE_FILE, STATE_VERSION, KNOWLEDGE_FULL_SWEEP_MS;
|
|
490
|
+
var init_memory_push_cache = __esm({
|
|
491
|
+
"src/tools/memory/memory-push-cache.ts"() {
|
|
492
|
+
"use strict";
|
|
493
|
+
MEMORY_SYNC_STATE_FILE = ".memory-sync-state.json";
|
|
494
|
+
STATE_VERSION = 1;
|
|
495
|
+
KNOWLEDGE_FULL_SWEEP_MS = 24 * 60 * 6e4;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
321
499
|
// src/tools/memory/memory-knowledge-bridge.ts
|
|
322
500
|
var memory_knowledge_bridge_exports = {};
|
|
323
501
|
__export(memory_knowledge_bridge_exports, {
|
|
324
502
|
extractMemoryTitle: () => extractMemoryTitle,
|
|
325
503
|
upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
|
|
326
504
|
});
|
|
327
|
-
import { existsSync as
|
|
505
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
|
|
328
506
|
function extractMemoryTitle(fileName, content) {
|
|
329
507
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
330
508
|
if (frontmatter) {
|
|
@@ -336,13 +514,13 @@ function extractMemoryTitle(fileName, content) {
|
|
|
336
514
|
return fileName;
|
|
337
515
|
}
|
|
338
516
|
async function upsertMemoryFilesAsKnowledge(options) {
|
|
339
|
-
const { controlPlaneUrl, token, memoryDir, fetchFn } = options;
|
|
517
|
+
const { controlPlaneUrl, token, memoryDir, fetchFn, cache, deadline } = options;
|
|
340
518
|
let files;
|
|
341
519
|
try {
|
|
342
|
-
if (!
|
|
343
|
-
return { attempted: 0, upserted: 0, failed: 0, failures: [] };
|
|
520
|
+
if (!existsSync7(memoryDir)) {
|
|
521
|
+
return { attempted: 0, upserted: 0, failed: 0, skipped: 0, failures: [] };
|
|
344
522
|
}
|
|
345
|
-
files =
|
|
523
|
+
files = readdirSync5(memoryDir).filter(
|
|
346
524
|
(f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
|
|
347
525
|
);
|
|
348
526
|
} catch (err) {
|
|
@@ -350,54 +528,81 @@ async function upsertMemoryFilesAsKnowledge(options) {
|
|
|
350
528
|
attempted: 0,
|
|
351
529
|
upserted: 0,
|
|
352
530
|
failed: 1,
|
|
531
|
+
skipped: 0,
|
|
353
532
|
failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
|
|
354
533
|
};
|
|
355
534
|
}
|
|
356
|
-
|
|
535
|
+
const sweepDue = cache ? knowledgeSweepDue(cache) : true;
|
|
536
|
+
const candidates = [];
|
|
357
537
|
const failures = [];
|
|
538
|
+
let skipped = 0;
|
|
358
539
|
for (const fileName of files) {
|
|
359
540
|
try {
|
|
360
|
-
const content =
|
|
541
|
+
const content = readFileSync13(resolveMemoryFilePath(memoryDir, fileName), "utf8");
|
|
361
542
|
if (content.length > CONTENT_HARD_LIMIT) {
|
|
362
543
|
failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
|
|
363
544
|
continue;
|
|
364
545
|
}
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
title,
|
|
370
|
-
content
|
|
371
|
-
};
|
|
372
|
-
const post = (body) => fetchFn(`${controlPlaneUrl}/api/v1/knowledge/private`, {
|
|
373
|
-
method: "POST",
|
|
374
|
-
headers: {
|
|
375
|
-
authorization: `Bearer ${token}`,
|
|
376
|
-
"content-type": "application/json"
|
|
377
|
-
},
|
|
378
|
-
body: JSON.stringify(body)
|
|
379
|
-
});
|
|
380
|
-
let response = await post({
|
|
381
|
-
...base,
|
|
382
|
-
provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
|
|
383
|
-
});
|
|
384
|
-
if (response.status === 400) {
|
|
385
|
-
response = await post(base);
|
|
386
|
-
}
|
|
387
|
-
if (response.status >= 200 && response.status < 300) {
|
|
388
|
-
upserted += 1;
|
|
389
|
-
} else {
|
|
390
|
-
const text = await response.text();
|
|
391
|
-
failures.push(`${fileName}: HTTP ${response.status} ${text.slice(0, 80)}`);
|
|
546
|
+
const hash = sha256(content);
|
|
547
|
+
if (cache && !needsKnowledgePush(cache, fileName, hash, sweepDue)) {
|
|
548
|
+
skipped += 1;
|
|
549
|
+
continue;
|
|
392
550
|
}
|
|
551
|
+
candidates.push({ fileName, content, hash });
|
|
393
552
|
} catch (err) {
|
|
394
553
|
failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
395
554
|
}
|
|
396
555
|
}
|
|
556
|
+
const url = `${controlPlaneUrl}/api/v1/knowledge/private`;
|
|
557
|
+
const outcomes = await mapWithConcurrency(candidates, options.concurrency ?? PUSH_CONCURRENCY, async (candidate) => {
|
|
558
|
+
deadline?.check();
|
|
559
|
+
const base = {
|
|
560
|
+
knowledge_class: "memory",
|
|
561
|
+
source_path: `memory/${candidate.fileName}`,
|
|
562
|
+
title: extractMemoryTitle(candidate.fileName, candidate.content),
|
|
563
|
+
content: candidate.content
|
|
564
|
+
};
|
|
565
|
+
const post = (body) => withRequestTimeout(url, () => fetchFn(url, {
|
|
566
|
+
method: "POST",
|
|
567
|
+
headers: {
|
|
568
|
+
authorization: `Bearer ${token}`,
|
|
569
|
+
"content-type": "application/json"
|
|
570
|
+
},
|
|
571
|
+
body: JSON.stringify(body)
|
|
572
|
+
}));
|
|
573
|
+
let response = await post({
|
|
574
|
+
...base,
|
|
575
|
+
provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
|
|
576
|
+
});
|
|
577
|
+
if (response.status === 400) {
|
|
578
|
+
response = await post(base);
|
|
579
|
+
}
|
|
580
|
+
if (response.status >= 200 && response.status < 300) return true;
|
|
581
|
+
const text = await response.text();
|
|
582
|
+
throw new Error(`HTTP ${response.status} ${text.slice(0, 80)}`);
|
|
583
|
+
});
|
|
584
|
+
let upserted = 0;
|
|
585
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
586
|
+
const outcome = outcomes[i];
|
|
587
|
+
const candidate = candidates[i];
|
|
588
|
+
if (outcome.ok) {
|
|
589
|
+
upserted += 1;
|
|
590
|
+
if (cache) recordKnowledgePush(cache, candidate.fileName, candidate.hash);
|
|
591
|
+
} else {
|
|
592
|
+
const error = outcome.error;
|
|
593
|
+
failures.push(`${candidate.fileName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (cache && sweepDue && failures.length === 0) {
|
|
597
|
+
cache.knowledgeSweptAtMs = Date.now();
|
|
598
|
+
}
|
|
397
599
|
return {
|
|
600
|
+
// Every memory file this run considered. `attempted === upserted + skipped
|
|
601
|
+
// + failed` holds, so a caller can tell "nothing to do" from "nothing ran".
|
|
398
602
|
attempted: files.length,
|
|
399
603
|
upserted,
|
|
400
604
|
failed: failures.length,
|
|
605
|
+
skipped,
|
|
401
606
|
failures: failures.slice(0, 5)
|
|
402
607
|
};
|
|
403
608
|
}
|
|
@@ -406,12 +611,14 @@ var init_memory_knowledge_bridge = __esm({
|
|
|
406
611
|
"src/tools/memory/memory-knowledge-bridge.ts"() {
|
|
407
612
|
"use strict";
|
|
408
613
|
init_safe_memory_file();
|
|
614
|
+
init_bounded_sync();
|
|
615
|
+
init_memory_push_cache();
|
|
409
616
|
CONTENT_HARD_LIMIT = 5e5;
|
|
410
617
|
}
|
|
411
618
|
});
|
|
412
619
|
|
|
413
620
|
// src/server.ts
|
|
414
|
-
import { randomUUID as
|
|
621
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
415
622
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
416
623
|
import {
|
|
417
624
|
CallToolRequestSchema,
|
|
@@ -5492,50 +5699,294 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
|
|
|
5492
5699
|
}
|
|
5493
5700
|
|
|
5494
5701
|
// src/tools/memory/sync-config.ts
|
|
5495
|
-
import {
|
|
5496
|
-
import {
|
|
5497
|
-
import {
|
|
5702
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
5703
|
+
import { homedir as homedir7 } from "node:os";
|
|
5704
|
+
import { join as join11 } from "node:path";
|
|
5705
|
+
|
|
5706
|
+
// src/tools/memory/memory-sync-http.ts
|
|
5498
5707
|
init_safe_memory_file();
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5708
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
5709
|
+
|
|
5710
|
+
// src/tools/memory/sync-lock.ts
|
|
5711
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
5712
|
+
import { hostname } from "node:os";
|
|
5713
|
+
import { join as join8 } from "node:path";
|
|
5714
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
5715
|
+
|
|
5716
|
+
// src/tools/memory/sync-lock-liveness.ts
|
|
5717
|
+
import { statSync as statSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
5718
|
+
function defaultIsProcessAlive(pid) {
|
|
5719
|
+
try {
|
|
5720
|
+
process.kill(pid, 0);
|
|
5721
|
+
return true;
|
|
5722
|
+
} catch (err) {
|
|
5723
|
+
return err.code === "EPERM";
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
function toPayload(parsed) {
|
|
5727
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
5728
|
+
const record = parsed;
|
|
5729
|
+
const token = record["token"];
|
|
5730
|
+
const host = record["hostname"];
|
|
5731
|
+
if (typeof token !== "string" || token.length === 0) return null;
|
|
5732
|
+
const pid = record["pid"];
|
|
5733
|
+
const acquiredAtMs = record["acquiredAtMs"];
|
|
5734
|
+
return {
|
|
5735
|
+
pid: typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : 0,
|
|
5736
|
+
hostname: typeof host === "string" ? host : "",
|
|
5737
|
+
sessionId: typeof record["sessionId"] === "string" ? record["sessionId"] : null,
|
|
5738
|
+
token,
|
|
5739
|
+
acquiredAt: typeof record["acquiredAt"] === "string" ? record["acquiredAt"] : "",
|
|
5740
|
+
acquiredAtMs: typeof acquiredAtMs === "number" && Number.isFinite(acquiredAtMs) ? acquiredAtMs : Number.NaN
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5743
|
+
function readLockRecord(path3) {
|
|
5744
|
+
let raw;
|
|
5745
|
+
try {
|
|
5746
|
+
raw = readFileSync8(path3, "utf8");
|
|
5747
|
+
} catch {
|
|
5748
|
+
return null;
|
|
5749
|
+
}
|
|
5750
|
+
try {
|
|
5751
|
+
return { raw, payload: toPayload(JSON.parse(raw)) };
|
|
5752
|
+
} catch {
|
|
5753
|
+
return { raw, payload: null };
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
function lockAgeMs(record, path3, nowMs) {
|
|
5757
|
+
let startedMs = Number.NaN;
|
|
5758
|
+
if (record.payload) {
|
|
5759
|
+
if (Number.isFinite(record.payload.acquiredAtMs)) {
|
|
5760
|
+
startedMs = record.payload.acquiredAtMs;
|
|
5761
|
+
} else if (record.payload.acquiredAt) {
|
|
5762
|
+
startedMs = Date.parse(record.payload.acquiredAt);
|
|
5511
5763
|
}
|
|
5512
|
-
}
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
return true;
|
|
5764
|
+
}
|
|
5765
|
+
if (!Number.isFinite(startedMs)) {
|
|
5766
|
+
try {
|
|
5767
|
+
startedMs = statSync4(path3).mtimeMs;
|
|
5768
|
+
} catch {
|
|
5769
|
+
return null;
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
const age = nowMs - startedMs;
|
|
5773
|
+
return Number.isFinite(age) && age >= 0 ? age : null;
|
|
5523
5774
|
}
|
|
5524
|
-
function
|
|
5525
|
-
|
|
5775
|
+
function classifyHolderLiveness(record, isProcessAlive, thisHost) {
|
|
5776
|
+
const payload = record.payload;
|
|
5777
|
+
if (payload === null) return "unknown";
|
|
5778
|
+
if (payload.pid <= 0) return "unknown";
|
|
5779
|
+
if (thisHost.length === 0) return "unknown";
|
|
5780
|
+
if (payload.hostname !== thisHost) return "unknown";
|
|
5781
|
+
return isProcessAlive(payload.pid) ? "alive" : "dead";
|
|
5526
5782
|
}
|
|
5527
|
-
function
|
|
5528
|
-
const
|
|
5529
|
-
|
|
5783
|
+
function isLockAbandoned(record, ageMs, ttlMs, isProcessAlive, thisHost) {
|
|
5784
|
+
const liveness = classifyHolderLiveness(record, isProcessAlive, thisHost);
|
|
5785
|
+
if (liveness === "alive") return false;
|
|
5786
|
+
if (liveness === "dead") return true;
|
|
5787
|
+
return ageMs !== null && ageMs > ttlMs;
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
// src/tools/memory/sync-lock.ts
|
|
5791
|
+
var MEMORY_SYNC_LOCK_FILE = ".memory-sync.lock";
|
|
5792
|
+
var DEFAULT_LOCK_TTL_MS = 15 * 6e4;
|
|
5793
|
+
var DEFAULT_LOCK_WAIT_MS = 1e4;
|
|
5794
|
+
var INITIAL_BACKOFF_MS = 25;
|
|
5795
|
+
var MAX_BACKOFF_MS = 500;
|
|
5796
|
+
var BACKOFF_FACTOR = 1.6;
|
|
5797
|
+
function positiveOr(value, fallback) {
|
|
5798
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
5799
|
+
}
|
|
5800
|
+
function createExclusive2(path3, contents) {
|
|
5801
|
+
let fd;
|
|
5802
|
+
try {
|
|
5803
|
+
fd = openSync3(path3, "wx");
|
|
5804
|
+
} catch (err) {
|
|
5805
|
+
const code = err.code;
|
|
5806
|
+
return { ok: false, exists: code === "EEXIST", message: err instanceof Error ? err.message : String(err) };
|
|
5807
|
+
}
|
|
5808
|
+
try {
|
|
5809
|
+
writeFileSync4(fd, contents, "utf8");
|
|
5810
|
+
} catch (err) {
|
|
5811
|
+
closeSync2(fd);
|
|
5812
|
+
try {
|
|
5813
|
+
unlinkSync2(path3);
|
|
5814
|
+
} catch {
|
|
5815
|
+
}
|
|
5816
|
+
return { ok: false, exists: false, message: err instanceof Error ? err.message : String(err) };
|
|
5817
|
+
}
|
|
5818
|
+
closeSync2(fd);
|
|
5819
|
+
return { ok: true };
|
|
5820
|
+
}
|
|
5821
|
+
function removeAbandoned(path3, expectedRaw) {
|
|
5822
|
+
let current;
|
|
5823
|
+
try {
|
|
5824
|
+
current = readFileSync9(path3, "utf8");
|
|
5825
|
+
} catch {
|
|
5826
|
+
return;
|
|
5827
|
+
}
|
|
5828
|
+
if (current !== expectedRaw) return;
|
|
5829
|
+
try {
|
|
5830
|
+
unlinkSync2(path3);
|
|
5831
|
+
} catch {
|
|
5832
|
+
}
|
|
5530
5833
|
}
|
|
5834
|
+
function makeRelease(path3, token) {
|
|
5835
|
+
let released = false;
|
|
5836
|
+
return () => {
|
|
5837
|
+
if (released) return;
|
|
5838
|
+
released = true;
|
|
5839
|
+
let raw;
|
|
5840
|
+
try {
|
|
5841
|
+
raw = readFileSync9(path3, "utf8");
|
|
5842
|
+
} catch {
|
|
5843
|
+
return;
|
|
5844
|
+
}
|
|
5845
|
+
let stillOurs;
|
|
5846
|
+
try {
|
|
5847
|
+
stillOurs = toPayload(JSON.parse(raw))?.token === token;
|
|
5848
|
+
} catch {
|
|
5849
|
+
stillOurs = false;
|
|
5850
|
+
}
|
|
5851
|
+
if (!stillOurs) return;
|
|
5852
|
+
try {
|
|
5853
|
+
unlinkSync2(path3);
|
|
5854
|
+
} catch {
|
|
5855
|
+
}
|
|
5856
|
+
};
|
|
5857
|
+
}
|
|
5858
|
+
function describeHolder(record) {
|
|
5859
|
+
const payload = record?.payload;
|
|
5860
|
+
if (!payload) return "an unreadable lock file";
|
|
5861
|
+
return `pid ${payload.pid} on ${payload.hostname || "(unknown host)"} (session ${payload.sessionId ?? "unknown"}, held since ${payload.acquiredAt || "unknown"})`;
|
|
5862
|
+
}
|
|
5863
|
+
async function acquireMemorySyncLock(options) {
|
|
5864
|
+
const waitMs = positiveOr(options.waitMs, DEFAULT_LOCK_WAIT_MS);
|
|
5865
|
+
const ttlMs = positiveOr(options.ttlMs, DEFAULT_LOCK_TTL_MS);
|
|
5866
|
+
const now = options.now ?? Date.now;
|
|
5867
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => {
|
|
5868
|
+
setTimeout(resolve3, ms);
|
|
5869
|
+
}));
|
|
5870
|
+
const isProcessAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
5871
|
+
const thisHost = hostname();
|
|
5872
|
+
const path3 = join8(options.memoryDir, MEMORY_SYNC_LOCK_FILE);
|
|
5873
|
+
if (options.createDir === true) mkdirSync5(options.memoryDir, { recursive: true });
|
|
5874
|
+
const deadline = now() + waitMs;
|
|
5875
|
+
let backoffMs = INITIAL_BACKOFF_MS;
|
|
5876
|
+
let tookOverFrom = null;
|
|
5877
|
+
let holderDescription = "another session";
|
|
5878
|
+
for (; ; ) {
|
|
5879
|
+
const acquiredAtMs = now();
|
|
5880
|
+
const payload = {
|
|
5881
|
+
pid: process.pid,
|
|
5882
|
+
hostname: thisHost,
|
|
5883
|
+
sessionId: options.sessionId ?? null,
|
|
5884
|
+
token: randomUUID2(),
|
|
5885
|
+
acquiredAt: new Date(acquiredAtMs).toISOString(),
|
|
5886
|
+
acquiredAtMs
|
|
5887
|
+
};
|
|
5888
|
+
const created = createExclusive2(path3, `${JSON.stringify(payload, null, 2)}
|
|
5889
|
+
`);
|
|
5890
|
+
if (created.ok) {
|
|
5891
|
+
return { path: path3, payload, tookOverFrom, release: makeRelease(path3, payload.token) };
|
|
5892
|
+
}
|
|
5893
|
+
if (!created.exists) {
|
|
5894
|
+
throw new Error(
|
|
5895
|
+
`memory sync lock ${path3} could not be created (${created.message}) \u2014 refusing to sync without exclusion`
|
|
5896
|
+
);
|
|
5897
|
+
}
|
|
5898
|
+
const record = readLockRecord(path3);
|
|
5899
|
+
let reclaimed = false;
|
|
5900
|
+
if (record) {
|
|
5901
|
+
holderDescription = describeHolder(record);
|
|
5902
|
+
const age = lockAgeMs(record, path3, now());
|
|
5903
|
+
if (isLockAbandoned(record, age, ttlMs, isProcessAlive, thisHost)) {
|
|
5904
|
+
tookOverFrom = record.payload;
|
|
5905
|
+
removeAbandoned(path3, record.raw);
|
|
5906
|
+
reclaimed = true;
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
if (now() >= deadline) {
|
|
5910
|
+
throw new Error(
|
|
5911
|
+
`memory sync lock ${path3} is held by ${holderDescription}; waited ${waitMs}ms \u2014 refusing to sync unlocked (concurrent memory writes corrupt the shared index). If that holder is provably gone, delete the lock file.`
|
|
5912
|
+
);
|
|
5913
|
+
}
|
|
5914
|
+
if (reclaimed) backoffMs = INITIAL_BACKOFF_MS;
|
|
5915
|
+
await sleep(Math.max(1, Math.min(backoffMs, deadline - now())));
|
|
5916
|
+
if (!reclaimed) backoffMs = Math.min(MAX_BACKOFF_MS, Math.ceil(backoffMs * BACKOFF_FACTOR));
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
async function withMemorySyncLock(options, fn) {
|
|
5920
|
+
const handle = await acquireMemorySyncLock(options);
|
|
5921
|
+
try {
|
|
5922
|
+
return await fn(handle);
|
|
5923
|
+
} finally {
|
|
5924
|
+
handle.release();
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5928
|
+
// src/tools/memory/memory-index-merge.ts
|
|
5929
|
+
var MEMORY_INDEX_FILE = "MEMORY.md";
|
|
5930
|
+
function isMemoryIndexFile(fileName) {
|
|
5931
|
+
return fileName.toUpperCase() === MEMORY_INDEX_FILE.toUpperCase();
|
|
5932
|
+
}
|
|
5933
|
+
var INDEX_ROW_RE = /^\s*[-*]\s+\[[^\]]*\]\(([^)]+)\)/;
|
|
5934
|
+
function indexRowKey(line) {
|
|
5935
|
+
const match = INDEX_ROW_RE.exec(line);
|
|
5936
|
+
if (!match) return null;
|
|
5937
|
+
let target = match[1].trim();
|
|
5938
|
+
if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1).trim();
|
|
5939
|
+
target = target.replace(/\s+(["'])[\s\S]*\1$/, "").trim();
|
|
5940
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(target)) {
|
|
5941
|
+
target = target.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
5942
|
+
target = target.replace(/^(?:\.\/)+/, "");
|
|
5943
|
+
}
|
|
5944
|
+
return target.length > 0 ? target.toLowerCase() : null;
|
|
5945
|
+
}
|
|
5946
|
+
function mergeMemoryIndex(localContent, cloudContent) {
|
|
5947
|
+
if (typeof cloudContent !== "string" || cloudContent.trim().length === 0) {
|
|
5948
|
+
return { content: localContent, addedFromCloud: [] };
|
|
5949
|
+
}
|
|
5950
|
+
const eol = localContent.includes("\r\n") ? "\r\n" : "\n";
|
|
5951
|
+
const localLines = localContent.split(/\r?\n/);
|
|
5952
|
+
const localKeys = /* @__PURE__ */ new Set();
|
|
5953
|
+
let lastLocalRowIndex = -1;
|
|
5954
|
+
for (let i = 0; i < localLines.length; i++) {
|
|
5955
|
+
const key = indexRowKey(localLines[i]);
|
|
5956
|
+
if (key === null) continue;
|
|
5957
|
+
localKeys.add(key);
|
|
5958
|
+
lastLocalRowIndex = i;
|
|
5959
|
+
}
|
|
5960
|
+
const addedFromCloud = [];
|
|
5961
|
+
const seenCloudKeys = /* @__PURE__ */ new Set();
|
|
5962
|
+
for (const rawLine of cloudContent.split(/\r?\n/)) {
|
|
5963
|
+
const key = indexRowKey(rawLine);
|
|
5964
|
+
if (key === null) continue;
|
|
5965
|
+
if (localKeys.has(key) || seenCloudKeys.has(key)) continue;
|
|
5966
|
+
seenCloudKeys.add(key);
|
|
5967
|
+
addedFromCloud.push(rawLine.replace(/\r$/, ""));
|
|
5968
|
+
}
|
|
5969
|
+
if (addedFromCloud.length === 0) {
|
|
5970
|
+
return { content: localContent, addedFromCloud: [] };
|
|
5971
|
+
}
|
|
5972
|
+
const merged = lastLocalRowIndex >= 0 ? [...localLines.slice(0, lastLocalRowIndex + 1), ...addedFromCloud, ...localLines.slice(lastLocalRowIndex + 1)] : [...localLines, ...addedFromCloud];
|
|
5973
|
+
return { content: merged.join(eol), addedFromCloud };
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
// src/tools/memory/memory-sync-http.ts
|
|
5977
|
+
init_bounded_sync();
|
|
5978
|
+
init_memory_push_cache();
|
|
5531
5979
|
async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
5532
5980
|
const url = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
5533
|
-
const response = await
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5981
|
+
const response = await withRequestTimeout(
|
|
5982
|
+
url,
|
|
5983
|
+
() => fetchFn(url, {
|
|
5984
|
+
method: "GET",
|
|
5985
|
+
headers: {
|
|
5986
|
+
authorization: `Bearer ${token}`
|
|
5987
|
+
}
|
|
5988
|
+
})
|
|
5989
|
+
);
|
|
5539
5990
|
if (response.status !== 200) {
|
|
5540
5991
|
const text = await response.text();
|
|
5541
5992
|
throw new Error(`GET /api/v1/agent-config/memory/me returned HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
@@ -5548,103 +5999,246 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
|
|
|
5548
5999
|
entry,
|
|
5549
6000
|
filePath: resolveMemoryFilePath(memoryDir, entry.file_name)
|
|
5550
6001
|
}));
|
|
5551
|
-
|
|
6002
|
+
mkdirSync6(memoryDir, { recursive: true });
|
|
5552
6003
|
const files = [];
|
|
5553
6004
|
for (const { entry, filePath } of writes) {
|
|
5554
|
-
|
|
6005
|
+
writeFileSync6(filePath, entry.content, "utf8");
|
|
5555
6006
|
files.push(entry.file_name);
|
|
5556
6007
|
}
|
|
5557
6008
|
return { pulled: data.entries.length, files };
|
|
5558
6009
|
}
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
6010
|
+
function listPushableFiles(memoryDir) {
|
|
6011
|
+
return readdirSync4(memoryDir).filter((f) => f.endsWith(".md") && f !== MEMORY_SYNC_LOCK_FILE);
|
|
6012
|
+
}
|
|
6013
|
+
async function uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline) {
|
|
6014
|
+
deadline.check();
|
|
6015
|
+
if (item.memoryId !== null) {
|
|
6016
|
+
const updateUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/${item.memoryId}`;
|
|
6017
|
+
const updateBody = { content: item.content, session_id: sessionId };
|
|
6018
|
+
const updateResponse = await withRequestTimeout(
|
|
6019
|
+
updateUrl,
|
|
6020
|
+
() => fetchFn(updateUrl, {
|
|
6021
|
+
method: "PUT",
|
|
6022
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
6023
|
+
body: JSON.stringify(updateBody)
|
|
6024
|
+
})
|
|
6025
|
+
);
|
|
6026
|
+
if (updateResponse.status !== 200) {
|
|
6027
|
+
const text = await updateResponse.text();
|
|
6028
|
+
throw new Error(
|
|
6029
|
+
`PUT /api/v1/agent-config/memory/${item.memoryId} returned HTTP ${updateResponse.status}: ${text.slice(0, 200)}`
|
|
6030
|
+
);
|
|
6031
|
+
}
|
|
6032
|
+
const updateData = JSON.parse(await updateResponse.text());
|
|
6033
|
+
if (!updateData.ok) throw new Error(`PUT /api/v1/agent-config/memory/${item.memoryId} returned ok=false`);
|
|
6034
|
+
return "updated";
|
|
6035
|
+
}
|
|
6036
|
+
const createUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
6037
|
+
const createBody = {
|
|
6038
|
+
entry_type: item.entryType,
|
|
6039
|
+
file_name: item.fileName,
|
|
6040
|
+
content: item.content,
|
|
6041
|
+
session_id: sessionId
|
|
6042
|
+
};
|
|
6043
|
+
const createResponse = await withRequestTimeout(
|
|
6044
|
+
createUrl,
|
|
6045
|
+
() => fetchFn(createUrl, {
|
|
6046
|
+
method: "POST",
|
|
6047
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
6048
|
+
body: JSON.stringify(createBody)
|
|
6049
|
+
})
|
|
6050
|
+
);
|
|
6051
|
+
if (createResponse.status !== 200 && createResponse.status !== 201) {
|
|
6052
|
+
const text = await createResponse.text();
|
|
6053
|
+
throw new Error(
|
|
6054
|
+
`POST /api/v1/agent-config/memory/me returned HTTP ${createResponse.status}: ${text.slice(0, 200)}`
|
|
6055
|
+
);
|
|
6056
|
+
}
|
|
6057
|
+
const createData = JSON.parse(await createResponse.text());
|
|
6058
|
+
if (!createData.ok) throw new Error("POST /api/v1/agent-config/memory/me returned ok=false");
|
|
6059
|
+
return "created";
|
|
6060
|
+
}
|
|
6061
|
+
async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn, options = {}) {
|
|
6062
|
+
const empty = { pushed: 0, created: 0, updated: 0, skipped: 0, indexRowsPreserved: 0 };
|
|
6063
|
+
if (!existsSync5(memoryDir)) {
|
|
6064
|
+
return empty;
|
|
5562
6065
|
}
|
|
5563
|
-
const localFiles =
|
|
6066
|
+
const localFiles = listPushableFiles(memoryDir).map((f) => ({
|
|
5564
6067
|
file_name: f,
|
|
5565
|
-
content:
|
|
5566
|
-
entry_type: f
|
|
6068
|
+
content: readFileSync11(resolveMemoryFilePath(memoryDir, f), "utf8"),
|
|
6069
|
+
entry_type: isMemoryIndexFile(f) ? "index" : "topic"
|
|
5567
6070
|
}));
|
|
5568
6071
|
if (localFiles.length === 0) {
|
|
5569
|
-
return
|
|
6072
|
+
return empty;
|
|
5570
6073
|
}
|
|
6074
|
+
const deadline = options.deadline ?? createSyncDeadline();
|
|
6075
|
+
const cache = options.cache ?? readPushCache(memoryDir, controlPlaneUrl);
|
|
6076
|
+
deadline.check();
|
|
5571
6077
|
const getUrl = `${controlPlaneUrl}/api/v1/agent-config/memory/me`;
|
|
5572
|
-
const getResponse = await
|
|
5573
|
-
|
|
5574
|
-
headers: {
|
|
5575
|
-
|
|
5576
|
-
}
|
|
5577
|
-
});
|
|
6078
|
+
const getResponse = await withRequestTimeout(
|
|
6079
|
+
getUrl,
|
|
6080
|
+
() => fetchFn(getUrl, { method: "GET", headers: { authorization: `Bearer ${token}` } })
|
|
6081
|
+
);
|
|
5578
6082
|
const existingMap = /* @__PURE__ */ new Map();
|
|
5579
6083
|
if (getResponse.status === 200) {
|
|
5580
6084
|
const getData = JSON.parse(await getResponse.text());
|
|
5581
6085
|
if (getData.ok && Array.isArray(getData.entries)) {
|
|
5582
6086
|
for (const entry of getData.entries) {
|
|
5583
|
-
existingMap.set(entry.file_name,
|
|
6087
|
+
existingMap.set(entry.file_name, {
|
|
6088
|
+
memoryId: entry.memory_id,
|
|
6089
|
+
content: typeof entry.content === "string" ? entry.content : ""
|
|
6090
|
+
});
|
|
5584
6091
|
}
|
|
5585
6092
|
}
|
|
5586
6093
|
}
|
|
6094
|
+
const toUpload = [];
|
|
6095
|
+
let skipped = 0;
|
|
6096
|
+
for (const localFile of localFiles) {
|
|
6097
|
+
const existing = existingMap.get(localFile.file_name);
|
|
6098
|
+
let content = localFile.content;
|
|
6099
|
+
let rowsPreserved = 0;
|
|
6100
|
+
if (localFile.entry_type === "index") {
|
|
6101
|
+
const merged = mergeMemoryIndex(localFile.content, existing?.content);
|
|
6102
|
+
content = merged.content;
|
|
6103
|
+
rowsPreserved = merged.addedFromCloud.length;
|
|
6104
|
+
}
|
|
6105
|
+
const payloadHash = sha256(content);
|
|
6106
|
+
if (!needsMemoryPush(cache, localFile.file_name, payloadHash, existing !== void 0)) {
|
|
6107
|
+
skipped++;
|
|
6108
|
+
continue;
|
|
6109
|
+
}
|
|
6110
|
+
toUpload.push({
|
|
6111
|
+
fileName: localFile.file_name,
|
|
6112
|
+
entryType: localFile.entry_type,
|
|
6113
|
+
content,
|
|
6114
|
+
payloadHash,
|
|
6115
|
+
memoryId: existing?.memoryId ?? null,
|
|
6116
|
+
rowsPreserved
|
|
6117
|
+
});
|
|
6118
|
+
}
|
|
6119
|
+
const outcomes = await mapWithConcurrency(
|
|
6120
|
+
toUpload,
|
|
6121
|
+
options.concurrency ?? PUSH_CONCURRENCY,
|
|
6122
|
+
(item) => uploadOne(item, controlPlaneUrl, token, sessionId, fetchFn, deadline)
|
|
6123
|
+
);
|
|
5587
6124
|
let created = 0;
|
|
5588
6125
|
let updated = 0;
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
6126
|
+
let indexRowsPreserved = 0;
|
|
6127
|
+
let firstError;
|
|
6128
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
6129
|
+
const outcome = outcomes[i];
|
|
6130
|
+
const item = toUpload[i];
|
|
6131
|
+
if (outcome.ok) {
|
|
6132
|
+
if (outcome.value === "created") created++;
|
|
6133
|
+
else updated++;
|
|
6134
|
+
indexRowsPreserved += item.rowsPreserved;
|
|
6135
|
+
recordMemoryPush(cache, item.fileName, item.payloadHash);
|
|
6136
|
+
} else if (firstError === void 0) {
|
|
6137
|
+
firstError = outcome.error;
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
pruneMissing(cache, localFiles.map((f) => f.file_name));
|
|
6141
|
+
if (options.persistCache ?? options.cache === void 0) writePushCache(memoryDir, cache);
|
|
6142
|
+
if (firstError !== void 0) throw firstError;
|
|
6143
|
+
return { pushed: created + updated, created, updated, skipped, indexRowsPreserved };
|
|
6144
|
+
}
|
|
6145
|
+
|
|
6146
|
+
// src/tools/memory/sync-config.ts
|
|
6147
|
+
init_bounded_sync();
|
|
6148
|
+
init_memory_push_cache();
|
|
6149
|
+
|
|
6150
|
+
// src/tools/memory/sync-kill-switch.ts
|
|
6151
|
+
import { existsSync as existsSync6, readFileSync as readFileSync12 } from "node:fs";
|
|
6152
|
+
import { homedir as homedir6 } from "node:os";
|
|
6153
|
+
import { join as join10 } from "node:path";
|
|
6154
|
+
var MEMORY_SYNC_DISABLE_ENV = "VO_MCP_DISABLE_MEMORY_SYNC";
|
|
6155
|
+
var MEMORY_SYNC_DISABLE_SENTINEL = "vo-memory-sync-disabled";
|
|
6156
|
+
var NEGATIONS = /* @__PURE__ */ new Set(["0", "false", "no"]);
|
|
6157
|
+
var MAX_LOGGED_VALUE = 32;
|
|
6158
|
+
function memorySyncSentinelPath(home) {
|
|
6159
|
+
return join10(home, ".claude", MEMORY_SYNC_DISABLE_SENTINEL);
|
|
6160
|
+
}
|
|
6161
|
+
function isKillSwitchValueOn(raw) {
|
|
6162
|
+
if (raw === void 0 || raw === null) return false;
|
|
6163
|
+
const v = raw.trim().toLowerCase();
|
|
6164
|
+
if (v === "") return false;
|
|
6165
|
+
return !NEGATIONS.has(v);
|
|
6166
|
+
}
|
|
6167
|
+
function clip(raw) {
|
|
6168
|
+
const v = raw.trim();
|
|
6169
|
+
return v.length > MAX_LOGGED_VALUE ? `${v.slice(0, MAX_LOGGED_VALUE)}\u2026` : v;
|
|
6170
|
+
}
|
|
6171
|
+
function evaluateMemorySyncKillSwitch(deps = {}) {
|
|
6172
|
+
const env = deps.env ?? process.env;
|
|
6173
|
+
const home = deps.home ?? homedir6();
|
|
6174
|
+
const fileExists = deps.fileExists ?? existsSync6;
|
|
6175
|
+
const readFile3 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
|
|
6176
|
+
const fired = [];
|
|
6177
|
+
const rawEnv = env[MEMORY_SYNC_DISABLE_ENV];
|
|
6178
|
+
if (isKillSwitchValueOn(rawEnv)) {
|
|
6179
|
+
fired.push(`env ${MEMORY_SYNC_DISABLE_ENV}=${clip(rawEnv)}`);
|
|
6180
|
+
}
|
|
6181
|
+
const sentinel = memorySyncSentinelPath(home);
|
|
6182
|
+
let sentinelPresent;
|
|
6183
|
+
try {
|
|
6184
|
+
sentinelPresent = fileExists(sentinel);
|
|
6185
|
+
} catch {
|
|
6186
|
+
sentinelPresent = false;
|
|
6187
|
+
}
|
|
6188
|
+
if (sentinelPresent) {
|
|
6189
|
+
let contents = "";
|
|
6190
|
+
let readable = true;
|
|
6191
|
+
try {
|
|
6192
|
+
contents = readFile3(sentinel);
|
|
6193
|
+
} catch {
|
|
6194
|
+
readable = false;
|
|
6195
|
+
}
|
|
6196
|
+
if (!readable || isKillSwitchValueOn(contents) || contents.trim() === "") {
|
|
6197
|
+
fired.push(`sentinel file ${sentinel}`);
|
|
5643
6198
|
}
|
|
5644
6199
|
}
|
|
5645
|
-
return {
|
|
6200
|
+
if (fired.length === 0) return { disabled: false, reason: null };
|
|
6201
|
+
return { disabled: true, reason: `memory sync DISABLED by ${fired.join(" + ")}` };
|
|
5646
6202
|
}
|
|
5647
|
-
|
|
6203
|
+
|
|
6204
|
+
// src/tools/memory/sync-config.ts
|
|
6205
|
+
var TOOL_NAME22 = "vo_sync_config";
|
|
6206
|
+
var inputSchema22 = {
|
|
6207
|
+
type: "object",
|
|
6208
|
+
properties: {
|
|
6209
|
+
action: {
|
|
6210
|
+
type: "string",
|
|
6211
|
+
enum: ["pull", "push"],
|
|
6212
|
+
description: "pull: download cloud memory to local files. push: upload local files to cloud."
|
|
6213
|
+
},
|
|
6214
|
+
cwd: {
|
|
6215
|
+
type: "string",
|
|
6216
|
+
description: "Working directory to derive project slug from (default: process.cwd())."
|
|
6217
|
+
}
|
|
6218
|
+
},
|
|
6219
|
+
required: ["action"],
|
|
6220
|
+
additionalProperties: false
|
|
6221
|
+
};
|
|
6222
|
+
var description22 = "Syncs memory entries between local ~/.claude/projects/<slug>/memory/ and cloud control-plane /api/v1/agent-config/memory/me. Requires operator auth (vo-mcp login). Actions: pull (cloud\u2192local), push (local\u2192cloud). Idempotent; push creates/updates as needed. Serialized across concurrent sessions by an exclusive lock in the memory dir.";
|
|
6223
|
+
function isToolInput22(v) {
|
|
6224
|
+
if (typeof v !== "object" || v === null) return false;
|
|
6225
|
+
const o = v;
|
|
6226
|
+
if (o["action"] !== "pull" && o["action"] !== "push") return false;
|
|
6227
|
+
if (o["cwd"] !== void 0 && typeof o["cwd"] !== "string") return false;
|
|
6228
|
+
return true;
|
|
6229
|
+
}
|
|
6230
|
+
function deriveProjectSlug(cwd) {
|
|
6231
|
+
return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
|
|
6232
|
+
}
|
|
6233
|
+
function getMemoryDir(cwd) {
|
|
6234
|
+
const slug = deriveProjectSlug(cwd);
|
|
6235
|
+
return join11(homedir7(), ".claude", "projects", slug, "memory");
|
|
6236
|
+
}
|
|
6237
|
+
async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch, lockOptions = {}) {
|
|
6238
|
+
const killSwitch = evaluateMemorySyncKillSwitch();
|
|
6239
|
+
if (killSwitch.disabled) {
|
|
6240
|
+
return { synced: false, reason: killSwitch.reason };
|
|
6241
|
+
}
|
|
5648
6242
|
const controlPlaneUrl = process.env["VO_CONTROL_PLANE_URL"];
|
|
5649
6243
|
if (!controlPlaneUrl) {
|
|
5650
6244
|
return { synced: false, reason: "VO_CONTROL_PLANE_URL not set \u2014 cloud mode disabled" };
|
|
@@ -5661,39 +6255,79 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
|
|
|
5661
6255
|
}
|
|
5662
6256
|
const memoryDir = getMemoryDir(cwd);
|
|
5663
6257
|
const baseUrl = controlPlaneUrl.replace(/\/+$/, "");
|
|
5664
|
-
|
|
5665
|
-
if (action === "pull") {
|
|
5666
|
-
const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
|
|
5667
|
-
return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
|
|
5668
|
-
}
|
|
5669
|
-
const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
|
|
5670
|
-
let bridge = { upserted: 0, failed: 0, failures: [] };
|
|
5671
|
-
try {
|
|
5672
|
-
const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
|
|
5673
|
-
bridge = await upsertMemoryFilesAsKnowledge2({
|
|
5674
|
-
controlPlaneUrl: baseUrl,
|
|
5675
|
-
token,
|
|
5676
|
-
memoryDir,
|
|
5677
|
-
fetchFn
|
|
5678
|
-
});
|
|
5679
|
-
} catch (err) {
|
|
5680
|
-
bridge = {
|
|
5681
|
-
upserted: 0,
|
|
5682
|
-
failed: 1,
|
|
5683
|
-
failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
|
|
5684
|
-
};
|
|
5685
|
-
}
|
|
6258
|
+
if (action === "push" && !existsSync8(memoryDir)) {
|
|
5686
6259
|
return {
|
|
5687
6260
|
synced: true,
|
|
5688
6261
|
action: "push",
|
|
5689
|
-
pushed:
|
|
5690
|
-
created:
|
|
5691
|
-
updated:
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
6262
|
+
pushed: 0,
|
|
6263
|
+
created: 0,
|
|
6264
|
+
updated: 0,
|
|
6265
|
+
skipped: 0,
|
|
6266
|
+
index_rows_preserved: 0,
|
|
6267
|
+
knowledge_upserted: 0,
|
|
6268
|
+
knowledge_failed: 0,
|
|
6269
|
+
memory_dir: memoryDir
|
|
5696
6270
|
};
|
|
6271
|
+
}
|
|
6272
|
+
try {
|
|
6273
|
+
return await withMemorySyncLock({ ...lockOptions, memoryDir, sessionId, createDir: action === "pull" }, async (lock) => {
|
|
6274
|
+
const takeover = lock.tookOverFrom ? { lock_taken_over_from_pid: lock.tookOverFrom.pid } : {};
|
|
6275
|
+
if (action === "pull") {
|
|
6276
|
+
const result2 = await pullMemory(baseUrl, token, memoryDir, fetchFn);
|
|
6277
|
+
return {
|
|
6278
|
+
synced: true,
|
|
6279
|
+
action: "pull",
|
|
6280
|
+
pulled: result2.pulled,
|
|
6281
|
+
files: result2.files,
|
|
6282
|
+
memory_dir: memoryDir,
|
|
6283
|
+
...takeover
|
|
6284
|
+
};
|
|
6285
|
+
}
|
|
6286
|
+
const deadline = createSyncDeadline();
|
|
6287
|
+
const cache = readPushCache(memoryDir, baseUrl);
|
|
6288
|
+
let result;
|
|
6289
|
+
try {
|
|
6290
|
+
result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn, { cache, deadline });
|
|
6291
|
+
} catch (err) {
|
|
6292
|
+
writePushCache(memoryDir, cache);
|
|
6293
|
+
throw err;
|
|
6294
|
+
}
|
|
6295
|
+
let bridge;
|
|
6296
|
+
try {
|
|
6297
|
+
const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
|
|
6298
|
+
bridge = await upsertMemoryFilesAsKnowledge2({
|
|
6299
|
+
controlPlaneUrl: baseUrl,
|
|
6300
|
+
token,
|
|
6301
|
+
memoryDir,
|
|
6302
|
+
fetchFn,
|
|
6303
|
+
cache,
|
|
6304
|
+
deadline
|
|
6305
|
+
});
|
|
6306
|
+
} catch (err) {
|
|
6307
|
+
bridge = {
|
|
6308
|
+
upserted: 0,
|
|
6309
|
+
failed: 1,
|
|
6310
|
+
skipped: 0,
|
|
6311
|
+
failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
|
|
6312
|
+
};
|
|
6313
|
+
}
|
|
6314
|
+
writePushCache(memoryDir, cache);
|
|
6315
|
+
return {
|
|
6316
|
+
synced: true,
|
|
6317
|
+
action: "push",
|
|
6318
|
+
pushed: result.pushed,
|
|
6319
|
+
created: result.created,
|
|
6320
|
+
updated: result.updated,
|
|
6321
|
+
skipped: result.skipped,
|
|
6322
|
+
index_rows_preserved: result.indexRowsPreserved,
|
|
6323
|
+
memory_dir: memoryDir,
|
|
6324
|
+
knowledge_upserted: bridge.upserted,
|
|
6325
|
+
knowledge_failed: bridge.failed,
|
|
6326
|
+
knowledge_skipped: bridge.skipped,
|
|
6327
|
+
...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {},
|
|
6328
|
+
...takeover
|
|
6329
|
+
};
|
|
6330
|
+
});
|
|
5697
6331
|
} catch (err) {
|
|
5698
6332
|
const message = err instanceof Error ? err.message : String(err);
|
|
5699
6333
|
return { synced: false, reason: `Sync failed: ${message}` };
|
|
@@ -5980,12 +6614,12 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
|
5980
6614
|
}
|
|
5981
6615
|
|
|
5982
6616
|
// src/tools/skills/skill-corpus.ts
|
|
5983
|
-
import { existsSync as
|
|
5984
|
-
import { dirname as dirname5, isAbsolute, join as
|
|
6617
|
+
import { existsSync as existsSync9, statSync as statSync6 } from "node:fs";
|
|
6618
|
+
import { dirname as dirname5, isAbsolute, join as join13, resolve as resolve2 } from "node:path";
|
|
5985
6619
|
|
|
5986
6620
|
// ../skill-registry/src/loader.ts
|
|
5987
|
-
import { readdirSync as readdirSync6, readFileSync as
|
|
5988
|
-
import { join as
|
|
6621
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync5 } from "node:fs";
|
|
6622
|
+
import { join as join12 } from "node:path";
|
|
5989
6623
|
var InvalidSkillFrontmatterError = class extends Error {
|
|
5990
6624
|
constructor(skillFile, reason) {
|
|
5991
6625
|
super(`Invalid frontmatter in ${skillFile}: ${reason}`);
|
|
@@ -6038,18 +6672,18 @@ function loadSkillsFromDir(skillsDir) {
|
|
|
6038
6672
|
const entries = readdirSync6(skillsDir);
|
|
6039
6673
|
const skills = [];
|
|
6040
6674
|
for (const entry of entries) {
|
|
6041
|
-
const entryPath =
|
|
6675
|
+
const entryPath = join12(skillsDir, entry);
|
|
6042
6676
|
let stat;
|
|
6043
6677
|
try {
|
|
6044
|
-
stat =
|
|
6678
|
+
stat = statSync5(entryPath);
|
|
6045
6679
|
} catch {
|
|
6046
6680
|
continue;
|
|
6047
6681
|
}
|
|
6048
6682
|
if (!stat.isDirectory()) continue;
|
|
6049
|
-
const skillFile =
|
|
6683
|
+
const skillFile = join12(entryPath, "SKILL.md");
|
|
6050
6684
|
let raw;
|
|
6051
6685
|
try {
|
|
6052
|
-
raw =
|
|
6686
|
+
raw = readFileSync14(skillFile, "utf8");
|
|
6053
6687
|
} catch {
|
|
6054
6688
|
continue;
|
|
6055
6689
|
}
|
|
@@ -6090,12 +6724,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
|
|
|
6090
6724
|
const override = env.VO_SKILLS_DIR;
|
|
6091
6725
|
if (typeof override === "string" && override.length > 0) {
|
|
6092
6726
|
const abs = isAbsolute(override) ? override : resolve2(startDir, override);
|
|
6093
|
-
return
|
|
6727
|
+
return existsSync9(abs) && statSync6(abs).isDirectory() ? abs : null;
|
|
6094
6728
|
}
|
|
6095
6729
|
let dir = resolve2(startDir);
|
|
6096
6730
|
for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
|
|
6097
|
-
const candidate =
|
|
6098
|
-
if (
|
|
6731
|
+
const candidate = join13(dir, ".claude", "skills");
|
|
6732
|
+
if (existsSync9(candidate) && statSync6(candidate).isDirectory()) return candidate;
|
|
6099
6733
|
const parent = dirname5(dir);
|
|
6100
6734
|
if (parent === dir) break;
|
|
6101
6735
|
dir = parent;
|
|
@@ -6406,7 +7040,7 @@ function buildToolRegistry() {
|
|
|
6406
7040
|
};
|
|
6407
7041
|
}
|
|
6408
7042
|
function createServer(options) {
|
|
6409
|
-
const sessionId = options.sessionId ??
|
|
7043
|
+
const sessionId = options.sessionId ?? randomUUID3();
|
|
6410
7044
|
const mode = createLocalMode();
|
|
6411
7045
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
6412
7046
|
const server = new Server(
|
|
@@ -6458,8 +7092,8 @@ function listToolNames() {
|
|
|
6458
7092
|
}
|
|
6459
7093
|
|
|
6460
7094
|
// src/cache/sqlite-cache.ts
|
|
6461
|
-
import { createHash as
|
|
6462
|
-
import { chmodSync as chmodSync3, mkdirSync as
|
|
7095
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
7096
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync7 } from "node:fs";
|
|
6463
7097
|
import { dirname as dirname6 } from "node:path";
|
|
6464
7098
|
import { DatabaseSync } from "node:sqlite";
|
|
6465
7099
|
|
|
@@ -6505,7 +7139,7 @@ function normalizeString(s) {
|
|
|
6505
7139
|
function createSqliteCache(options) {
|
|
6506
7140
|
const fileBacked = options.dbPath !== ":memory:";
|
|
6507
7141
|
if (fileBacked) {
|
|
6508
|
-
|
|
7142
|
+
mkdirSync7(dirname6(options.dbPath), { recursive: true, mode: 448 });
|
|
6509
7143
|
}
|
|
6510
7144
|
const versionNamespace = options.cacheVersionNamespace ?? "";
|
|
6511
7145
|
const db = new DatabaseSync(options.dbPath);
|
|
@@ -6536,7 +7170,7 @@ function createSqliteCache(options) {
|
|
|
6536
7170
|
return {
|
|
6537
7171
|
keyFor(toolName, input, opts) {
|
|
6538
7172
|
const canonical = canonicalize(input, opts);
|
|
6539
|
-
const hash =
|
|
7173
|
+
const hash = createHash4("sha256");
|
|
6540
7174
|
if (versionNamespace.length > 0) {
|
|
6541
7175
|
hash.update(versionNamespace);
|
|
6542
7176
|
hash.update("|");
|
|
@@ -6628,7 +7262,7 @@ function createStubRatchetClient() {
|
|
|
6628
7262
|
let m;
|
|
6629
7263
|
while ((m = pat.regex.exec(req.source)) !== null) {
|
|
6630
7264
|
findings.push({
|
|
6631
|
-
line_excerpt:
|
|
7265
|
+
line_excerpt: clip2(m[0], 80),
|
|
6632
7266
|
severity: pat.severity,
|
|
6633
7267
|
code: pat.code,
|
|
6634
7268
|
message: pat.message
|
|
@@ -6660,7 +7294,7 @@ function createStubRatchetClient() {
|
|
|
6660
7294
|
}
|
|
6661
7295
|
};
|
|
6662
7296
|
}
|
|
6663
|
-
function
|
|
7297
|
+
function clip2(s, n) {
|
|
6664
7298
|
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
6665
7299
|
}
|
|
6666
7300
|
function buildSummary2(args) {
|
|
@@ -6689,7 +7323,7 @@ function createNullConsensusEngineClient(reason = NULL_CLIENT_DEFAULT_REASON) {
|
|
|
6689
7323
|
}
|
|
6690
7324
|
|
|
6691
7325
|
// src/consensus/engine-client.ts
|
|
6692
|
-
import { randomUUID as
|
|
7326
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
6693
7327
|
|
|
6694
7328
|
// src/consensus/meta-model-caller.ts
|
|
6695
7329
|
var META_CONSENSUS_MODEL = "muse-spark-1.1";
|