@hasna/mementos 0.7.0 → 0.9.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/dist/cli/index.js +1997 -132
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/session-jobs.d.ts +49 -0
- package/dist/db/session-jobs.d.ts.map +1 -0
- package/dist/db/synthesis.d.ts +101 -0
- package/dist/db/synthesis.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/lib/built-in-hooks.d.ts.map +1 -1
- package/dist/lib/open-sessions-connector.d.ts +60 -0
- package/dist/lib/open-sessions-connector.d.ts.map +1 -0
- package/dist/lib/session-auto-resolve.d.ts +28 -0
- package/dist/lib/session-auto-resolve.d.ts.map +1 -0
- package/dist/lib/session-processor.d.ts +38 -0
- package/dist/lib/session-processor.d.ts.map +1 -0
- package/dist/lib/session-queue.d.ts +28 -0
- package/dist/lib/session-queue.d.ts.map +1 -0
- package/dist/lib/synthesis/corpus-builder.d.ts +30 -0
- package/dist/lib/synthesis/corpus-builder.d.ts.map +1 -0
- package/dist/lib/synthesis/executor.d.ts +14 -0
- package/dist/lib/synthesis/executor.d.ts.map +1 -0
- package/dist/lib/synthesis/index.d.ts +37 -0
- package/dist/lib/synthesis/index.d.ts.map +1 -0
- package/dist/lib/synthesis/llm-analyzer.d.ts +18 -0
- package/dist/lib/synthesis/llm-analyzer.d.ts.map +1 -0
- package/dist/lib/synthesis/metrics.d.ts +13 -0
- package/dist/lib/synthesis/metrics.d.ts.map +1 -0
- package/dist/lib/synthesis/scheduler.d.ts +18 -0
- package/dist/lib/synthesis/scheduler.d.ts.map +1 -0
- package/dist/lib/synthesis/validator.d.ts +19 -0
- package/dist/lib/synthesis/validator.d.ts.map +1 -0
- package/dist/mcp/index.js +1793 -33
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1856 -19
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -403,6 +403,75 @@ var init_database = __esm(() => {
|
|
|
403
403
|
ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
|
|
404
404
|
CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
|
|
405
405
|
INSERT OR IGNORE INTO _migrations (id) VALUES (9);
|
|
406
|
+
`,
|
|
407
|
+
`
|
|
408
|
+
CREATE TABLE IF NOT EXISTS synthesis_events (
|
|
409
|
+
id TEXT PRIMARY KEY,
|
|
410
|
+
event_type TEXT NOT NULL CHECK(event_type IN ('recalled','searched','saved','updated','deleted','injected')),
|
|
411
|
+
memory_id TEXT,
|
|
412
|
+
agent_id TEXT,
|
|
413
|
+
project_id TEXT,
|
|
414
|
+
session_id TEXT,
|
|
415
|
+
query TEXT,
|
|
416
|
+
importance_at_time INTEGER,
|
|
417
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
418
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
419
|
+
);
|
|
420
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_events_memory ON synthesis_events(memory_id);
|
|
421
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_events_project ON synthesis_events(project_id);
|
|
422
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_events_type ON synthesis_events(event_type);
|
|
423
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_events_created ON synthesis_events(created_at);
|
|
424
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (11);
|
|
425
|
+
`,
|
|
426
|
+
`
|
|
427
|
+
CREATE TABLE IF NOT EXISTS synthesis_runs (
|
|
428
|
+
id TEXT PRIMARY KEY,
|
|
429
|
+
triggered_by TEXT NOT NULL DEFAULT 'manual' CHECK(triggered_by IN ('scheduler','manual','threshold','hook')),
|
|
430
|
+
project_id TEXT,
|
|
431
|
+
agent_id TEXT,
|
|
432
|
+
corpus_size INTEGER NOT NULL DEFAULT 0,
|
|
433
|
+
proposals_generated INTEGER NOT NULL DEFAULT 0,
|
|
434
|
+
proposals_accepted INTEGER NOT NULL DEFAULT 0,
|
|
435
|
+
proposals_rejected INTEGER NOT NULL DEFAULT 0,
|
|
436
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','running','completed','failed','rolled_back')),
|
|
437
|
+
error TEXT,
|
|
438
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
439
|
+
completed_at TEXT
|
|
440
|
+
);
|
|
441
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_runs_project ON synthesis_runs(project_id);
|
|
442
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_runs_status ON synthesis_runs(status);
|
|
443
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_runs_started ON synthesis_runs(started_at);
|
|
444
|
+
|
|
445
|
+
CREATE TABLE IF NOT EXISTS synthesis_proposals (
|
|
446
|
+
id TEXT PRIMARY KEY,
|
|
447
|
+
run_id TEXT NOT NULL REFERENCES synthesis_runs(id) ON DELETE CASCADE,
|
|
448
|
+
proposal_type TEXT NOT NULL CHECK(proposal_type IN ('merge','archive','promote','update_value','add_tag','remove_duplicate')),
|
|
449
|
+
memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
450
|
+
target_memory_id TEXT,
|
|
451
|
+
proposed_changes TEXT NOT NULL DEFAULT '{}',
|
|
452
|
+
reasoning TEXT,
|
|
453
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
454
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','accepted','rejected','rolled_back')),
|
|
455
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
456
|
+
executed_at TEXT,
|
|
457
|
+
rollback_data TEXT
|
|
458
|
+
);
|
|
459
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_proposals_run ON synthesis_proposals(run_id);
|
|
460
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_proposals_status ON synthesis_proposals(status);
|
|
461
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_proposals_type ON synthesis_proposals(proposal_type);
|
|
462
|
+
|
|
463
|
+
CREATE TABLE IF NOT EXISTS synthesis_metrics (
|
|
464
|
+
id TEXT PRIMARY KEY,
|
|
465
|
+
run_id TEXT NOT NULL REFERENCES synthesis_runs(id) ON DELETE CASCADE,
|
|
466
|
+
metric_type TEXT NOT NULL,
|
|
467
|
+
value REAL NOT NULL,
|
|
468
|
+
baseline REAL,
|
|
469
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
470
|
+
);
|
|
471
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_metrics_run ON synthesis_metrics(run_id);
|
|
472
|
+
CREATE INDEX IF NOT EXISTS idx_synthesis_metrics_type ON synthesis_metrics(metric_type);
|
|
473
|
+
|
|
474
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (12);
|
|
406
475
|
`,
|
|
407
476
|
`
|
|
408
477
|
CREATE TABLE IF NOT EXISTS webhook_hooks (
|
|
@@ -422,7 +491,30 @@ var init_database = __esm(() => {
|
|
|
422
491
|
CREATE INDEX IF NOT EXISTS idx_webhook_hooks_type ON webhook_hooks(type);
|
|
423
492
|
CREATE INDEX IF NOT EXISTS idx_webhook_hooks_enabled ON webhook_hooks(enabled);
|
|
424
493
|
INSERT OR IGNORE INTO _migrations (id) VALUES (10);
|
|
425
|
-
|
|
494
|
+
`,
|
|
495
|
+
`
|
|
496
|
+
CREATE TABLE IF NOT EXISTS session_memory_jobs (
|
|
497
|
+
id TEXT PRIMARY KEY,
|
|
498
|
+
session_id TEXT NOT NULL,
|
|
499
|
+
agent_id TEXT,
|
|
500
|
+
project_id TEXT,
|
|
501
|
+
source TEXT NOT NULL DEFAULT 'manual' CHECK(source IN ('claude-code','codex','manual','open-sessions')),
|
|
502
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','processing','completed','failed')),
|
|
503
|
+
transcript TEXT NOT NULL,
|
|
504
|
+
chunk_count INTEGER NOT NULL DEFAULT 0,
|
|
505
|
+
memories_extracted INTEGER NOT NULL DEFAULT 0,
|
|
506
|
+
error TEXT,
|
|
507
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
508
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
509
|
+
started_at TEXT,
|
|
510
|
+
completed_at TEXT
|
|
511
|
+
);
|
|
512
|
+
CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_status ON session_memory_jobs(status);
|
|
513
|
+
CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_agent ON session_memory_jobs(agent_id);
|
|
514
|
+
CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_project ON session_memory_jobs(project_id);
|
|
515
|
+
CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_session ON session_memory_jobs(session_id);
|
|
516
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (13);
|
|
517
|
+
`
|
|
426
518
|
];
|
|
427
519
|
});
|
|
428
520
|
|
|
@@ -2716,6 +2808,294 @@ var init_auto_memory = __esm(() => {
|
|
|
2716
2808
|
autoMemoryQueue.setHandler(processJob);
|
|
2717
2809
|
});
|
|
2718
2810
|
|
|
2811
|
+
// src/db/synthesis.ts
|
|
2812
|
+
var exports_synthesis = {};
|
|
2813
|
+
__export(exports_synthesis, {
|
|
2814
|
+
updateSynthesisRun: () => updateSynthesisRun,
|
|
2815
|
+
updateProposal: () => updateProposal,
|
|
2816
|
+
recordSynthesisEvent: () => recordSynthesisEvent,
|
|
2817
|
+
listSynthesisRuns: () => listSynthesisRuns,
|
|
2818
|
+
listSynthesisEvents: () => listSynthesisEvents,
|
|
2819
|
+
listProposals: () => listProposals,
|
|
2820
|
+
listMetrics: () => listMetrics,
|
|
2821
|
+
getSynthesisRun: () => getSynthesisRun,
|
|
2822
|
+
getProposal: () => getProposal,
|
|
2823
|
+
createSynthesisRun: () => createSynthesisRun,
|
|
2824
|
+
createProposal: () => createProposal,
|
|
2825
|
+
createMetric: () => createMetric
|
|
2826
|
+
});
|
|
2827
|
+
function parseRunRow(row) {
|
|
2828
|
+
return {
|
|
2829
|
+
id: row["id"],
|
|
2830
|
+
triggered_by: row["triggered_by"],
|
|
2831
|
+
project_id: row["project_id"] || null,
|
|
2832
|
+
agent_id: row["agent_id"] || null,
|
|
2833
|
+
corpus_size: row["corpus_size"],
|
|
2834
|
+
proposals_generated: row["proposals_generated"],
|
|
2835
|
+
proposals_accepted: row["proposals_accepted"],
|
|
2836
|
+
proposals_rejected: row["proposals_rejected"],
|
|
2837
|
+
status: row["status"],
|
|
2838
|
+
error: row["error"] || null,
|
|
2839
|
+
started_at: row["started_at"],
|
|
2840
|
+
completed_at: row["completed_at"] || null
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
function parseProposalRow(row) {
|
|
2844
|
+
return {
|
|
2845
|
+
id: row["id"],
|
|
2846
|
+
run_id: row["run_id"],
|
|
2847
|
+
proposal_type: row["proposal_type"],
|
|
2848
|
+
memory_ids: JSON.parse(row["memory_ids"] || "[]"),
|
|
2849
|
+
target_memory_id: row["target_memory_id"] || null,
|
|
2850
|
+
proposed_changes: JSON.parse(row["proposed_changes"] || "{}"),
|
|
2851
|
+
reasoning: row["reasoning"] || null,
|
|
2852
|
+
confidence: row["confidence"],
|
|
2853
|
+
status: row["status"],
|
|
2854
|
+
created_at: row["created_at"],
|
|
2855
|
+
executed_at: row["executed_at"] || null,
|
|
2856
|
+
rollback_data: row["rollback_data"] ? JSON.parse(row["rollback_data"]) : null
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
function parseMetricRow(row) {
|
|
2860
|
+
return {
|
|
2861
|
+
id: row["id"],
|
|
2862
|
+
run_id: row["run_id"],
|
|
2863
|
+
metric_type: row["metric_type"],
|
|
2864
|
+
value: row["value"],
|
|
2865
|
+
baseline: row["baseline"] != null ? row["baseline"] : null,
|
|
2866
|
+
created_at: row["created_at"]
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
function parseEventRow(row) {
|
|
2870
|
+
return {
|
|
2871
|
+
id: row["id"],
|
|
2872
|
+
event_type: row["event_type"],
|
|
2873
|
+
memory_id: row["memory_id"] || null,
|
|
2874
|
+
agent_id: row["agent_id"] || null,
|
|
2875
|
+
project_id: row["project_id"] || null,
|
|
2876
|
+
session_id: row["session_id"] || null,
|
|
2877
|
+
query: row["query"] || null,
|
|
2878
|
+
importance_at_time: row["importance_at_time"] != null ? row["importance_at_time"] : null,
|
|
2879
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
2880
|
+
created_at: row["created_at"]
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
function createSynthesisRun(input, db) {
|
|
2884
|
+
const d = db || getDatabase();
|
|
2885
|
+
const id = shortUuid();
|
|
2886
|
+
const timestamp = now();
|
|
2887
|
+
d.run(`INSERT INTO synthesis_runs (id, triggered_by, project_id, agent_id, corpus_size, proposals_generated, proposals_accepted, proposals_rejected, status, started_at)
|
|
2888
|
+
VALUES (?, ?, ?, ?, ?, 0, 0, 0, 'pending', ?)`, [
|
|
2889
|
+
id,
|
|
2890
|
+
input.triggered_by,
|
|
2891
|
+
input.project_id ?? null,
|
|
2892
|
+
input.agent_id ?? null,
|
|
2893
|
+
input.corpus_size ?? 0,
|
|
2894
|
+
timestamp
|
|
2895
|
+
]);
|
|
2896
|
+
return getSynthesisRun(id, d);
|
|
2897
|
+
}
|
|
2898
|
+
function getSynthesisRun(id, db) {
|
|
2899
|
+
const d = db || getDatabase();
|
|
2900
|
+
const row = d.query("SELECT * FROM synthesis_runs WHERE id = ?").get(id);
|
|
2901
|
+
if (!row)
|
|
2902
|
+
return null;
|
|
2903
|
+
return parseRunRow(row);
|
|
2904
|
+
}
|
|
2905
|
+
function listSynthesisRuns(filter, db) {
|
|
2906
|
+
const d = db || getDatabase();
|
|
2907
|
+
const conditions = [];
|
|
2908
|
+
const params = [];
|
|
2909
|
+
if (filter.project_id !== undefined) {
|
|
2910
|
+
if (filter.project_id === null) {
|
|
2911
|
+
conditions.push("project_id IS NULL");
|
|
2912
|
+
} else {
|
|
2913
|
+
conditions.push("project_id = ?");
|
|
2914
|
+
params.push(filter.project_id);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
if (filter.status) {
|
|
2918
|
+
conditions.push("status = ?");
|
|
2919
|
+
params.push(filter.status);
|
|
2920
|
+
}
|
|
2921
|
+
let sql = "SELECT * FROM synthesis_runs";
|
|
2922
|
+
if (conditions.length > 0) {
|
|
2923
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
2924
|
+
}
|
|
2925
|
+
sql += " ORDER BY started_at DESC";
|
|
2926
|
+
if (filter.limit) {
|
|
2927
|
+
sql += " LIMIT ?";
|
|
2928
|
+
params.push(filter.limit);
|
|
2929
|
+
}
|
|
2930
|
+
const rows = d.query(sql).all(...params);
|
|
2931
|
+
return rows.map(parseRunRow);
|
|
2932
|
+
}
|
|
2933
|
+
function updateSynthesisRun(id, updates, db) {
|
|
2934
|
+
const d = db || getDatabase();
|
|
2935
|
+
const sets = [];
|
|
2936
|
+
const params = [];
|
|
2937
|
+
if (updates.status !== undefined) {
|
|
2938
|
+
sets.push("status = ?");
|
|
2939
|
+
params.push(updates.status);
|
|
2940
|
+
}
|
|
2941
|
+
if (updates.error !== undefined) {
|
|
2942
|
+
sets.push("error = ?");
|
|
2943
|
+
params.push(updates.error);
|
|
2944
|
+
}
|
|
2945
|
+
if (updates.corpus_size !== undefined) {
|
|
2946
|
+
sets.push("corpus_size = ?");
|
|
2947
|
+
params.push(updates.corpus_size);
|
|
2948
|
+
}
|
|
2949
|
+
if (updates.proposals_generated !== undefined) {
|
|
2950
|
+
sets.push("proposals_generated = ?");
|
|
2951
|
+
params.push(updates.proposals_generated);
|
|
2952
|
+
}
|
|
2953
|
+
if (updates.proposals_accepted !== undefined) {
|
|
2954
|
+
sets.push("proposals_accepted = ?");
|
|
2955
|
+
params.push(updates.proposals_accepted);
|
|
2956
|
+
}
|
|
2957
|
+
if (updates.proposals_rejected !== undefined) {
|
|
2958
|
+
sets.push("proposals_rejected = ?");
|
|
2959
|
+
params.push(updates.proposals_rejected);
|
|
2960
|
+
}
|
|
2961
|
+
if (updates.completed_at !== undefined) {
|
|
2962
|
+
sets.push("completed_at = ?");
|
|
2963
|
+
params.push(updates.completed_at);
|
|
2964
|
+
}
|
|
2965
|
+
if (sets.length === 0)
|
|
2966
|
+
return getSynthesisRun(id, d);
|
|
2967
|
+
params.push(id);
|
|
2968
|
+
d.run(`UPDATE synthesis_runs SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
2969
|
+
return getSynthesisRun(id, d);
|
|
2970
|
+
}
|
|
2971
|
+
function createProposal(input, db) {
|
|
2972
|
+
const d = db || getDatabase();
|
|
2973
|
+
const id = shortUuid();
|
|
2974
|
+
const timestamp = now();
|
|
2975
|
+
d.run(`INSERT INTO synthesis_proposals (id, run_id, proposal_type, memory_ids, target_memory_id, proposed_changes, reasoning, confidence, status, created_at)
|
|
2976
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`, [
|
|
2977
|
+
id,
|
|
2978
|
+
input.run_id,
|
|
2979
|
+
input.proposal_type,
|
|
2980
|
+
JSON.stringify(input.memory_ids),
|
|
2981
|
+
input.target_memory_id ?? null,
|
|
2982
|
+
JSON.stringify(input.proposed_changes),
|
|
2983
|
+
input.reasoning ?? null,
|
|
2984
|
+
input.confidence,
|
|
2985
|
+
timestamp
|
|
2986
|
+
]);
|
|
2987
|
+
return getProposal(id, d);
|
|
2988
|
+
}
|
|
2989
|
+
function getProposal(id, db) {
|
|
2990
|
+
const d = db || getDatabase();
|
|
2991
|
+
const row = d.query("SELECT * FROM synthesis_proposals WHERE id = ?").get(id);
|
|
2992
|
+
if (!row)
|
|
2993
|
+
return null;
|
|
2994
|
+
return parseProposalRow(row);
|
|
2995
|
+
}
|
|
2996
|
+
function listProposals(run_id, filter, db) {
|
|
2997
|
+
const d = db || getDatabase();
|
|
2998
|
+
const params = [run_id];
|
|
2999
|
+
let sql = "SELECT * FROM synthesis_proposals WHERE run_id = ?";
|
|
3000
|
+
if (filter?.status) {
|
|
3001
|
+
sql += " AND status = ?";
|
|
3002
|
+
params.push(filter.status);
|
|
3003
|
+
}
|
|
3004
|
+
sql += " ORDER BY created_at ASC";
|
|
3005
|
+
const rows = d.query(sql).all(...params);
|
|
3006
|
+
return rows.map(parseProposalRow);
|
|
3007
|
+
}
|
|
3008
|
+
function updateProposal(id, updates, db) {
|
|
3009
|
+
const d = db || getDatabase();
|
|
3010
|
+
const sets = [];
|
|
3011
|
+
const params = [];
|
|
3012
|
+
if (updates.status !== undefined) {
|
|
3013
|
+
sets.push("status = ?");
|
|
3014
|
+
params.push(updates.status);
|
|
3015
|
+
}
|
|
3016
|
+
if (updates.executed_at !== undefined) {
|
|
3017
|
+
sets.push("executed_at = ?");
|
|
3018
|
+
params.push(updates.executed_at);
|
|
3019
|
+
}
|
|
3020
|
+
if (updates.rollback_data !== undefined) {
|
|
3021
|
+
sets.push("rollback_data = ?");
|
|
3022
|
+
params.push(JSON.stringify(updates.rollback_data));
|
|
3023
|
+
}
|
|
3024
|
+
if (sets.length === 0)
|
|
3025
|
+
return getProposal(id, d);
|
|
3026
|
+
params.push(id);
|
|
3027
|
+
d.run(`UPDATE synthesis_proposals SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
3028
|
+
return getProposal(id, d);
|
|
3029
|
+
}
|
|
3030
|
+
function createMetric(input, db) {
|
|
3031
|
+
const d = db || getDatabase();
|
|
3032
|
+
const id = shortUuid();
|
|
3033
|
+
const timestamp = now();
|
|
3034
|
+
d.run(`INSERT INTO synthesis_metrics (id, run_id, metric_type, value, baseline, created_at)
|
|
3035
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [id, input.run_id, input.metric_type, input.value, input.baseline ?? null, timestamp]);
|
|
3036
|
+
return { id, run_id: input.run_id, metric_type: input.metric_type, value: input.value, baseline: input.baseline ?? null, created_at: timestamp };
|
|
3037
|
+
}
|
|
3038
|
+
function listMetrics(run_id, db) {
|
|
3039
|
+
const d = db || getDatabase();
|
|
3040
|
+
const rows = d.query("SELECT * FROM synthesis_metrics WHERE run_id = ? ORDER BY created_at ASC").all(run_id);
|
|
3041
|
+
return rows.map(parseMetricRow);
|
|
3042
|
+
}
|
|
3043
|
+
function recordSynthesisEvent(input, db) {
|
|
3044
|
+
try {
|
|
3045
|
+
const d = db || getDatabase();
|
|
3046
|
+
const id = shortUuid();
|
|
3047
|
+
const timestamp = now();
|
|
3048
|
+
d.run(`INSERT INTO synthesis_events (id, event_type, memory_id, agent_id, project_id, session_id, query, importance_at_time, metadata, created_at)
|
|
3049
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3050
|
+
id,
|
|
3051
|
+
input.event_type,
|
|
3052
|
+
input.memory_id ?? null,
|
|
3053
|
+
input.agent_id ?? null,
|
|
3054
|
+
input.project_id ?? null,
|
|
3055
|
+
input.session_id ?? null,
|
|
3056
|
+
input.query ?? null,
|
|
3057
|
+
input.importance_at_time ?? null,
|
|
3058
|
+
JSON.stringify(input.metadata ?? {}),
|
|
3059
|
+
timestamp
|
|
3060
|
+
]);
|
|
3061
|
+
} catch {}
|
|
3062
|
+
}
|
|
3063
|
+
function listSynthesisEvents(filter, db) {
|
|
3064
|
+
const d = db || getDatabase();
|
|
3065
|
+
const conditions = [];
|
|
3066
|
+
const params = [];
|
|
3067
|
+
if (filter.memory_id) {
|
|
3068
|
+
conditions.push("memory_id = ?");
|
|
3069
|
+
params.push(filter.memory_id);
|
|
3070
|
+
}
|
|
3071
|
+
if (filter.project_id) {
|
|
3072
|
+
conditions.push("project_id = ?");
|
|
3073
|
+
params.push(filter.project_id);
|
|
3074
|
+
}
|
|
3075
|
+
if (filter.event_type) {
|
|
3076
|
+
conditions.push("event_type = ?");
|
|
3077
|
+
params.push(filter.event_type);
|
|
3078
|
+
}
|
|
3079
|
+
if (filter.since) {
|
|
3080
|
+
conditions.push("created_at >= ?");
|
|
3081
|
+
params.push(filter.since);
|
|
3082
|
+
}
|
|
3083
|
+
let sql = "SELECT * FROM synthesis_events";
|
|
3084
|
+
if (conditions.length > 0) {
|
|
3085
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
3086
|
+
}
|
|
3087
|
+
sql += " ORDER BY created_at DESC";
|
|
3088
|
+
if (filter.limit) {
|
|
3089
|
+
sql += " LIMIT ?";
|
|
3090
|
+
params.push(filter.limit);
|
|
3091
|
+
}
|
|
3092
|
+
const rows = d.query(sql).all(...params);
|
|
3093
|
+
return rows.map(parseEventRow);
|
|
3094
|
+
}
|
|
3095
|
+
var init_synthesis = __esm(() => {
|
|
3096
|
+
init_database();
|
|
3097
|
+
});
|
|
3098
|
+
|
|
2719
3099
|
// src/lib/built-in-hooks.ts
|
|
2720
3100
|
var exports_built_in_hooks = {};
|
|
2721
3101
|
__export(exports_built_in_hooks, {
|
|
@@ -2816,6 +3196,41 @@ var init_built_in_hooks = __esm(() => {
|
|
|
2816
3196
|
} catch {}
|
|
2817
3197
|
}
|
|
2818
3198
|
});
|
|
3199
|
+
hookRegistry.register({
|
|
3200
|
+
type: "PostMemorySave",
|
|
3201
|
+
blocking: false,
|
|
3202
|
+
builtin: true,
|
|
3203
|
+
priority: 200,
|
|
3204
|
+
description: "Record memory save event for synthesis analytics",
|
|
3205
|
+
handler: async (ctx) => {
|
|
3206
|
+
const { recordSynthesisEvent: recordSynthesisEvent2 } = await Promise.resolve().then(() => (init_synthesis(), exports_synthesis));
|
|
3207
|
+
recordSynthesisEvent2({
|
|
3208
|
+
event_type: "saved",
|
|
3209
|
+
memory_id: ctx.memory.id,
|
|
3210
|
+
agent_id: ctx.agentId,
|
|
3211
|
+
project_id: ctx.projectId,
|
|
3212
|
+
session_id: ctx.sessionId,
|
|
3213
|
+
importance_at_time: ctx.memory.importance
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
});
|
|
3217
|
+
hookRegistry.register({
|
|
3218
|
+
type: "PostMemoryInject",
|
|
3219
|
+
blocking: false,
|
|
3220
|
+
builtin: true,
|
|
3221
|
+
priority: 200,
|
|
3222
|
+
description: "Record injection event for synthesis analytics",
|
|
3223
|
+
handler: async (ctx) => {
|
|
3224
|
+
const { recordSynthesisEvent: recordSynthesisEvent2 } = await Promise.resolve().then(() => (init_synthesis(), exports_synthesis));
|
|
3225
|
+
recordSynthesisEvent2({
|
|
3226
|
+
event_type: "injected",
|
|
3227
|
+
agent_id: ctx.agentId,
|
|
3228
|
+
project_id: ctx.projectId,
|
|
3229
|
+
session_id: ctx.sessionId,
|
|
3230
|
+
metadata: { count: ctx.memoriesCount, format: ctx.format }
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
});
|
|
2819
3234
|
});
|
|
2820
3235
|
|
|
2821
3236
|
// src/mcp/index.ts
|
|
@@ -7198,40 +7613,1289 @@ var FORMAT_UNITS = [
|
|
|
7198
7613
|
init_hooks();
|
|
7199
7614
|
init_built_in_hooks();
|
|
7200
7615
|
init_webhook_hooks();
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7616
|
+
|
|
7617
|
+
// src/lib/synthesis/index.ts
|
|
7618
|
+
init_database();
|
|
7619
|
+
init_synthesis();
|
|
7620
|
+
|
|
7621
|
+
// src/lib/synthesis/corpus-builder.ts
|
|
7622
|
+
init_database();
|
|
7623
|
+
init_memories();
|
|
7624
|
+
init_synthesis();
|
|
7625
|
+
function extractTerms(text) {
|
|
7626
|
+
const stopWords = new Set([
|
|
7627
|
+
"a",
|
|
7628
|
+
"an",
|
|
7629
|
+
"the",
|
|
7630
|
+
"is",
|
|
7631
|
+
"it",
|
|
7632
|
+
"in",
|
|
7633
|
+
"of",
|
|
7634
|
+
"for",
|
|
7635
|
+
"to",
|
|
7636
|
+
"and",
|
|
7637
|
+
"or",
|
|
7638
|
+
"but",
|
|
7639
|
+
"not",
|
|
7640
|
+
"with",
|
|
7641
|
+
"this",
|
|
7642
|
+
"that",
|
|
7643
|
+
"are",
|
|
7644
|
+
"was",
|
|
7645
|
+
"be",
|
|
7646
|
+
"by",
|
|
7647
|
+
"at",
|
|
7648
|
+
"as",
|
|
7649
|
+
"on",
|
|
7650
|
+
"has",
|
|
7651
|
+
"have",
|
|
7652
|
+
"had",
|
|
7653
|
+
"do",
|
|
7654
|
+
"did",
|
|
7655
|
+
"does",
|
|
7656
|
+
"will",
|
|
7657
|
+
"would",
|
|
7658
|
+
"can",
|
|
7659
|
+
"could",
|
|
7660
|
+
"should",
|
|
7661
|
+
"may",
|
|
7662
|
+
"might",
|
|
7663
|
+
"shall",
|
|
7664
|
+
"from",
|
|
7665
|
+
"into",
|
|
7666
|
+
"then",
|
|
7667
|
+
"than",
|
|
7668
|
+
"so",
|
|
7669
|
+
"if",
|
|
7670
|
+
"up",
|
|
7671
|
+
"out",
|
|
7672
|
+
"about",
|
|
7673
|
+
"its",
|
|
7674
|
+
"my",
|
|
7675
|
+
"we",
|
|
7676
|
+
"i",
|
|
7677
|
+
"you"
|
|
7678
|
+
]);
|
|
7679
|
+
return new Set(text.toLowerCase().replace(/[^a-z0-9\s\-_]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w)));
|
|
7218
7680
|
}
|
|
7219
|
-
function
|
|
7220
|
-
if (
|
|
7221
|
-
return
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
return `Duplicate: ${error.message}`;
|
|
7226
|
-
if (error instanceof InvalidScopeError)
|
|
7227
|
-
return `Invalid scope: ${error.message}`;
|
|
7228
|
-
if (error instanceof Error)
|
|
7229
|
-
return error.message;
|
|
7230
|
-
return String(error);
|
|
7681
|
+
function jaccardSimilarity(a, b) {
|
|
7682
|
+
if (a.size === 0 && b.size === 0)
|
|
7683
|
+
return 0;
|
|
7684
|
+
const intersection = new Set([...a].filter((x) => b.has(x)));
|
|
7685
|
+
const union = new Set([...a, ...b]);
|
|
7686
|
+
return intersection.size / union.size;
|
|
7231
7687
|
}
|
|
7232
|
-
function
|
|
7233
|
-
|
|
7234
|
-
|
|
7688
|
+
function isStale(memory) {
|
|
7689
|
+
if (memory.status !== "active")
|
|
7690
|
+
return false;
|
|
7691
|
+
if (memory.importance >= 7)
|
|
7692
|
+
return false;
|
|
7693
|
+
if (!memory.accessed_at)
|
|
7694
|
+
return true;
|
|
7695
|
+
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
7696
|
+
return new Date(memory.accessed_at).getTime() < thirtyDaysAgo;
|
|
7697
|
+
}
|
|
7698
|
+
async function buildCorpus(options) {
|
|
7699
|
+
const d = options.db || getDatabase();
|
|
7700
|
+
const limit = options.limit ?? 500;
|
|
7701
|
+
const projectId = options.projectId ?? null;
|
|
7702
|
+
const memories = listMemories({
|
|
7703
|
+
status: "active",
|
|
7704
|
+
project_id: options.projectId,
|
|
7705
|
+
agent_id: options.agentId,
|
|
7706
|
+
limit
|
|
7707
|
+
}, d);
|
|
7708
|
+
const recallEvents = listSynthesisEvents({
|
|
7709
|
+
event_type: "recalled",
|
|
7710
|
+
project_id: options.projectId
|
|
7711
|
+
}, d);
|
|
7712
|
+
const recallCounts = new Map;
|
|
7713
|
+
const lastRecalledMap = new Map;
|
|
7714
|
+
for (const event of recallEvents) {
|
|
7715
|
+
if (!event.memory_id)
|
|
7716
|
+
continue;
|
|
7717
|
+
recallCounts.set(event.memory_id, (recallCounts.get(event.memory_id) ?? 0) + 1);
|
|
7718
|
+
const existing = lastRecalledMap.get(event.memory_id);
|
|
7719
|
+
if (!existing || event.created_at > existing) {
|
|
7720
|
+
lastRecalledMap.set(event.memory_id, event.created_at);
|
|
7721
|
+
}
|
|
7722
|
+
}
|
|
7723
|
+
const searchEvents = listSynthesisEvents({
|
|
7724
|
+
event_type: "searched",
|
|
7725
|
+
project_id: options.projectId
|
|
7726
|
+
}, d);
|
|
7727
|
+
const searchHits = new Map;
|
|
7728
|
+
for (const event of searchEvents) {
|
|
7729
|
+
if (!event.memory_id)
|
|
7730
|
+
continue;
|
|
7731
|
+
searchHits.set(event.memory_id, (searchHits.get(event.memory_id) ?? 0) + 1);
|
|
7732
|
+
}
|
|
7733
|
+
const termSets = new Map;
|
|
7734
|
+
for (const m of memories) {
|
|
7735
|
+
termSets.set(m.id, extractTerms(`${m.key} ${m.value} ${m.summary ?? ""}`));
|
|
7736
|
+
}
|
|
7737
|
+
const duplicateCandidates = [];
|
|
7738
|
+
const similarityThreshold = 0.5;
|
|
7739
|
+
const similarIds = new Map;
|
|
7740
|
+
for (let i = 0;i < memories.length; i++) {
|
|
7741
|
+
const a = memories[i];
|
|
7742
|
+
const termsA = termSets.get(a.id);
|
|
7743
|
+
const aSimIds = [];
|
|
7744
|
+
for (let j = i + 1;j < memories.length; j++) {
|
|
7745
|
+
const b = memories[j];
|
|
7746
|
+
const termsB = termSets.get(b.id);
|
|
7747
|
+
const sim = jaccardSimilarity(termsA, termsB);
|
|
7748
|
+
if (sim >= similarityThreshold) {
|
|
7749
|
+
duplicateCandidates.push({ a, b, similarity: sim });
|
|
7750
|
+
aSimIds.push(b.id);
|
|
7751
|
+
const bSimIds = similarIds.get(b.id) ?? [];
|
|
7752
|
+
bSimIds.push(a.id);
|
|
7753
|
+
similarIds.set(b.id, bSimIds);
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
if (aSimIds.length > 0) {
|
|
7757
|
+
const existing = similarIds.get(a.id) ?? [];
|
|
7758
|
+
similarIds.set(a.id, [...existing, ...aSimIds]);
|
|
7759
|
+
}
|
|
7760
|
+
}
|
|
7761
|
+
duplicateCandidates.sort((a, b) => b.similarity - a.similarity);
|
|
7762
|
+
const items = memories.map((memory) => ({
|
|
7763
|
+
memory,
|
|
7764
|
+
recallCount: recallCounts.get(memory.id) ?? 0,
|
|
7765
|
+
lastRecalled: lastRecalledMap.get(memory.id) ?? null,
|
|
7766
|
+
searchHits: searchHits.get(memory.id) ?? 0,
|
|
7767
|
+
similarMemoryIds: similarIds.get(memory.id) ?? []
|
|
7768
|
+
}));
|
|
7769
|
+
const staleMemories = memories.filter(isStale);
|
|
7770
|
+
const lowImportanceHighRecall = memories.filter((m) => m.importance < 5 && (recallCounts.get(m.id) ?? 0) > 3);
|
|
7771
|
+
const highImportanceLowRecall = memories.filter((m) => m.importance > 7 && (recallCounts.get(m.id) ?? 0) === 0);
|
|
7772
|
+
return {
|
|
7773
|
+
projectId,
|
|
7774
|
+
totalMemories: memories.length,
|
|
7775
|
+
items,
|
|
7776
|
+
staleMemories,
|
|
7777
|
+
duplicateCandidates,
|
|
7778
|
+
lowImportanceHighRecall,
|
|
7779
|
+
highImportanceLowRecall,
|
|
7780
|
+
generatedAt: now()
|
|
7781
|
+
};
|
|
7782
|
+
}
|
|
7783
|
+
|
|
7784
|
+
// src/lib/synthesis/llm-analyzer.ts
|
|
7785
|
+
init_registry();
|
|
7786
|
+
var SYNTHESIS_SYSTEM_PROMPT = "You are a memory synthesizer. Analyze this agent's memory corpus and propose consolidations to improve quality and reduce redundancy. Return ONLY a JSON array of proposal objects.";
|
|
7787
|
+
function buildCorpusPrompt(corpus, maxProposals) {
|
|
7788
|
+
const lines = [];
|
|
7789
|
+
lines.push(`## Memory Corpus Analysis`);
|
|
7790
|
+
lines.push(`Project: ${corpus.projectId ?? "(global)"}`);
|
|
7791
|
+
lines.push(`Total active memories: ${corpus.totalMemories}`);
|
|
7792
|
+
lines.push(`Analysis generated at: ${corpus.generatedAt}`);
|
|
7793
|
+
lines.push("");
|
|
7794
|
+
if (corpus.staleMemories.length > 0) {
|
|
7795
|
+
lines.push(`## Stale Memories (not accessed in 30+ days, importance < 7)`);
|
|
7796
|
+
lines.push(`Count: ${corpus.staleMemories.length}`);
|
|
7797
|
+
for (const m of corpus.staleMemories.slice(0, 30)) {
|
|
7798
|
+
const accessed = m.accessed_at ?? "never";
|
|
7799
|
+
lines.push(`- id:${m.id} key="${m.key}" importance=${m.importance} lastAccessed=${accessed}`);
|
|
7800
|
+
lines.push(` value: ${m.value.slice(0, 120)}`);
|
|
7801
|
+
}
|
|
7802
|
+
lines.push("");
|
|
7803
|
+
}
|
|
7804
|
+
if (corpus.duplicateCandidates.length > 0) {
|
|
7805
|
+
lines.push(`## Potential Duplicates (term overlap \u2265 50%)`);
|
|
7806
|
+
lines.push(`Count: ${corpus.duplicateCandidates.length} pairs`);
|
|
7807
|
+
for (const { a, b, similarity } of corpus.duplicateCandidates.slice(0, 20)) {
|
|
7808
|
+
lines.push(`- similarity=${similarity.toFixed(2)}`);
|
|
7809
|
+
lines.push(` A id:${a.id} key="${a.key}" importance=${a.importance}: ${a.value.slice(0, 80)}`);
|
|
7810
|
+
lines.push(` B id:${b.id} key="${b.key}" importance=${b.importance}: ${b.value.slice(0, 80)}`);
|
|
7811
|
+
}
|
|
7812
|
+
lines.push("");
|
|
7813
|
+
}
|
|
7814
|
+
if (corpus.lowImportanceHighRecall.length > 0) {
|
|
7815
|
+
lines.push(`## Low Importance But High Recall (candidates for importance promotion)`);
|
|
7816
|
+
for (const m of corpus.lowImportanceHighRecall.slice(0, 20)) {
|
|
7817
|
+
lines.push(`- id:${m.id} key="${m.key}" importance=${m.importance} recall_count=${m.access_count}`);
|
|
7818
|
+
}
|
|
7819
|
+
lines.push("");
|
|
7820
|
+
}
|
|
7821
|
+
if (corpus.highImportanceLowRecall.length > 0) {
|
|
7822
|
+
lines.push(`## High Importance But Never Recalled (may need value update)`);
|
|
7823
|
+
for (const m of corpus.highImportanceLowRecall.slice(0, 20)) {
|
|
7824
|
+
lines.push(`- id:${m.id} key="${m.key}" importance=${m.importance}: ${m.value.slice(0, 80)}`);
|
|
7825
|
+
}
|
|
7826
|
+
lines.push("");
|
|
7827
|
+
}
|
|
7828
|
+
lines.push(`## Instructions`);
|
|
7829
|
+
lines.push(`Generate up to ${maxProposals} proposals as a JSON array. Each proposal must have:`);
|
|
7830
|
+
lines.push(` - type: "merge" | "archive" | "promote" | "update_value" | "add_tag" | "remove_duplicate"`);
|
|
7831
|
+
lines.push(` - memory_ids: string[] (IDs this proposal acts on)`);
|
|
7832
|
+
lines.push(` - target_memory_id: string (optional \u2014 used for merge/remove_duplicate)`);
|
|
7833
|
+
lines.push(` - proposed_changes: object (e.g. {new_importance:8} or {new_value:"..."} or {tags:["x"]})`);
|
|
7834
|
+
lines.push(` - reasoning: string (one sentence)`);
|
|
7835
|
+
lines.push(` - confidence: number 0.0-1.0`);
|
|
7836
|
+
lines.push("");
|
|
7837
|
+
lines.push(`Return ONLY the JSON array. No markdown, no explanation.`);
|
|
7838
|
+
return lines.join(`
|
|
7839
|
+
`);
|
|
7840
|
+
}
|
|
7841
|
+
async function analyzeCorpus(corpus, options) {
|
|
7842
|
+
const startMs = Date.now();
|
|
7843
|
+
const maxProposals = options?.maxProposals ?? 20;
|
|
7844
|
+
const empty = {
|
|
7845
|
+
proposals: [],
|
|
7846
|
+
summary: "No LLM provider available.",
|
|
7847
|
+
analysisDurationMs: 0
|
|
7848
|
+
};
|
|
7849
|
+
let provider = options?.provider ? providerRegistry.getProvider(options.provider) : providerRegistry.getAvailable();
|
|
7850
|
+
if (!provider) {
|
|
7851
|
+
return { ...empty, analysisDurationMs: Date.now() - startMs };
|
|
7852
|
+
}
|
|
7853
|
+
const userPrompt = buildCorpusPrompt(corpus, maxProposals);
|
|
7854
|
+
try {
|
|
7855
|
+
const rawResponse = await callProviderRaw(provider, SYNTHESIS_SYSTEM_PROMPT, userPrompt);
|
|
7856
|
+
if (!rawResponse) {
|
|
7857
|
+
return { ...empty, analysisDurationMs: Date.now() - startMs };
|
|
7858
|
+
}
|
|
7859
|
+
const parsed = parseProposalsResponse(rawResponse);
|
|
7860
|
+
const analysisDurationMs = Date.now() - startMs;
|
|
7861
|
+
const validProposals = parsed.slice(0, maxProposals);
|
|
7862
|
+
return {
|
|
7863
|
+
proposals: validProposals,
|
|
7864
|
+
summary: buildSummary(corpus, validProposals.length),
|
|
7865
|
+
analysisDurationMs
|
|
7866
|
+
};
|
|
7867
|
+
} catch {
|
|
7868
|
+
return { ...empty, analysisDurationMs: Date.now() - startMs };
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
async function callProviderRaw(provider, systemPrompt, userPrompt) {
|
|
7872
|
+
if (!provider)
|
|
7873
|
+
return null;
|
|
7874
|
+
const { config, name } = provider;
|
|
7875
|
+
if (!config.apiKey)
|
|
7876
|
+
return null;
|
|
7877
|
+
const timeoutMs = config.timeoutMs ?? 30000;
|
|
7878
|
+
try {
|
|
7879
|
+
if (name === "anthropic") {
|
|
7880
|
+
return await callAnthropic(config.apiKey, config.model, systemPrompt, userPrompt, timeoutMs);
|
|
7881
|
+
} else if (name === "openai" || name === "cerebras" || name === "grok") {
|
|
7882
|
+
return await callOpenAICompat(name, config.apiKey, config.model, systemPrompt, userPrompt, timeoutMs);
|
|
7883
|
+
}
|
|
7884
|
+
} catch {}
|
|
7885
|
+
return null;
|
|
7886
|
+
}
|
|
7887
|
+
async function callAnthropic(apiKey, model, systemPrompt, userPrompt, timeoutMs) {
|
|
7888
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
7889
|
+
method: "POST",
|
|
7890
|
+
headers: {
|
|
7891
|
+
"x-api-key": apiKey,
|
|
7892
|
+
"anthropic-version": "2023-06-01",
|
|
7893
|
+
"content-type": "application/json"
|
|
7894
|
+
},
|
|
7895
|
+
body: JSON.stringify({
|
|
7896
|
+
model,
|
|
7897
|
+
max_tokens: 2048,
|
|
7898
|
+
system: systemPrompt,
|
|
7899
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
7900
|
+
}),
|
|
7901
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
7902
|
+
});
|
|
7903
|
+
if (!response.ok)
|
|
7904
|
+
return null;
|
|
7905
|
+
const data = await response.json();
|
|
7906
|
+
return data.content?.[0]?.text ?? null;
|
|
7907
|
+
}
|
|
7908
|
+
function getBaseUrlForProvider(providerName) {
|
|
7909
|
+
switch (providerName) {
|
|
7910
|
+
case "openai":
|
|
7911
|
+
return "https://api.openai.com/v1";
|
|
7912
|
+
case "cerebras":
|
|
7913
|
+
return "https://api.cerebras.ai/v1";
|
|
7914
|
+
case "grok":
|
|
7915
|
+
return "https://api.x.ai/v1";
|
|
7916
|
+
default:
|
|
7917
|
+
return "https://api.openai.com/v1";
|
|
7918
|
+
}
|
|
7919
|
+
}
|
|
7920
|
+
async function callOpenAICompat(providerName, apiKey, model, systemPrompt, userPrompt, timeoutMs) {
|
|
7921
|
+
const baseUrl = getBaseUrlForProvider(providerName);
|
|
7922
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
7923
|
+
method: "POST",
|
|
7924
|
+
headers: {
|
|
7925
|
+
authorization: `Bearer ${apiKey}`,
|
|
7926
|
+
"content-type": "application/json"
|
|
7927
|
+
},
|
|
7928
|
+
body: JSON.stringify({
|
|
7929
|
+
model,
|
|
7930
|
+
max_tokens: 2048,
|
|
7931
|
+
temperature: 0,
|
|
7932
|
+
messages: [
|
|
7933
|
+
{ role: "system", content: systemPrompt },
|
|
7934
|
+
{ role: "user", content: userPrompt }
|
|
7935
|
+
]
|
|
7936
|
+
}),
|
|
7937
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
7938
|
+
});
|
|
7939
|
+
if (!response.ok)
|
|
7940
|
+
return null;
|
|
7941
|
+
const data = await response.json();
|
|
7942
|
+
return data.choices?.[0]?.message?.content ?? null;
|
|
7943
|
+
}
|
|
7944
|
+
function parseProposalsResponse(raw) {
|
|
7945
|
+
try {
|
|
7946
|
+
const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim();
|
|
7947
|
+
const parsed = JSON.parse(cleaned);
|
|
7948
|
+
if (!Array.isArray(parsed))
|
|
7949
|
+
return [];
|
|
7950
|
+
const validTypes = new Set([
|
|
7951
|
+
"merge",
|
|
7952
|
+
"archive",
|
|
7953
|
+
"promote",
|
|
7954
|
+
"update_value",
|
|
7955
|
+
"add_tag",
|
|
7956
|
+
"remove_duplicate"
|
|
7957
|
+
]);
|
|
7958
|
+
return parsed.filter((item) => {
|
|
7959
|
+
if (!item || typeof item !== "object")
|
|
7960
|
+
return false;
|
|
7961
|
+
const p = item;
|
|
7962
|
+
return typeof p["type"] === "string" && validTypes.has(p["type"]) && Array.isArray(p["memory_ids"]) && typeof p["proposed_changes"] === "object" && typeof p["reasoning"] === "string" && typeof p["confidence"] === "number";
|
|
7963
|
+
}).map((p) => ({
|
|
7964
|
+
type: p.type,
|
|
7965
|
+
memory_ids: p.memory_ids.filter((id) => typeof id === "string"),
|
|
7966
|
+
target_memory_id: typeof p.target_memory_id === "string" ? p.target_memory_id : undefined,
|
|
7967
|
+
proposed_changes: p.proposed_changes ?? {},
|
|
7968
|
+
reasoning: p.reasoning,
|
|
7969
|
+
confidence: Math.max(0, Math.min(1, p.confidence))
|
|
7970
|
+
}));
|
|
7971
|
+
} catch {
|
|
7972
|
+
return [];
|
|
7973
|
+
}
|
|
7974
|
+
}
|
|
7975
|
+
function buildSummary(corpus, proposalCount) {
|
|
7976
|
+
return `Analyzed ${corpus.totalMemories} memories. ` + `Found ${corpus.staleMemories.length} stale, ` + `${corpus.duplicateCandidates.length} duplicate pairs. ` + `Generated ${proposalCount} proposals.`;
|
|
7977
|
+
}
|
|
7978
|
+
|
|
7979
|
+
// src/lib/synthesis/validator.ts
|
|
7980
|
+
var DEFAULT_SAFETY_CONFIG = {
|
|
7981
|
+
maxArchivePercent: 20,
|
|
7982
|
+
maxMergePercent: 30,
|
|
7983
|
+
minConfidence: 0.6,
|
|
7984
|
+
protectPinned: true,
|
|
7985
|
+
protectHighImportance: 9
|
|
7986
|
+
};
|
|
7987
|
+
function validateProposals(proposals, corpus, config) {
|
|
7988
|
+
const cfg = { ...DEFAULT_SAFETY_CONFIG, ...config };
|
|
7989
|
+
const memoryMap = new Map(corpus.items.map((item) => [item.memory.id, item.memory]));
|
|
7990
|
+
const rejectedProposals = [];
|
|
7991
|
+
const warnings = [];
|
|
7992
|
+
let archiveCount = 0;
|
|
7993
|
+
let mergeCount = 0;
|
|
7994
|
+
for (const proposal of proposals) {
|
|
7995
|
+
if (proposal.type === "archive" || proposal.type === "remove_duplicate") {
|
|
7996
|
+
archiveCount++;
|
|
7997
|
+
}
|
|
7998
|
+
if (proposal.type === "merge") {
|
|
7999
|
+
mergeCount++;
|
|
8000
|
+
}
|
|
8001
|
+
}
|
|
8002
|
+
const corpusSize = corpus.totalMemories;
|
|
8003
|
+
const archivePercent = corpusSize > 0 ? archiveCount / corpusSize * 100 : 0;
|
|
8004
|
+
const mergePercent = corpusSize > 0 ? mergeCount / corpusSize * 100 : 0;
|
|
8005
|
+
if (archivePercent > cfg.maxArchivePercent) {
|
|
8006
|
+
warnings.push(`Archive proposals (${archiveCount}) exceed ${cfg.maxArchivePercent}% of corpus (${archivePercent.toFixed(1)}%). Some will be rejected.`);
|
|
8007
|
+
}
|
|
8008
|
+
if (mergePercent > cfg.maxMergePercent) {
|
|
8009
|
+
warnings.push(`Merge proposals (${mergeCount}) exceed ${cfg.maxMergePercent}% of corpus (${mergePercent.toFixed(1)}%). Some will be rejected.`);
|
|
8010
|
+
}
|
|
8011
|
+
let remainingArchiveSlots = Math.floor(cfg.maxArchivePercent / 100 * corpusSize);
|
|
8012
|
+
let remainingMergeSlots = Math.floor(cfg.maxMergePercent / 100 * corpusSize);
|
|
8013
|
+
for (let i = 0;i < proposals.length; i++) {
|
|
8014
|
+
const proposal = proposals[i];
|
|
8015
|
+
const proposalRef = `proposal[${i}]`;
|
|
8016
|
+
if (proposal.confidence < cfg.minConfidence) {
|
|
8017
|
+
rejectedProposals.push({
|
|
8018
|
+
proposalId: proposalRef,
|
|
8019
|
+
reason: `Confidence ${proposal.confidence.toFixed(2)} below minimum ${cfg.minConfidence}`
|
|
8020
|
+
});
|
|
8021
|
+
continue;
|
|
8022
|
+
}
|
|
8023
|
+
const missingIds = proposal.memory_ids.filter((id) => !memoryMap.has(id));
|
|
8024
|
+
if (missingIds.length > 0) {
|
|
8025
|
+
rejectedProposals.push({
|
|
8026
|
+
proposalId: proposalRef,
|
|
8027
|
+
reason: `References unknown memory IDs: ${missingIds.join(", ")}`
|
|
8028
|
+
});
|
|
8029
|
+
continue;
|
|
8030
|
+
}
|
|
8031
|
+
if (cfg.protectPinned) {
|
|
8032
|
+
const pinnedIds = proposal.memory_ids.filter((id) => {
|
|
8033
|
+
const m = memoryMap.get(id);
|
|
8034
|
+
return m?.pinned === true;
|
|
8035
|
+
});
|
|
8036
|
+
if (pinnedIds.length > 0) {
|
|
8037
|
+
rejectedProposals.push({
|
|
8038
|
+
proposalId: proposalRef,
|
|
8039
|
+
reason: `Attempts to modify pinned memories: ${pinnedIds.join(", ")}`
|
|
8040
|
+
});
|
|
8041
|
+
continue;
|
|
8042
|
+
}
|
|
8043
|
+
}
|
|
8044
|
+
if (proposal.type === "archive" || proposal.type === "remove_duplicate" || proposal.type === "merge") {
|
|
8045
|
+
const highImportanceIds = proposal.memory_ids.filter((id) => {
|
|
8046
|
+
const m = memoryMap.get(id);
|
|
8047
|
+
return m && m.importance >= cfg.protectHighImportance;
|
|
8048
|
+
});
|
|
8049
|
+
if (highImportanceIds.length > 0) {
|
|
8050
|
+
rejectedProposals.push({
|
|
8051
|
+
proposalId: proposalRef,
|
|
8052
|
+
reason: `Attempts to archive/merge memories with importance \u2265 ${cfg.protectHighImportance}: ${highImportanceIds.join(", ")}`
|
|
8053
|
+
});
|
|
8054
|
+
continue;
|
|
8055
|
+
}
|
|
8056
|
+
}
|
|
8057
|
+
if (proposal.type === "archive" || proposal.type === "remove_duplicate") {
|
|
8058
|
+
if (remainingArchiveSlots <= 0) {
|
|
8059
|
+
rejectedProposals.push({
|
|
8060
|
+
proposalId: proposalRef,
|
|
8061
|
+
reason: `Archive quota exhausted (max ${cfg.maxArchivePercent}% of corpus)`
|
|
8062
|
+
});
|
|
8063
|
+
continue;
|
|
8064
|
+
}
|
|
8065
|
+
remainingArchiveSlots--;
|
|
8066
|
+
}
|
|
8067
|
+
if (proposal.type === "merge") {
|
|
8068
|
+
if (remainingMergeSlots <= 0) {
|
|
8069
|
+
rejectedProposals.push({
|
|
8070
|
+
proposalId: proposalRef,
|
|
8071
|
+
reason: `Merge quota exhausted (max ${cfg.maxMergePercent}% of corpus)`
|
|
8072
|
+
});
|
|
8073
|
+
continue;
|
|
8074
|
+
}
|
|
8075
|
+
remainingMergeSlots--;
|
|
8076
|
+
}
|
|
8077
|
+
if (proposal.target_memory_id && !memoryMap.has(proposal.target_memory_id)) {
|
|
8078
|
+
rejectedProposals.push({
|
|
8079
|
+
proposalId: proposalRef,
|
|
8080
|
+
reason: `target_memory_id "${proposal.target_memory_id}" does not exist in corpus`
|
|
8081
|
+
});
|
|
8082
|
+
continue;
|
|
8083
|
+
}
|
|
8084
|
+
}
|
|
8085
|
+
return {
|
|
8086
|
+
valid: rejectedProposals.length === 0,
|
|
8087
|
+
rejectedProposals,
|
|
8088
|
+
warnings
|
|
8089
|
+
};
|
|
8090
|
+
}
|
|
8091
|
+
|
|
8092
|
+
// src/lib/synthesis/executor.ts
|
|
8093
|
+
init_database();
|
|
8094
|
+
init_memories();
|
|
8095
|
+
init_synthesis();
|
|
8096
|
+
async function executeProposals(runId, proposals, db) {
|
|
8097
|
+
const d = db || getDatabase();
|
|
8098
|
+
let executed = 0;
|
|
8099
|
+
let failed = 0;
|
|
8100
|
+
const rollbackData = {};
|
|
8101
|
+
for (const proposal of proposals) {
|
|
8102
|
+
try {
|
|
8103
|
+
const rollback = executeProposal(proposal, d);
|
|
8104
|
+
updateProposal(proposal.id, {
|
|
8105
|
+
status: "accepted",
|
|
8106
|
+
executed_at: now(),
|
|
8107
|
+
rollback_data: rollback
|
|
8108
|
+
}, d);
|
|
8109
|
+
rollbackData[proposal.id] = rollback;
|
|
8110
|
+
executed++;
|
|
8111
|
+
} catch (err) {
|
|
8112
|
+
try {
|
|
8113
|
+
updateProposal(proposal.id, { status: "rejected" }, d);
|
|
8114
|
+
} catch {}
|
|
8115
|
+
failed++;
|
|
8116
|
+
}
|
|
8117
|
+
}
|
|
8118
|
+
return { runId, executed, failed, rollbackData };
|
|
8119
|
+
}
|
|
8120
|
+
function executeProposal(proposal, d) {
|
|
8121
|
+
let rollbackData = {};
|
|
8122
|
+
d.transaction(() => {
|
|
8123
|
+
switch (proposal.proposal_type) {
|
|
8124
|
+
case "archive":
|
|
8125
|
+
rollbackData = executeArchive(proposal, d);
|
|
8126
|
+
break;
|
|
8127
|
+
case "promote":
|
|
8128
|
+
rollbackData = executePromote(proposal, d);
|
|
8129
|
+
break;
|
|
8130
|
+
case "update_value":
|
|
8131
|
+
rollbackData = executeUpdateValue(proposal, d);
|
|
8132
|
+
break;
|
|
8133
|
+
case "add_tag":
|
|
8134
|
+
rollbackData = executeAddTag(proposal, d);
|
|
8135
|
+
break;
|
|
8136
|
+
case "merge":
|
|
8137
|
+
rollbackData = executeMerge(proposal, d);
|
|
8138
|
+
break;
|
|
8139
|
+
case "remove_duplicate":
|
|
8140
|
+
rollbackData = executeRemoveDuplicate(proposal, d);
|
|
8141
|
+
break;
|
|
8142
|
+
default:
|
|
8143
|
+
throw new Error(`Unknown proposal type: ${proposal.proposal_type}`);
|
|
8144
|
+
}
|
|
8145
|
+
})();
|
|
8146
|
+
return rollbackData;
|
|
8147
|
+
}
|
|
8148
|
+
function executeArchive(proposal, d) {
|
|
8149
|
+
const rollback = {};
|
|
8150
|
+
for (const memId of proposal.memory_ids) {
|
|
8151
|
+
const mem = getMemory(memId, d);
|
|
8152
|
+
if (!mem)
|
|
8153
|
+
continue;
|
|
8154
|
+
rollback[memId] = mem.status;
|
|
8155
|
+
d.run("UPDATE memories SET status = 'archived', updated_at = ? WHERE id = ?", [now(), memId]);
|
|
8156
|
+
}
|
|
8157
|
+
return { old_status: rollback };
|
|
8158
|
+
}
|
|
8159
|
+
function executePromote(proposal, d) {
|
|
8160
|
+
const newImportance = proposal.proposed_changes["new_importance"];
|
|
8161
|
+
if (typeof newImportance !== "number") {
|
|
8162
|
+
throw new Error("promote proposal missing new_importance in proposed_changes");
|
|
8163
|
+
}
|
|
8164
|
+
const rollback = {};
|
|
8165
|
+
for (const memId of proposal.memory_ids) {
|
|
8166
|
+
const mem = getMemory(memId, d);
|
|
8167
|
+
if (!mem)
|
|
8168
|
+
continue;
|
|
8169
|
+
rollback[memId] = mem.importance;
|
|
8170
|
+
d.run("UPDATE memories SET importance = ?, updated_at = ? WHERE id = ?", [Math.max(1, Math.min(10, Math.round(newImportance))), now(), memId]);
|
|
8171
|
+
}
|
|
8172
|
+
return { old_importance: rollback };
|
|
8173
|
+
}
|
|
8174
|
+
function executeUpdateValue(proposal, d) {
|
|
8175
|
+
const newValue = proposal.proposed_changes["new_value"];
|
|
8176
|
+
if (typeof newValue !== "string") {
|
|
8177
|
+
throw new Error("update_value proposal missing new_value in proposed_changes");
|
|
8178
|
+
}
|
|
8179
|
+
const rollback = {};
|
|
8180
|
+
const memId = proposal.memory_ids[0];
|
|
8181
|
+
if (!memId)
|
|
8182
|
+
throw new Error("update_value proposal has no memory_ids");
|
|
8183
|
+
const mem = getMemory(memId, d);
|
|
8184
|
+
if (!mem)
|
|
8185
|
+
throw new Error(`Memory ${memId} not found`);
|
|
8186
|
+
rollback[memId] = { value: mem.value, version: mem.version };
|
|
8187
|
+
d.run("UPDATE memories SET value = ?, version = version + 1, updated_at = ? WHERE id = ?", [newValue, now(), memId]);
|
|
8188
|
+
return { old_state: rollback };
|
|
8189
|
+
}
|
|
8190
|
+
function executeAddTag(proposal, d) {
|
|
8191
|
+
const tagsToAdd = proposal.proposed_changes["tags"];
|
|
8192
|
+
if (!Array.isArray(tagsToAdd)) {
|
|
8193
|
+
throw new Error("add_tag proposal missing tags array in proposed_changes");
|
|
8194
|
+
}
|
|
8195
|
+
const rollback = {};
|
|
8196
|
+
for (const memId of proposal.memory_ids) {
|
|
8197
|
+
const mem = getMemory(memId, d);
|
|
8198
|
+
if (!mem)
|
|
8199
|
+
continue;
|
|
8200
|
+
rollback[memId] = [...mem.tags];
|
|
8201
|
+
const newTags = Array.from(new Set([...mem.tags, ...tagsToAdd]));
|
|
8202
|
+
d.run("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?", [JSON.stringify(newTags), now(), memId]);
|
|
8203
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
8204
|
+
for (const tag of tagsToAdd) {
|
|
8205
|
+
insertTag.run(memId, tag);
|
|
8206
|
+
}
|
|
8207
|
+
}
|
|
8208
|
+
return { old_tags: rollback };
|
|
8209
|
+
}
|
|
8210
|
+
function executeMerge(proposal, d) {
|
|
8211
|
+
const targetId = proposal.target_memory_id;
|
|
8212
|
+
if (!targetId)
|
|
8213
|
+
throw new Error("merge proposal missing target_memory_id");
|
|
8214
|
+
const target = getMemory(targetId, d);
|
|
8215
|
+
if (!target)
|
|
8216
|
+
throw new Error(`Target memory ${targetId} not found`);
|
|
8217
|
+
const rollback = {
|
|
8218
|
+
target_old_value: target.value,
|
|
8219
|
+
target_old_version: target.version,
|
|
8220
|
+
archived_memories: {}
|
|
8221
|
+
};
|
|
8222
|
+
const sourceValues = [];
|
|
8223
|
+
for (const memId of proposal.memory_ids) {
|
|
8224
|
+
if (memId === targetId)
|
|
8225
|
+
continue;
|
|
8226
|
+
const mem = getMemory(memId, d);
|
|
8227
|
+
if (!mem)
|
|
8228
|
+
continue;
|
|
8229
|
+
sourceValues.push(mem.value);
|
|
8230
|
+
rollback.archived_memories[memId] = mem.status;
|
|
8231
|
+
}
|
|
8232
|
+
const mergedValue = proposal.proposed_changes["merged_value"] ?? [target.value, ...sourceValues].join(`
|
|
8233
|
+
---
|
|
8234
|
+
`);
|
|
8235
|
+
d.run("UPDATE memories SET value = ?, version = version + 1, updated_at = ? WHERE id = ?", [mergedValue, now(), targetId]);
|
|
8236
|
+
for (const memId of proposal.memory_ids) {
|
|
8237
|
+
if (memId === targetId)
|
|
8238
|
+
continue;
|
|
8239
|
+
d.run("UPDATE memories SET status = 'archived', updated_at = ? WHERE id = ?", [now(), memId]);
|
|
8240
|
+
}
|
|
8241
|
+
return rollback;
|
|
8242
|
+
}
|
|
8243
|
+
function executeRemoveDuplicate(proposal, d) {
|
|
8244
|
+
const memories = proposal.memory_ids.map((id) => getMemory(id, d)).filter((m) => m !== null);
|
|
8245
|
+
if (memories.length < 2) {
|
|
8246
|
+
throw new Error("remove_duplicate requires at least 2 memories");
|
|
8247
|
+
}
|
|
8248
|
+
memories.sort((a, b) => b.importance - a.importance || a.created_at.localeCompare(b.created_at));
|
|
8249
|
+
const keepId = proposal.target_memory_id ?? memories[0].id;
|
|
8250
|
+
const rollback = {};
|
|
8251
|
+
for (const mem of memories) {
|
|
8252
|
+
if (mem.id === keepId)
|
|
8253
|
+
continue;
|
|
8254
|
+
rollback[mem.id] = mem.status;
|
|
8255
|
+
d.run("UPDATE memories SET status = 'archived', updated_at = ? WHERE id = ?", [now(), mem.id]);
|
|
8256
|
+
}
|
|
8257
|
+
return { old_status: rollback, kept_id: keepId };
|
|
8258
|
+
}
|
|
8259
|
+
async function rollbackRun(runId, db) {
|
|
8260
|
+
const d = db || getDatabase();
|
|
8261
|
+
const proposals = listProposals(runId, { status: "accepted" }, d);
|
|
8262
|
+
let rolledBack = 0;
|
|
8263
|
+
const errors2 = [];
|
|
8264
|
+
for (const proposal of proposals) {
|
|
8265
|
+
try {
|
|
8266
|
+
rollbackProposal(proposal, d);
|
|
8267
|
+
updateProposal(proposal.id, { status: "rolled_back" }, d);
|
|
8268
|
+
rolledBack++;
|
|
8269
|
+
} catch (err) {
|
|
8270
|
+
errors2.push(`Proposal ${proposal.id} (${proposal.proposal_type}): ${err instanceof Error ? err.message : String(err)}`);
|
|
8271
|
+
}
|
|
8272
|
+
}
|
|
8273
|
+
return { rolled_back: rolledBack, errors: errors2 };
|
|
8274
|
+
}
|
|
8275
|
+
function rollbackProposal(proposal, d) {
|
|
8276
|
+
const rb = proposal.rollback_data;
|
|
8277
|
+
if (!rb)
|
|
8278
|
+
return;
|
|
8279
|
+
d.transaction(() => {
|
|
8280
|
+
switch (proposal.proposal_type) {
|
|
8281
|
+
case "archive":
|
|
8282
|
+
case "remove_duplicate": {
|
|
8283
|
+
const oldStatus = rb["old_status"];
|
|
8284
|
+
if (!oldStatus)
|
|
8285
|
+
break;
|
|
8286
|
+
for (const [memId, status] of Object.entries(oldStatus)) {
|
|
8287
|
+
d.run("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", [status, now(), memId]);
|
|
8288
|
+
}
|
|
8289
|
+
break;
|
|
8290
|
+
}
|
|
8291
|
+
case "promote": {
|
|
8292
|
+
const oldImportance = rb["old_importance"];
|
|
8293
|
+
if (!oldImportance)
|
|
8294
|
+
break;
|
|
8295
|
+
for (const [memId, importance] of Object.entries(oldImportance)) {
|
|
8296
|
+
d.run("UPDATE memories SET importance = ?, updated_at = ? WHERE id = ?", [importance, now(), memId]);
|
|
8297
|
+
}
|
|
8298
|
+
break;
|
|
8299
|
+
}
|
|
8300
|
+
case "update_value": {
|
|
8301
|
+
const oldState = rb["old_state"];
|
|
8302
|
+
if (!oldState)
|
|
8303
|
+
break;
|
|
8304
|
+
for (const [memId, state] of Object.entries(oldState)) {
|
|
8305
|
+
d.run("UPDATE memories SET value = ?, version = ?, updated_at = ? WHERE id = ?", [state.value, state.version, now(), memId]);
|
|
8306
|
+
}
|
|
8307
|
+
break;
|
|
8308
|
+
}
|
|
8309
|
+
case "add_tag": {
|
|
8310
|
+
const oldTags = rb["old_tags"];
|
|
8311
|
+
if (!oldTags)
|
|
8312
|
+
break;
|
|
8313
|
+
for (const [memId, tags] of Object.entries(oldTags)) {
|
|
8314
|
+
d.run("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?", [JSON.stringify(tags), now(), memId]);
|
|
8315
|
+
d.run("DELETE FROM memory_tags WHERE memory_id = ?", [memId]);
|
|
8316
|
+
const insertTag = d.prepare("INSERT OR IGNORE INTO memory_tags (memory_id, tag) VALUES (?, ?)");
|
|
8317
|
+
for (const tag of tags) {
|
|
8318
|
+
insertTag.run(memId, tag);
|
|
8319
|
+
}
|
|
8320
|
+
}
|
|
8321
|
+
break;
|
|
8322
|
+
}
|
|
8323
|
+
case "merge": {
|
|
8324
|
+
const targetOldValue = rb["target_old_value"];
|
|
8325
|
+
const targetOldVersion = rb["target_old_version"];
|
|
8326
|
+
const archivedMemories = rb["archived_memories"];
|
|
8327
|
+
const targetId = proposal.target_memory_id;
|
|
8328
|
+
if (targetId && targetOldValue !== undefined && targetOldVersion !== undefined) {
|
|
8329
|
+
d.run("UPDATE memories SET value = ?, version = ?, updated_at = ? WHERE id = ?", [targetOldValue, targetOldVersion, now(), targetId]);
|
|
8330
|
+
}
|
|
8331
|
+
if (archivedMemories) {
|
|
8332
|
+
for (const [memId, status] of Object.entries(archivedMemories)) {
|
|
8333
|
+
d.run("UPDATE memories SET status = ?, updated_at = ? WHERE id = ?", [status, now(), memId]);
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
break;
|
|
8337
|
+
}
|
|
8338
|
+
default:
|
|
8339
|
+
break;
|
|
8340
|
+
}
|
|
8341
|
+
})();
|
|
8342
|
+
}
|
|
8343
|
+
|
|
8344
|
+
// src/lib/synthesis/metrics.ts
|
|
8345
|
+
init_database();
|
|
8346
|
+
init_synthesis();
|
|
8347
|
+
init_memories();
|
|
8348
|
+
async function measureEffectiveness(runId, preCorpus, db) {
|
|
8349
|
+
const d = db || getDatabase();
|
|
8350
|
+
const postMemories = listMemories({
|
|
8351
|
+
status: "active",
|
|
8352
|
+
project_id: preCorpus.projectId ?? undefined
|
|
8353
|
+
}, d);
|
|
8354
|
+
const postCount = postMemories.length;
|
|
8355
|
+
const preCount = preCorpus.totalMemories;
|
|
8356
|
+
const corpusReduction = preCount > 0 ? (preCount - postCount) / preCount * 100 : 0;
|
|
8357
|
+
const preAvgImportance = preCorpus.items.length > 0 ? preCorpus.items.reduce((sum, i) => sum + i.memory.importance, 0) / preCorpus.items.length : 0;
|
|
8358
|
+
const postAvgImportance = postMemories.length > 0 ? postMemories.reduce((sum, m) => sum + m.importance, 0) / postMemories.length : 0;
|
|
8359
|
+
const importanceDrift = postAvgImportance - preAvgImportance;
|
|
8360
|
+
const preDuplicatePairs = preCorpus.duplicateCandidates.length;
|
|
8361
|
+
const acceptedProposals = listProposals(runId, { status: "accepted" }, d);
|
|
8362
|
+
const deduplicationProposals = acceptedProposals.filter((p) => p.proposal_type === "remove_duplicate" || p.proposal_type === "merge");
|
|
8363
|
+
const deduplicationRate = preDuplicatePairs > 0 ? deduplicationProposals.length / preDuplicatePairs * 100 : 0;
|
|
8364
|
+
const recallWeight = {
|
|
8365
|
+
corpusReduction: 0.3,
|
|
8366
|
+
importanceDrift: 0.4,
|
|
8367
|
+
deduplicationRate: 0.3
|
|
8368
|
+
};
|
|
8369
|
+
const estimatedRecallImprovement = Math.max(0, Math.min(100, corpusReduction * recallWeight.corpusReduction + Math.max(0, importanceDrift * 10) * recallWeight.importanceDrift + deduplicationRate * recallWeight.deduplicationRate));
|
|
8370
|
+
const metricDefs = [
|
|
8371
|
+
{ metric_type: "corpus_reduction_pct", value: corpusReduction, baseline: 0 },
|
|
8372
|
+
{ metric_type: "importance_drift", value: importanceDrift, baseline: 0 },
|
|
8373
|
+
{ metric_type: "deduplication_rate_pct", value: deduplicationRate, baseline: 0 },
|
|
8374
|
+
{ metric_type: "estimated_recall_improvement_pct", value: estimatedRecallImprovement, baseline: 0 },
|
|
8375
|
+
{ metric_type: "pre_corpus_size", value: preCount, baseline: preCount },
|
|
8376
|
+
{ metric_type: "post_corpus_size", value: postCount, baseline: preCount },
|
|
8377
|
+
{ metric_type: "proposals_accepted", value: acceptedProposals.length, baseline: 0 },
|
|
8378
|
+
{ metric_type: "pre_avg_importance", value: preAvgImportance, baseline: preAvgImportance },
|
|
8379
|
+
{ metric_type: "post_avg_importance", value: postAvgImportance, baseline: preAvgImportance }
|
|
8380
|
+
];
|
|
8381
|
+
for (const def of metricDefs) {
|
|
8382
|
+
createMetric({
|
|
8383
|
+
run_id: runId,
|
|
8384
|
+
metric_type: def.metric_type,
|
|
8385
|
+
value: def.value,
|
|
8386
|
+
baseline: def.baseline ?? null
|
|
8387
|
+
}, d);
|
|
8388
|
+
}
|
|
8389
|
+
const metrics = listMetrics(runId, d);
|
|
8390
|
+
return {
|
|
8391
|
+
runId,
|
|
8392
|
+
corpusReduction,
|
|
8393
|
+
importanceDrift,
|
|
8394
|
+
deduplicationRate,
|
|
8395
|
+
estimatedRecallImprovement,
|
|
8396
|
+
metrics
|
|
8397
|
+
};
|
|
8398
|
+
}
|
|
8399
|
+
|
|
8400
|
+
// src/lib/synthesis/index.ts
|
|
8401
|
+
async function runSynthesis(options = {}) {
|
|
8402
|
+
const d = options.db || getDatabase();
|
|
8403
|
+
const projectId = options.projectId ?? null;
|
|
8404
|
+
const agentId = options.agentId ?? null;
|
|
8405
|
+
const dryRun = options.dryRun ?? false;
|
|
8406
|
+
const run = createSynthesisRun({
|
|
8407
|
+
triggered_by: "manual",
|
|
8408
|
+
project_id: projectId,
|
|
8409
|
+
agent_id: agentId
|
|
8410
|
+
}, d);
|
|
8411
|
+
updateSynthesisRun(run.id, { status: "running" }, d);
|
|
8412
|
+
try {
|
|
8413
|
+
const corpus = await buildCorpus({
|
|
8414
|
+
projectId: projectId ?? undefined,
|
|
8415
|
+
agentId: agentId ?? undefined,
|
|
8416
|
+
db: d
|
|
8417
|
+
});
|
|
8418
|
+
updateSynthesisRun(run.id, { corpus_size: corpus.totalMemories }, d);
|
|
8419
|
+
const analysisResult = await analyzeCorpus(corpus, {
|
|
8420
|
+
provider: options.provider,
|
|
8421
|
+
maxProposals: options.maxProposals ?? 20
|
|
8422
|
+
});
|
|
8423
|
+
const validation = validateProposals(analysisResult.proposals, corpus, options.safetyConfig);
|
|
8424
|
+
const rejectedIndices = new Set(validation.rejectedProposals.map((r) => {
|
|
8425
|
+
const match = r.proposalId.match(/\[(\d+)\]/);
|
|
8426
|
+
return match ? parseInt(match[1], 10) : -1;
|
|
8427
|
+
}));
|
|
8428
|
+
const validProposals = analysisResult.proposals.filter((_, idx) => !rejectedIndices.has(idx));
|
|
8429
|
+
const savedProposals = [];
|
|
8430
|
+
for (const p of validProposals) {
|
|
8431
|
+
const saved = createProposal({
|
|
8432
|
+
run_id: run.id,
|
|
8433
|
+
proposal_type: p.type,
|
|
8434
|
+
memory_ids: p.memory_ids,
|
|
8435
|
+
target_memory_id: p.target_memory_id ?? null,
|
|
8436
|
+
proposed_changes: p.proposed_changes,
|
|
8437
|
+
reasoning: p.reasoning,
|
|
8438
|
+
confidence: p.confidence
|
|
8439
|
+
}, d);
|
|
8440
|
+
savedProposals.push(saved);
|
|
8441
|
+
}
|
|
8442
|
+
updateSynthesisRun(run.id, {
|
|
8443
|
+
proposals_generated: analysisResult.proposals.length,
|
|
8444
|
+
proposals_rejected: validation.rejectedProposals.length
|
|
8445
|
+
}, d);
|
|
8446
|
+
let executedCount = 0;
|
|
8447
|
+
if (!dryRun && savedProposals.length > 0) {
|
|
8448
|
+
const execResult = await executeProposals(run.id, savedProposals, d);
|
|
8449
|
+
executedCount = execResult.executed;
|
|
8450
|
+
updateSynthesisRun(run.id, {
|
|
8451
|
+
proposals_accepted: execResult.executed,
|
|
8452
|
+
proposals_rejected: validation.rejectedProposals.length + execResult.failed,
|
|
8453
|
+
status: "completed",
|
|
8454
|
+
completed_at: now()
|
|
8455
|
+
}, d);
|
|
8456
|
+
} else {
|
|
8457
|
+
updateSynthesisRun(run.id, {
|
|
8458
|
+
status: "completed",
|
|
8459
|
+
completed_at: now()
|
|
8460
|
+
}, d);
|
|
8461
|
+
}
|
|
8462
|
+
let effectivenessReport = null;
|
|
8463
|
+
if (!dryRun && executedCount > 0) {
|
|
8464
|
+
try {
|
|
8465
|
+
effectivenessReport = await measureEffectiveness(run.id, corpus, d);
|
|
8466
|
+
} catch {}
|
|
8467
|
+
}
|
|
8468
|
+
const finalRun = listSynthesisRuns({ project_id: projectId, limit: 1 }, d)[0] ?? run;
|
|
8469
|
+
const finalProposals = listProposals(run.id, undefined, d);
|
|
8470
|
+
return {
|
|
8471
|
+
run: finalRun,
|
|
8472
|
+
proposals: finalProposals,
|
|
8473
|
+
executed: executedCount,
|
|
8474
|
+
metrics: effectivenessReport,
|
|
8475
|
+
dryRun
|
|
8476
|
+
};
|
|
8477
|
+
} catch (err) {
|
|
8478
|
+
updateSynthesisRun(run.id, {
|
|
8479
|
+
status: "failed",
|
|
8480
|
+
error: err instanceof Error ? err.message : String(err),
|
|
8481
|
+
completed_at: now()
|
|
8482
|
+
}, d);
|
|
8483
|
+
const failedRun = listSynthesisRuns({ project_id: projectId, limit: 1 }, d)[0] ?? run;
|
|
8484
|
+
return {
|
|
8485
|
+
run: failedRun,
|
|
8486
|
+
proposals: [],
|
|
8487
|
+
executed: 0,
|
|
8488
|
+
metrics: null,
|
|
8489
|
+
dryRun
|
|
8490
|
+
};
|
|
8491
|
+
}
|
|
8492
|
+
}
|
|
8493
|
+
async function rollbackSynthesis(runId, db) {
|
|
8494
|
+
const d = db || getDatabase();
|
|
8495
|
+
const result = await rollbackRun(runId, d);
|
|
8496
|
+
if (result.errors.length === 0) {
|
|
8497
|
+
updateSynthesisRun(runId, { status: "rolled_back", completed_at: now() }, d);
|
|
8498
|
+
}
|
|
8499
|
+
return result;
|
|
8500
|
+
}
|
|
8501
|
+
function getSynthesisStatus(runId, projectId, db) {
|
|
8502
|
+
const d = db || getDatabase();
|
|
8503
|
+
if (runId) {
|
|
8504
|
+
const recentRuns2 = listSynthesisRuns({ project_id: projectId ?? null, limit: 10 }, d);
|
|
8505
|
+
const specificRun = recentRuns2.find((r) => r.id === runId) ?? null;
|
|
8506
|
+
return {
|
|
8507
|
+
lastRun: specificRun ?? recentRuns2[0] ?? null,
|
|
8508
|
+
recentRuns: recentRuns2
|
|
8509
|
+
};
|
|
8510
|
+
}
|
|
8511
|
+
const recentRuns = listSynthesisRuns({ project_id: projectId ?? null, limit: 10 }, d);
|
|
8512
|
+
return {
|
|
8513
|
+
lastRun: recentRuns[0] ?? null,
|
|
8514
|
+
recentRuns
|
|
8515
|
+
};
|
|
8516
|
+
}
|
|
8517
|
+
|
|
8518
|
+
// src/mcp/index.ts
|
|
8519
|
+
init_synthesis();
|
|
8520
|
+
|
|
8521
|
+
// src/db/session-jobs.ts
|
|
8522
|
+
init_database();
|
|
8523
|
+
function parseJobRow(row) {
|
|
8524
|
+
return {
|
|
8525
|
+
id: row["id"],
|
|
8526
|
+
session_id: row["session_id"],
|
|
8527
|
+
agent_id: row["agent_id"] || null,
|
|
8528
|
+
project_id: row["project_id"] || null,
|
|
8529
|
+
source: row["source"],
|
|
8530
|
+
status: row["status"],
|
|
8531
|
+
transcript: row["transcript"],
|
|
8532
|
+
chunk_count: row["chunk_count"],
|
|
8533
|
+
memories_extracted: row["memories_extracted"],
|
|
8534
|
+
error: row["error"] || null,
|
|
8535
|
+
metadata: JSON.parse(row["metadata"] || "{}"),
|
|
8536
|
+
created_at: row["created_at"],
|
|
8537
|
+
started_at: row["started_at"] || null,
|
|
8538
|
+
completed_at: row["completed_at"] || null
|
|
8539
|
+
};
|
|
8540
|
+
}
|
|
8541
|
+
function createSessionJob(input, db) {
|
|
8542
|
+
const d = db || getDatabase();
|
|
8543
|
+
const id = uuid();
|
|
8544
|
+
const timestamp = now();
|
|
8545
|
+
const source = input.source ?? "manual";
|
|
8546
|
+
const metadata = JSON.stringify(input.metadata ?? {});
|
|
8547
|
+
d.run(`INSERT INTO session_memory_jobs
|
|
8548
|
+
(id, session_id, agent_id, project_id, source, status, transcript, chunk_count, memories_extracted, metadata, created_at)
|
|
8549
|
+
VALUES (?, ?, ?, ?, ?, 'pending', ?, 0, 0, ?, ?)`, [
|
|
8550
|
+
id,
|
|
8551
|
+
input.session_id,
|
|
8552
|
+
input.agent_id ?? null,
|
|
8553
|
+
input.project_id ?? null,
|
|
8554
|
+
source,
|
|
8555
|
+
input.transcript,
|
|
8556
|
+
metadata,
|
|
8557
|
+
timestamp
|
|
8558
|
+
]);
|
|
8559
|
+
return getSessionJob(id, d);
|
|
8560
|
+
}
|
|
8561
|
+
function getSessionJob(id, db) {
|
|
8562
|
+
const d = db || getDatabase();
|
|
8563
|
+
const row = d.query("SELECT * FROM session_memory_jobs WHERE id = ?").get(id);
|
|
8564
|
+
if (!row)
|
|
8565
|
+
return null;
|
|
8566
|
+
return parseJobRow(row);
|
|
8567
|
+
}
|
|
8568
|
+
function listSessionJobs(filter, db) {
|
|
8569
|
+
const d = db || getDatabase();
|
|
8570
|
+
const conditions = [];
|
|
8571
|
+
const params = [];
|
|
8572
|
+
if (filter?.agent_id) {
|
|
8573
|
+
conditions.push("agent_id = ?");
|
|
8574
|
+
params.push(filter.agent_id);
|
|
8575
|
+
}
|
|
8576
|
+
if (filter?.project_id) {
|
|
8577
|
+
conditions.push("project_id = ?");
|
|
8578
|
+
params.push(filter.project_id);
|
|
8579
|
+
}
|
|
8580
|
+
if (filter?.status) {
|
|
8581
|
+
conditions.push("status = ?");
|
|
8582
|
+
params.push(filter.status);
|
|
8583
|
+
}
|
|
8584
|
+
if (filter?.session_id) {
|
|
8585
|
+
conditions.push("session_id = ?");
|
|
8586
|
+
params.push(filter.session_id);
|
|
8587
|
+
}
|
|
8588
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
8589
|
+
const limit = filter?.limit ?? 20;
|
|
8590
|
+
const offset = filter?.offset ?? 0;
|
|
8591
|
+
const rows = d.query(`SELECT * FROM session_memory_jobs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
8592
|
+
return rows.map(parseJobRow);
|
|
8593
|
+
}
|
|
8594
|
+
function updateSessionJob(id, updates, db) {
|
|
8595
|
+
const d = db || getDatabase();
|
|
8596
|
+
const setClauses = [];
|
|
8597
|
+
const params = [];
|
|
8598
|
+
if (updates.status !== undefined) {
|
|
8599
|
+
setClauses.push("status = ?");
|
|
8600
|
+
params.push(updates.status);
|
|
8601
|
+
}
|
|
8602
|
+
if (updates.chunk_count !== undefined) {
|
|
8603
|
+
setClauses.push("chunk_count = ?");
|
|
8604
|
+
params.push(updates.chunk_count);
|
|
8605
|
+
}
|
|
8606
|
+
if (updates.memories_extracted !== undefined) {
|
|
8607
|
+
setClauses.push("memories_extracted = ?");
|
|
8608
|
+
params.push(updates.memories_extracted);
|
|
8609
|
+
}
|
|
8610
|
+
if ("error" in updates) {
|
|
8611
|
+
setClauses.push("error = ?");
|
|
8612
|
+
params.push(updates.error ?? null);
|
|
8613
|
+
}
|
|
8614
|
+
if ("started_at" in updates) {
|
|
8615
|
+
setClauses.push("started_at = ?");
|
|
8616
|
+
params.push(updates.started_at ?? null);
|
|
8617
|
+
}
|
|
8618
|
+
if ("completed_at" in updates) {
|
|
8619
|
+
setClauses.push("completed_at = ?");
|
|
8620
|
+
params.push(updates.completed_at ?? null);
|
|
8621
|
+
}
|
|
8622
|
+
if (setClauses.length === 0)
|
|
8623
|
+
return getSessionJob(id, d);
|
|
8624
|
+
params.push(id);
|
|
8625
|
+
d.run(`UPDATE session_memory_jobs SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
8626
|
+
return getSessionJob(id, d);
|
|
8627
|
+
}
|
|
8628
|
+
function getNextPendingJob(db) {
|
|
8629
|
+
const d = db || getDatabase();
|
|
8630
|
+
const row = d.query("SELECT * FROM session_memory_jobs WHERE status = 'pending' ORDER BY created_at ASC LIMIT 1").get();
|
|
8631
|
+
if (!row)
|
|
8632
|
+
return null;
|
|
8633
|
+
return parseJobRow(row);
|
|
8634
|
+
}
|
|
8635
|
+
|
|
8636
|
+
// src/lib/session-queue.ts
|
|
8637
|
+
init_database();
|
|
8638
|
+
|
|
8639
|
+
// src/lib/session-processor.ts
|
|
8640
|
+
init_memories();
|
|
8641
|
+
init_registry();
|
|
8642
|
+
var SESSION_EXTRACTION_USER_TEMPLATE = (chunk, sessionId) => `Extract memories from this session chunk (session: ${sessionId}):
|
|
8643
|
+
|
|
8644
|
+
${chunk}
|
|
8645
|
+
|
|
8646
|
+
Return JSON array: [{"key": "...", "value": "...", "category": "knowledge|fact|preference|history", "importance": 1-10, "tags": [...]}]`;
|
|
8647
|
+
function chunkTranscript(transcript, chunkSize = 2000, overlap = 200) {
|
|
8648
|
+
if (!transcript || transcript.length === 0)
|
|
8649
|
+
return [];
|
|
8650
|
+
if (transcript.length <= chunkSize)
|
|
8651
|
+
return [transcript];
|
|
8652
|
+
const chunks = [];
|
|
8653
|
+
let start = 0;
|
|
8654
|
+
while (start < transcript.length) {
|
|
8655
|
+
const end = Math.min(start + chunkSize, transcript.length);
|
|
8656
|
+
chunks.push(transcript.slice(start, end));
|
|
8657
|
+
if (end === transcript.length)
|
|
8658
|
+
break;
|
|
8659
|
+
start += chunkSize - overlap;
|
|
8660
|
+
}
|
|
8661
|
+
return chunks;
|
|
8662
|
+
}
|
|
8663
|
+
async function extractMemoriesFromChunk(chunk, context, db) {
|
|
8664
|
+
const provider = providerRegistry.getAvailable();
|
|
8665
|
+
if (!provider)
|
|
8666
|
+
return 0;
|
|
8667
|
+
try {
|
|
8668
|
+
const extracted = await provider.extractMemories(SESSION_EXTRACTION_USER_TEMPLATE(chunk, context.sessionId), {
|
|
8669
|
+
sessionId: context.sessionId,
|
|
8670
|
+
agentId: context.agentId,
|
|
8671
|
+
projectId: context.projectId
|
|
8672
|
+
});
|
|
8673
|
+
let savedCount = 0;
|
|
8674
|
+
const sourceTag = context.source ? `source:${context.source}` : "source:manual";
|
|
8675
|
+
for (const memory of extracted) {
|
|
8676
|
+
if (!memory.content || !memory.content.trim())
|
|
8677
|
+
continue;
|
|
8678
|
+
try {
|
|
8679
|
+
createMemory({
|
|
8680
|
+
key: memory.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
|
|
8681
|
+
value: memory.content,
|
|
8682
|
+
category: memory.category,
|
|
8683
|
+
scope: memory.suggestedScope ?? "shared",
|
|
8684
|
+
importance: memory.importance,
|
|
8685
|
+
tags: [
|
|
8686
|
+
...memory.tags,
|
|
8687
|
+
"session-extracted",
|
|
8688
|
+
sourceTag,
|
|
8689
|
+
`session:${context.sessionId}`
|
|
8690
|
+
],
|
|
8691
|
+
source: "auto",
|
|
8692
|
+
agent_id: context.agentId,
|
|
8693
|
+
project_id: context.projectId,
|
|
8694
|
+
session_id: context.sessionId,
|
|
8695
|
+
metadata: {
|
|
8696
|
+
auto_extracted: true,
|
|
8697
|
+
session_source: context.source ?? "manual",
|
|
8698
|
+
extracted_at: new Date().toISOString(),
|
|
8699
|
+
reasoning: memory.reasoning
|
|
8700
|
+
}
|
|
8701
|
+
}, "merge", db);
|
|
8702
|
+
savedCount++;
|
|
8703
|
+
} catch {}
|
|
8704
|
+
}
|
|
8705
|
+
return savedCount;
|
|
8706
|
+
} catch {
|
|
8707
|
+
try {
|
|
8708
|
+
const fallbacks = providerRegistry.getFallbacks();
|
|
8709
|
+
for (const fallback of fallbacks) {
|
|
8710
|
+
try {
|
|
8711
|
+
const extracted = await fallback.extractMemories(SESSION_EXTRACTION_USER_TEMPLATE(chunk, context.sessionId), {
|
|
8712
|
+
sessionId: context.sessionId,
|
|
8713
|
+
agentId: context.agentId,
|
|
8714
|
+
projectId: context.projectId
|
|
8715
|
+
});
|
|
8716
|
+
let savedCount = 0;
|
|
8717
|
+
const sourceTag = context.source ? `source:${context.source}` : "source:manual";
|
|
8718
|
+
for (const memory of extracted) {
|
|
8719
|
+
if (!memory.content || !memory.content.trim())
|
|
8720
|
+
continue;
|
|
8721
|
+
try {
|
|
8722
|
+
createMemory({
|
|
8723
|
+
key: memory.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
|
|
8724
|
+
value: memory.content,
|
|
8725
|
+
category: memory.category,
|
|
8726
|
+
scope: memory.suggestedScope ?? "shared",
|
|
8727
|
+
importance: memory.importance,
|
|
8728
|
+
tags: [
|
|
8729
|
+
...memory.tags,
|
|
8730
|
+
"session-extracted",
|
|
8731
|
+
sourceTag,
|
|
8732
|
+
`session:${context.sessionId}`
|
|
8733
|
+
],
|
|
8734
|
+
source: "auto",
|
|
8735
|
+
agent_id: context.agentId,
|
|
8736
|
+
project_id: context.projectId,
|
|
8737
|
+
session_id: context.sessionId,
|
|
8738
|
+
metadata: {
|
|
8739
|
+
auto_extracted: true,
|
|
8740
|
+
session_source: context.source ?? "manual",
|
|
8741
|
+
extracted_at: new Date().toISOString(),
|
|
8742
|
+
reasoning: memory.reasoning
|
|
8743
|
+
}
|
|
8744
|
+
}, "merge", db);
|
|
8745
|
+
savedCount++;
|
|
8746
|
+
} catch {}
|
|
8747
|
+
}
|
|
8748
|
+
if (savedCount > 0)
|
|
8749
|
+
return savedCount;
|
|
8750
|
+
} catch {
|
|
8751
|
+
continue;
|
|
8752
|
+
}
|
|
8753
|
+
}
|
|
8754
|
+
} catch {}
|
|
8755
|
+
return 0;
|
|
8756
|
+
}
|
|
8757
|
+
}
|
|
8758
|
+
async function processSessionJob(jobId, db) {
|
|
8759
|
+
const result = {
|
|
8760
|
+
jobId,
|
|
8761
|
+
chunksProcessed: 0,
|
|
8762
|
+
memoriesExtracted: 0,
|
|
8763
|
+
errors: []
|
|
8764
|
+
};
|
|
8765
|
+
let job;
|
|
8766
|
+
try {
|
|
8767
|
+
job = getSessionJob(jobId, db);
|
|
8768
|
+
if (!job) {
|
|
8769
|
+
result.errors.push(`Job not found: ${jobId}`);
|
|
8770
|
+
return result;
|
|
8771
|
+
}
|
|
8772
|
+
} catch (e) {
|
|
8773
|
+
result.errors.push(`Failed to fetch job: ${String(e)}`);
|
|
8774
|
+
return result;
|
|
8775
|
+
}
|
|
8776
|
+
try {
|
|
8777
|
+
updateSessionJob(jobId, { status: "processing", started_at: new Date().toISOString() }, db);
|
|
8778
|
+
} catch (e) {
|
|
8779
|
+
result.errors.push(`Failed to mark job as processing: ${String(e)}`);
|
|
8780
|
+
return result;
|
|
8781
|
+
}
|
|
8782
|
+
const chunks = chunkTranscript(job.transcript);
|
|
8783
|
+
try {
|
|
8784
|
+
updateSessionJob(jobId, { chunk_count: chunks.length }, db);
|
|
8785
|
+
} catch {}
|
|
8786
|
+
let totalMemories = 0;
|
|
8787
|
+
for (let i = 0;i < chunks.length; i++) {
|
|
8788
|
+
const chunk = chunks[i];
|
|
8789
|
+
try {
|
|
8790
|
+
const count = await extractMemoriesFromChunk(chunk, {
|
|
8791
|
+
sessionId: job.session_id,
|
|
8792
|
+
agentId: job.agent_id ?? undefined,
|
|
8793
|
+
projectId: job.project_id ?? undefined,
|
|
8794
|
+
source: job.source
|
|
8795
|
+
}, db);
|
|
8796
|
+
totalMemories += count;
|
|
8797
|
+
result.chunksProcessed++;
|
|
8798
|
+
} catch (e) {
|
|
8799
|
+
result.errors.push(`Chunk ${i} failed: ${String(e)}`);
|
|
8800
|
+
}
|
|
8801
|
+
}
|
|
8802
|
+
result.memoriesExtracted = totalMemories;
|
|
8803
|
+
try {
|
|
8804
|
+
if (result.errors.length > 0 && result.chunksProcessed === 0) {
|
|
8805
|
+
updateSessionJob(jobId, {
|
|
8806
|
+
status: "failed",
|
|
8807
|
+
error: result.errors.join("; "),
|
|
8808
|
+
completed_at: new Date().toISOString(),
|
|
8809
|
+
memories_extracted: totalMemories,
|
|
8810
|
+
chunk_count: chunks.length
|
|
8811
|
+
}, db);
|
|
8812
|
+
} else {
|
|
8813
|
+
updateSessionJob(jobId, {
|
|
8814
|
+
status: "completed",
|
|
8815
|
+
completed_at: new Date().toISOString(),
|
|
8816
|
+
memories_extracted: totalMemories,
|
|
8817
|
+
chunk_count: chunks.length
|
|
8818
|
+
}, db);
|
|
8819
|
+
}
|
|
8820
|
+
} catch (e) {
|
|
8821
|
+
result.errors.push(`Failed to update job status: ${String(e)}`);
|
|
8822
|
+
}
|
|
8823
|
+
return result;
|
|
8824
|
+
}
|
|
8825
|
+
|
|
8826
|
+
// src/lib/session-queue.ts
|
|
8827
|
+
var _pendingQueue = new Set;
|
|
8828
|
+
var _isProcessing = false;
|
|
8829
|
+
function enqueueSessionJob(jobId) {
|
|
8830
|
+
_pendingQueue.add(jobId);
|
|
8831
|
+
if (!_isProcessing) {
|
|
8832
|
+
_processNext();
|
|
8833
|
+
}
|
|
8834
|
+
}
|
|
8835
|
+
async function _processNext() {
|
|
8836
|
+
if (_isProcessing)
|
|
8837
|
+
return;
|
|
8838
|
+
let jobId;
|
|
8839
|
+
if (_pendingQueue.size > 0) {
|
|
8840
|
+
jobId = [..._pendingQueue][0];
|
|
8841
|
+
_pendingQueue.delete(jobId);
|
|
8842
|
+
} else {
|
|
8843
|
+
try {
|
|
8844
|
+
const job = getNextPendingJob();
|
|
8845
|
+
if (job)
|
|
8846
|
+
jobId = job.id;
|
|
8847
|
+
} catch {
|
|
8848
|
+
return;
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
if (!jobId)
|
|
8852
|
+
return;
|
|
8853
|
+
_isProcessing = true;
|
|
8854
|
+
try {
|
|
8855
|
+
await processSessionJob(jobId);
|
|
8856
|
+
} catch {} finally {
|
|
8857
|
+
_isProcessing = false;
|
|
8858
|
+
if (_pendingQueue.size > 0) {
|
|
8859
|
+
_processNext();
|
|
8860
|
+
}
|
|
8861
|
+
}
|
|
8862
|
+
}
|
|
8863
|
+
|
|
8864
|
+
// src/mcp/index.ts
|
|
8865
|
+
init_auto_memory();
|
|
8866
|
+
init_registry();
|
|
8867
|
+
import { createRequire } from "module";
|
|
8868
|
+
var _require = createRequire(import.meta.url);
|
|
8869
|
+
var _pkg = _require("../../package.json");
|
|
8870
|
+
var server = new McpServer({
|
|
8871
|
+
name: "mementos",
|
|
8872
|
+
version: _pkg.version
|
|
8873
|
+
});
|
|
8874
|
+
var _autoProjectInitialized = false;
|
|
8875
|
+
function ensureAutoProject() {
|
|
8876
|
+
if (_autoProjectInitialized)
|
|
8877
|
+
return;
|
|
8878
|
+
_autoProjectInitialized = true;
|
|
8879
|
+
try {
|
|
8880
|
+
detectProject();
|
|
8881
|
+
} catch {}
|
|
8882
|
+
}
|
|
8883
|
+
function formatError(error) {
|
|
8884
|
+
if (error instanceof VersionConflictError)
|
|
8885
|
+
return `Version conflict: ${error.message}`;
|
|
8886
|
+
if (error instanceof MemoryNotFoundError)
|
|
8887
|
+
return `Not found: ${error.message}`;
|
|
8888
|
+
if (error instanceof DuplicateMemoryError)
|
|
8889
|
+
return `Duplicate: ${error.message}`;
|
|
8890
|
+
if (error instanceof InvalidScopeError)
|
|
8891
|
+
return `Invalid scope: ${error.message}`;
|
|
8892
|
+
if (error instanceof Error)
|
|
8893
|
+
return error.message;
|
|
8894
|
+
return String(error);
|
|
8895
|
+
}
|
|
8896
|
+
function resolveId(partialId, table = "memories") {
|
|
8897
|
+
const db = getDatabase();
|
|
8898
|
+
const id = resolvePartialId(db, table, partialId);
|
|
7235
8899
|
if (!id)
|
|
7236
8900
|
throw new Error(`Could not resolve ID: ${partialId}`);
|
|
7237
8901
|
return id;
|
|
@@ -8006,6 +9670,58 @@ server.tool("webhook_update", "Enable, disable, or update a persisted webhook ho
|
|
|
8006
9670
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
8007
9671
|
}
|
|
8008
9672
|
});
|
|
9673
|
+
server.tool("memory_synthesize", "Run ALMA synthesis: analyze memory corpus, find redundancies, propose and apply consolidations.", {
|
|
9674
|
+
project_id: exports_external.string().optional(),
|
|
9675
|
+
agent_id: exports_external.string().optional(),
|
|
9676
|
+
dry_run: exports_external.boolean().optional(),
|
|
9677
|
+
max_proposals: exports_external.coerce.number().optional(),
|
|
9678
|
+
provider: exports_external.string().optional()
|
|
9679
|
+
}, async (args) => {
|
|
9680
|
+
try {
|
|
9681
|
+
const result = await runSynthesis({
|
|
9682
|
+
projectId: args.project_id,
|
|
9683
|
+
agentId: args.agent_id,
|
|
9684
|
+
dryRun: args.dry_run ?? false,
|
|
9685
|
+
maxProposals: args.max_proposals,
|
|
9686
|
+
provider: args.provider
|
|
9687
|
+
});
|
|
9688
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
9689
|
+
run_id: result.run.id,
|
|
9690
|
+
status: result.run.status,
|
|
9691
|
+
corpus_size: result.run.corpus_size,
|
|
9692
|
+
proposals_generated: result.run.proposals_generated,
|
|
9693
|
+
proposals_accepted: result.run.proposals_accepted,
|
|
9694
|
+
dry_run: result.dryRun,
|
|
9695
|
+
metrics: result.metrics ? { corpus_reduction: result.metrics.corpusReduction, deduplication_rate: result.metrics.deduplicationRate } : null
|
|
9696
|
+
}, null, 2) }] };
|
|
9697
|
+
} catch (e) {
|
|
9698
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
9699
|
+
}
|
|
9700
|
+
});
|
|
9701
|
+
server.tool("memory_synthesis_status", "Get the status of synthesis runs.", { project_id: exports_external.string().optional(), run_id: exports_external.string().optional() }, async (args) => {
|
|
9702
|
+
try {
|
|
9703
|
+
const status = getSynthesisStatus(args.run_id, args.project_id);
|
|
9704
|
+
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
9705
|
+
} catch (e) {
|
|
9706
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
9707
|
+
}
|
|
9708
|
+
});
|
|
9709
|
+
server.tool("memory_synthesis_history", "List past synthesis runs.", { project_id: exports_external.string().optional(), limit: exports_external.coerce.number().optional() }, async (args) => {
|
|
9710
|
+
try {
|
|
9711
|
+
const runs = listSynthesisRuns({ project_id: args.project_id, limit: args.limit ?? 20 });
|
|
9712
|
+
return { content: [{ type: "text", text: JSON.stringify(runs, null, 2) }] };
|
|
9713
|
+
} catch (e) {
|
|
9714
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
9715
|
+
}
|
|
9716
|
+
});
|
|
9717
|
+
server.tool("memory_synthesis_rollback", "Roll back a synthesis run, reversing all applied proposals.", { run_id: exports_external.string() }, async (args) => {
|
|
9718
|
+
try {
|
|
9719
|
+
const result = await rollbackSynthesis(args.run_id);
|
|
9720
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
9721
|
+
} catch (e) {
|
|
9722
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
9723
|
+
}
|
|
9724
|
+
});
|
|
8009
9725
|
server.tool("register_agent", "Register an agent. Idempotent \u2014 same name returns existing agent.", {
|
|
8010
9726
|
name: exports_external.string(),
|
|
8011
9727
|
session_id: exports_external.string().optional(),
|
|
@@ -9364,6 +11080,50 @@ server.tool("memory_auto_test", "Test memory extraction on text WITHOUT saving a
|
|
|
9364
11080
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
9365
11081
|
}
|
|
9366
11082
|
});
|
|
11083
|
+
server.tool("memory_ingest_session", "Submit a session transcript for async memory extraction. Returns job_id to track progress.", {
|
|
11084
|
+
transcript: exports_external.string(),
|
|
11085
|
+
session_id: exports_external.string(),
|
|
11086
|
+
agent_id: exports_external.string().optional(),
|
|
11087
|
+
project_id: exports_external.string().optional(),
|
|
11088
|
+
source: exports_external.enum(["claude-code", "codex", "manual", "open-sessions"]).optional()
|
|
11089
|
+
}, async (args) => {
|
|
11090
|
+
try {
|
|
11091
|
+
const job = createSessionJob({
|
|
11092
|
+
session_id: args.session_id,
|
|
11093
|
+
transcript: args.transcript,
|
|
11094
|
+
source: args.source ?? "manual",
|
|
11095
|
+
agent_id: args.agent_id,
|
|
11096
|
+
project_id: args.project_id
|
|
11097
|
+
});
|
|
11098
|
+
enqueueSessionJob(job.id);
|
|
11099
|
+
return { content: [{ type: "text", text: JSON.stringify({ job_id: job.id, status: "queued" }, null, 2) }] };
|
|
11100
|
+
} catch (e) {
|
|
11101
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11102
|
+
}
|
|
11103
|
+
});
|
|
11104
|
+
server.tool("memory_session_status", "Get the status of a session memory extraction job.", { job_id: exports_external.string() }, async (args) => {
|
|
11105
|
+
try {
|
|
11106
|
+
const job = getSessionJob(args.job_id);
|
|
11107
|
+
if (!job)
|
|
11108
|
+
return { content: [{ type: "text", text: `Job not found: ${args.job_id}` }], isError: true };
|
|
11109
|
+
return { content: [{ type: "text", text: JSON.stringify(job, null, 2) }] };
|
|
11110
|
+
} catch (e) {
|
|
11111
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11112
|
+
}
|
|
11113
|
+
});
|
|
11114
|
+
server.tool("memory_session_list", "List session memory extraction jobs.", {
|
|
11115
|
+
agent_id: exports_external.string().optional(),
|
|
11116
|
+
project_id: exports_external.string().optional(),
|
|
11117
|
+
status: exports_external.enum(["pending", "processing", "completed", "failed"]).optional(),
|
|
11118
|
+
limit: exports_external.coerce.number().optional()
|
|
11119
|
+
}, async (args) => {
|
|
11120
|
+
try {
|
|
11121
|
+
const jobs = listSessionJobs({ agent_id: args.agent_id, project_id: args.project_id, status: args.status, limit: args.limit ?? 20 });
|
|
11122
|
+
return { content: [{ type: "text", text: JSON.stringify(jobs, null, 2) }] };
|
|
11123
|
+
} catch (e) {
|
|
11124
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
11125
|
+
}
|
|
11126
|
+
});
|
|
9367
11127
|
async function main() {
|
|
9368
11128
|
loadWebhooksFromDb();
|
|
9369
11129
|
const transport = new StdioServerTransport;
|