@hasna/mementos 0.8.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.
@@ -490,7 +490,30 @@ var init_database = __esm(() => {
490
490
  CREATE INDEX IF NOT EXISTS idx_webhook_hooks_type ON webhook_hooks(type);
491
491
  CREATE INDEX IF NOT EXISTS idx_webhook_hooks_enabled ON webhook_hooks(enabled);
492
492
  INSERT OR IGNORE INTO _migrations (id) VALUES (10);
493
- `
493
+ `,
494
+ `
495
+ CREATE TABLE IF NOT EXISTS session_memory_jobs (
496
+ id TEXT PRIMARY KEY,
497
+ session_id TEXT NOT NULL,
498
+ agent_id TEXT,
499
+ project_id TEXT,
500
+ source TEXT NOT NULL DEFAULT 'manual' CHECK(source IN ('claude-code','codex','manual','open-sessions')),
501
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','processing','completed','failed')),
502
+ transcript TEXT NOT NULL,
503
+ chunk_count INTEGER NOT NULL DEFAULT 0,
504
+ memories_extracted INTEGER NOT NULL DEFAULT 0,
505
+ error TEXT,
506
+ metadata TEXT NOT NULL DEFAULT '{}',
507
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
508
+ started_at TEXT,
509
+ completed_at TEXT
510
+ );
511
+ CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_status ON session_memory_jobs(status);
512
+ CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_agent ON session_memory_jobs(agent_id);
513
+ CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_project ON session_memory_jobs(project_id);
514
+ CREATE INDEX IF NOT EXISTS idx_session_memory_jobs_session ON session_memory_jobs(session_id);
515
+ INSERT OR IGNORE INTO _migrations (id) VALUES (13);
516
+ `
494
517
  ];
495
518
  });
496
519
 
@@ -4499,6 +4522,457 @@ function getSynthesisStatus(runId, projectId, db) {
4499
4522
 
4500
4523
  // src/server/index.ts
4501
4524
  init_synthesis();
4525
+
4526
+ // src/db/session-jobs.ts
4527
+ init_database();
4528
+ function parseJobRow(row) {
4529
+ return {
4530
+ id: row["id"],
4531
+ session_id: row["session_id"],
4532
+ agent_id: row["agent_id"] || null,
4533
+ project_id: row["project_id"] || null,
4534
+ source: row["source"],
4535
+ status: row["status"],
4536
+ transcript: row["transcript"],
4537
+ chunk_count: row["chunk_count"],
4538
+ memories_extracted: row["memories_extracted"],
4539
+ error: row["error"] || null,
4540
+ metadata: JSON.parse(row["metadata"] || "{}"),
4541
+ created_at: row["created_at"],
4542
+ started_at: row["started_at"] || null,
4543
+ completed_at: row["completed_at"] || null
4544
+ };
4545
+ }
4546
+ function createSessionJob(input, db) {
4547
+ const d = db || getDatabase();
4548
+ const id = uuid();
4549
+ const timestamp = now();
4550
+ const source = input.source ?? "manual";
4551
+ const metadata = JSON.stringify(input.metadata ?? {});
4552
+ d.run(`INSERT INTO session_memory_jobs
4553
+ (id, session_id, agent_id, project_id, source, status, transcript, chunk_count, memories_extracted, metadata, created_at)
4554
+ VALUES (?, ?, ?, ?, ?, 'pending', ?, 0, 0, ?, ?)`, [
4555
+ id,
4556
+ input.session_id,
4557
+ input.agent_id ?? null,
4558
+ input.project_id ?? null,
4559
+ source,
4560
+ input.transcript,
4561
+ metadata,
4562
+ timestamp
4563
+ ]);
4564
+ return getSessionJob(id, d);
4565
+ }
4566
+ function getSessionJob(id, db) {
4567
+ const d = db || getDatabase();
4568
+ const row = d.query("SELECT * FROM session_memory_jobs WHERE id = ?").get(id);
4569
+ if (!row)
4570
+ return null;
4571
+ return parseJobRow(row);
4572
+ }
4573
+ function listSessionJobs(filter, db) {
4574
+ const d = db || getDatabase();
4575
+ const conditions = [];
4576
+ const params = [];
4577
+ if (filter?.agent_id) {
4578
+ conditions.push("agent_id = ?");
4579
+ params.push(filter.agent_id);
4580
+ }
4581
+ if (filter?.project_id) {
4582
+ conditions.push("project_id = ?");
4583
+ params.push(filter.project_id);
4584
+ }
4585
+ if (filter?.status) {
4586
+ conditions.push("status = ?");
4587
+ params.push(filter.status);
4588
+ }
4589
+ if (filter?.session_id) {
4590
+ conditions.push("session_id = ?");
4591
+ params.push(filter.session_id);
4592
+ }
4593
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4594
+ const limit = filter?.limit ?? 20;
4595
+ const offset = filter?.offset ?? 0;
4596
+ const rows = d.query(`SELECT * FROM session_memory_jobs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
4597
+ return rows.map(parseJobRow);
4598
+ }
4599
+ function updateSessionJob(id, updates, db) {
4600
+ const d = db || getDatabase();
4601
+ const setClauses = [];
4602
+ const params = [];
4603
+ if (updates.status !== undefined) {
4604
+ setClauses.push("status = ?");
4605
+ params.push(updates.status);
4606
+ }
4607
+ if (updates.chunk_count !== undefined) {
4608
+ setClauses.push("chunk_count = ?");
4609
+ params.push(updates.chunk_count);
4610
+ }
4611
+ if (updates.memories_extracted !== undefined) {
4612
+ setClauses.push("memories_extracted = ?");
4613
+ params.push(updates.memories_extracted);
4614
+ }
4615
+ if ("error" in updates) {
4616
+ setClauses.push("error = ?");
4617
+ params.push(updates.error ?? null);
4618
+ }
4619
+ if ("started_at" in updates) {
4620
+ setClauses.push("started_at = ?");
4621
+ params.push(updates.started_at ?? null);
4622
+ }
4623
+ if ("completed_at" in updates) {
4624
+ setClauses.push("completed_at = ?");
4625
+ params.push(updates.completed_at ?? null);
4626
+ }
4627
+ if (setClauses.length === 0)
4628
+ return getSessionJob(id, d);
4629
+ params.push(id);
4630
+ d.run(`UPDATE session_memory_jobs SET ${setClauses.join(", ")} WHERE id = ?`, params);
4631
+ return getSessionJob(id, d);
4632
+ }
4633
+ function getNextPendingJob(db) {
4634
+ const d = db || getDatabase();
4635
+ const row = d.query("SELECT * FROM session_memory_jobs WHERE status = 'pending' ORDER BY created_at ASC LIMIT 1").get();
4636
+ if (!row)
4637
+ return null;
4638
+ return parseJobRow(row);
4639
+ }
4640
+
4641
+ // src/lib/session-queue.ts
4642
+ init_database();
4643
+
4644
+ // src/lib/session-processor.ts
4645
+ init_memories();
4646
+ init_registry();
4647
+ var SESSION_EXTRACTION_USER_TEMPLATE = (chunk, sessionId) => `Extract memories from this session chunk (session: ${sessionId}):
4648
+
4649
+ ${chunk}
4650
+
4651
+ Return JSON array: [{"key": "...", "value": "...", "category": "knowledge|fact|preference|history", "importance": 1-10, "tags": [...]}]`;
4652
+ function chunkTranscript(transcript, chunkSize = 2000, overlap = 200) {
4653
+ if (!transcript || transcript.length === 0)
4654
+ return [];
4655
+ if (transcript.length <= chunkSize)
4656
+ return [transcript];
4657
+ const chunks = [];
4658
+ let start = 0;
4659
+ while (start < transcript.length) {
4660
+ const end = Math.min(start + chunkSize, transcript.length);
4661
+ chunks.push(transcript.slice(start, end));
4662
+ if (end === transcript.length)
4663
+ break;
4664
+ start += chunkSize - overlap;
4665
+ }
4666
+ return chunks;
4667
+ }
4668
+ async function extractMemoriesFromChunk(chunk, context, db) {
4669
+ const provider = providerRegistry.getAvailable();
4670
+ if (!provider)
4671
+ return 0;
4672
+ try {
4673
+ const extracted = await provider.extractMemories(SESSION_EXTRACTION_USER_TEMPLATE(chunk, context.sessionId), {
4674
+ sessionId: context.sessionId,
4675
+ agentId: context.agentId,
4676
+ projectId: context.projectId
4677
+ });
4678
+ let savedCount = 0;
4679
+ const sourceTag = context.source ? `source:${context.source}` : "source:manual";
4680
+ for (const memory of extracted) {
4681
+ if (!memory.content || !memory.content.trim())
4682
+ continue;
4683
+ try {
4684
+ createMemory({
4685
+ key: memory.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
4686
+ value: memory.content,
4687
+ category: memory.category,
4688
+ scope: memory.suggestedScope ?? "shared",
4689
+ importance: memory.importance,
4690
+ tags: [
4691
+ ...memory.tags,
4692
+ "session-extracted",
4693
+ sourceTag,
4694
+ `session:${context.sessionId}`
4695
+ ],
4696
+ source: "auto",
4697
+ agent_id: context.agentId,
4698
+ project_id: context.projectId,
4699
+ session_id: context.sessionId,
4700
+ metadata: {
4701
+ auto_extracted: true,
4702
+ session_source: context.source ?? "manual",
4703
+ extracted_at: new Date().toISOString(),
4704
+ reasoning: memory.reasoning
4705
+ }
4706
+ }, "merge", db);
4707
+ savedCount++;
4708
+ } catch {}
4709
+ }
4710
+ return savedCount;
4711
+ } catch {
4712
+ try {
4713
+ const fallbacks = providerRegistry.getFallbacks();
4714
+ for (const fallback of fallbacks) {
4715
+ try {
4716
+ const extracted = await fallback.extractMemories(SESSION_EXTRACTION_USER_TEMPLATE(chunk, context.sessionId), {
4717
+ sessionId: context.sessionId,
4718
+ agentId: context.agentId,
4719
+ projectId: context.projectId
4720
+ });
4721
+ let savedCount = 0;
4722
+ const sourceTag = context.source ? `source:${context.source}` : "source:manual";
4723
+ for (const memory of extracted) {
4724
+ if (!memory.content || !memory.content.trim())
4725
+ continue;
4726
+ try {
4727
+ createMemory({
4728
+ key: memory.content.slice(0, 120).replace(/\s+/g, "-").toLowerCase(),
4729
+ value: memory.content,
4730
+ category: memory.category,
4731
+ scope: memory.suggestedScope ?? "shared",
4732
+ importance: memory.importance,
4733
+ tags: [
4734
+ ...memory.tags,
4735
+ "session-extracted",
4736
+ sourceTag,
4737
+ `session:${context.sessionId}`
4738
+ ],
4739
+ source: "auto",
4740
+ agent_id: context.agentId,
4741
+ project_id: context.projectId,
4742
+ session_id: context.sessionId,
4743
+ metadata: {
4744
+ auto_extracted: true,
4745
+ session_source: context.source ?? "manual",
4746
+ extracted_at: new Date().toISOString(),
4747
+ reasoning: memory.reasoning
4748
+ }
4749
+ }, "merge", db);
4750
+ savedCount++;
4751
+ } catch {}
4752
+ }
4753
+ if (savedCount > 0)
4754
+ return savedCount;
4755
+ } catch {
4756
+ continue;
4757
+ }
4758
+ }
4759
+ } catch {}
4760
+ return 0;
4761
+ }
4762
+ }
4763
+ async function processSessionJob(jobId, db) {
4764
+ const result = {
4765
+ jobId,
4766
+ chunksProcessed: 0,
4767
+ memoriesExtracted: 0,
4768
+ errors: []
4769
+ };
4770
+ let job;
4771
+ try {
4772
+ job = getSessionJob(jobId, db);
4773
+ if (!job) {
4774
+ result.errors.push(`Job not found: ${jobId}`);
4775
+ return result;
4776
+ }
4777
+ } catch (e) {
4778
+ result.errors.push(`Failed to fetch job: ${String(e)}`);
4779
+ return result;
4780
+ }
4781
+ try {
4782
+ updateSessionJob(jobId, { status: "processing", started_at: new Date().toISOString() }, db);
4783
+ } catch (e) {
4784
+ result.errors.push(`Failed to mark job as processing: ${String(e)}`);
4785
+ return result;
4786
+ }
4787
+ const chunks = chunkTranscript(job.transcript);
4788
+ try {
4789
+ updateSessionJob(jobId, { chunk_count: chunks.length }, db);
4790
+ } catch {}
4791
+ let totalMemories = 0;
4792
+ for (let i = 0;i < chunks.length; i++) {
4793
+ const chunk = chunks[i];
4794
+ try {
4795
+ const count = await extractMemoriesFromChunk(chunk, {
4796
+ sessionId: job.session_id,
4797
+ agentId: job.agent_id ?? undefined,
4798
+ projectId: job.project_id ?? undefined,
4799
+ source: job.source
4800
+ }, db);
4801
+ totalMemories += count;
4802
+ result.chunksProcessed++;
4803
+ } catch (e) {
4804
+ result.errors.push(`Chunk ${i} failed: ${String(e)}`);
4805
+ }
4806
+ }
4807
+ result.memoriesExtracted = totalMemories;
4808
+ try {
4809
+ if (result.errors.length > 0 && result.chunksProcessed === 0) {
4810
+ updateSessionJob(jobId, {
4811
+ status: "failed",
4812
+ error: result.errors.join("; "),
4813
+ completed_at: new Date().toISOString(),
4814
+ memories_extracted: totalMemories,
4815
+ chunk_count: chunks.length
4816
+ }, db);
4817
+ } else {
4818
+ updateSessionJob(jobId, {
4819
+ status: "completed",
4820
+ completed_at: new Date().toISOString(),
4821
+ memories_extracted: totalMemories,
4822
+ chunk_count: chunks.length
4823
+ }, db);
4824
+ }
4825
+ } catch (e) {
4826
+ result.errors.push(`Failed to update job status: ${String(e)}`);
4827
+ }
4828
+ return result;
4829
+ }
4830
+
4831
+ // src/lib/session-queue.ts
4832
+ var _pendingQueue = new Set;
4833
+ var _isProcessing = false;
4834
+ var _workerStarted = false;
4835
+ function enqueueSessionJob(jobId) {
4836
+ _pendingQueue.add(jobId);
4837
+ if (!_isProcessing) {
4838
+ _processNext();
4839
+ }
4840
+ }
4841
+ function getSessionQueueStats() {
4842
+ try {
4843
+ const db = getDatabase();
4844
+ const rows = db.query("SELECT status, COUNT(*) as count FROM session_memory_jobs GROUP BY status").all();
4845
+ const stats = {
4846
+ pending: 0,
4847
+ processing: 0,
4848
+ completed: 0,
4849
+ failed: 0
4850
+ };
4851
+ for (const row of rows) {
4852
+ if (row.status === "pending")
4853
+ stats.pending = row.count;
4854
+ else if (row.status === "processing")
4855
+ stats.processing = row.count;
4856
+ else if (row.status === "completed")
4857
+ stats.completed = row.count;
4858
+ else if (row.status === "failed")
4859
+ stats.failed = row.count;
4860
+ }
4861
+ return stats;
4862
+ } catch {
4863
+ return {
4864
+ pending: _pendingQueue.size,
4865
+ processing: _isProcessing ? 1 : 0,
4866
+ completed: 0,
4867
+ failed: 0
4868
+ };
4869
+ }
4870
+ }
4871
+ function startSessionQueueWorker() {
4872
+ if (_workerStarted)
4873
+ return;
4874
+ _workerStarted = true;
4875
+ setInterval(() => {
4876
+ _processNext();
4877
+ }, 5000);
4878
+ }
4879
+ async function _processNext() {
4880
+ if (_isProcessing)
4881
+ return;
4882
+ let jobId;
4883
+ if (_pendingQueue.size > 0) {
4884
+ jobId = [..._pendingQueue][0];
4885
+ _pendingQueue.delete(jobId);
4886
+ } else {
4887
+ try {
4888
+ const job = getNextPendingJob();
4889
+ if (job)
4890
+ jobId = job.id;
4891
+ } catch {
4892
+ return;
4893
+ }
4894
+ }
4895
+ if (!jobId)
4896
+ return;
4897
+ _isProcessing = true;
4898
+ try {
4899
+ await processSessionJob(jobId);
4900
+ } catch {} finally {
4901
+ _isProcessing = false;
4902
+ if (_pendingQueue.size > 0) {
4903
+ _processNext();
4904
+ }
4905
+ }
4906
+ }
4907
+
4908
+ // src/lib/session-auto-resolve.ts
4909
+ function autoResolveAgentProject(metadata, db) {
4910
+ let agentId = null;
4911
+ let projectId = null;
4912
+ let confidence = "none";
4913
+ const methods = [];
4914
+ if (metadata.agentName) {
4915
+ try {
4916
+ const agent = getAgent(metadata.agentName, db);
4917
+ if (agent) {
4918
+ agentId = agent.id;
4919
+ confidence = "high";
4920
+ methods.push(`agent-by-name:${metadata.agentName}`);
4921
+ if (agent.active_project_id && !projectId) {
4922
+ projectId = agent.active_project_id;
4923
+ methods.push("project-from-agent-active");
4924
+ }
4925
+ }
4926
+ } catch {}
4927
+ }
4928
+ if (metadata.workingDir && !projectId) {
4929
+ try {
4930
+ const project = getProject(metadata.workingDir, db);
4931
+ if (project) {
4932
+ projectId = project.id;
4933
+ if (confidence !== "high")
4934
+ confidence = "high";
4935
+ methods.push(`project-by-path:${metadata.workingDir}`);
4936
+ } else {
4937
+ const allProjects = listProjects(db);
4938
+ for (const p of allProjects) {
4939
+ if (p.path && metadata.workingDir.startsWith(p.path)) {
4940
+ projectId = p.id;
4941
+ if (confidence !== "high")
4942
+ confidence = "high";
4943
+ methods.push(`project-by-path-prefix:${p.path}`);
4944
+ break;
4945
+ }
4946
+ }
4947
+ }
4948
+ } catch {}
4949
+ }
4950
+ if (metadata.gitRemote && !projectId) {
4951
+ try {
4952
+ const repoName = metadata.gitRemote.replace(/\.git$/, "").split(/[/:]/).filter(Boolean).pop();
4953
+ if (repoName) {
4954
+ const project = getProject(repoName, db);
4955
+ if (project) {
4956
+ projectId = project.id;
4957
+ if (confidence === "none")
4958
+ confidence = "low";
4959
+ methods.push(`project-by-git-remote:${repoName}`);
4960
+ }
4961
+ }
4962
+ } catch {}
4963
+ }
4964
+ if (methods.length === 0) {
4965
+ confidence = "none";
4966
+ }
4967
+ return {
4968
+ agentId,
4969
+ projectId,
4970
+ confidence,
4971
+ method: methods.join(", ") || "none"
4972
+ };
4973
+ }
4974
+
4975
+ // src/server/index.ts
4502
4976
  var DEFAULT_PORT = 19428;
4503
4977
  function parsePort() {
4504
4978
  const envPort = process.env["PORT"];
@@ -5600,8 +6074,51 @@ addRoute("POST", "/api/synthesis/rollback/:run_id", async (_req, _url, params) =
5600
6074
  const result = await rollbackSynthesis(params["run_id"]);
5601
6075
  return json(result);
5602
6076
  });
6077
+ addRoute("POST", "/api/sessions/ingest", async (req) => {
6078
+ const body = await readJson(req) ?? {};
6079
+ const { transcript, session_id, agent_id, project_id, source, metadata } = body;
6080
+ if (!transcript || typeof transcript !== "string")
6081
+ return errorResponse("transcript is required", 400);
6082
+ if (!session_id || typeof session_id !== "string")
6083
+ return errorResponse("session_id is required", 400);
6084
+ let resolvedAgentId = agent_id;
6085
+ let resolvedProjectId = project_id;
6086
+ if (!resolvedAgentId || !resolvedProjectId) {
6087
+ const resolved = autoResolveAgentProject(metadata ?? {});
6088
+ if (!resolvedAgentId && resolved.agentId)
6089
+ resolvedAgentId = resolved.agentId;
6090
+ if (!resolvedProjectId && resolved.projectId)
6091
+ resolvedProjectId = resolved.projectId;
6092
+ }
6093
+ const job = createSessionJob({
6094
+ session_id,
6095
+ transcript,
6096
+ source: source ?? "manual",
6097
+ agent_id: resolvedAgentId,
6098
+ project_id: resolvedProjectId,
6099
+ metadata: metadata ?? {}
6100
+ });
6101
+ enqueueSessionJob(job.id);
6102
+ return json({ job_id: job.id, status: "queued", message: "Session queued for memory extraction" }, 202);
6103
+ });
6104
+ addRoute("GET", "/api/sessions/jobs", (_req, url) => {
6105
+ const agentId = url.searchParams.get("agent_id") ?? undefined;
6106
+ const projectId = url.searchParams.get("project_id") ?? undefined;
6107
+ const status = url.searchParams.get("status") ?? undefined;
6108
+ const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit")) : 20;
6109
+ const jobs = listSessionJobs({ agent_id: agentId, project_id: projectId, status, limit });
6110
+ return json({ jobs, count: jobs.length });
6111
+ });
6112
+ addRoute("GET", "/api/sessions/jobs/:id", (_req, _url, params) => {
6113
+ const job = getSessionJob(params["id"]);
6114
+ if (!job)
6115
+ return errorResponse("Session job not found", 404);
6116
+ return json(job);
6117
+ });
6118
+ addRoute("GET", "/api/sessions/queue/stats", () => json(getSessionQueueStats()));
5603
6119
  function startServer(port) {
5604
6120
  loadWebhooksFromDb();
6121
+ startSessionQueueWorker();
5605
6122
  const hostname = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
5606
6123
  Bun.serve({
5607
6124
  port,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/mementos",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Universal memory system for AI agents - CLI + MCP server + library API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",