@hasna/mementos 0.14.18 → 0.14.20

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.
@@ -9790,6 +9790,7 @@ CREATE TABLE IF NOT EXISTS machines (
9790
9790
  name TEXT NOT NULL UNIQUE,
9791
9791
  hostname TEXT NOT NULL,
9792
9792
  platform TEXT NOT NULL DEFAULT 'unknown',
9793
+ is_primary INTEGER NOT NULL DEFAULT 0,
9793
9794
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
9794
9795
  last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
9795
9796
  );
@@ -10169,33 +10170,44 @@ CREATE INDEX IF NOT EXISTS idx_memories_sequence_group ON memories(sequence_grou
10169
10170
  INSERT OR IGNORE INTO _migrations (id) VALUES (32);
10170
10171
  `,
10171
10172
  `
10172
- ALTER TABLE machines ADD COLUMN is_primary INTEGER NOT NULL DEFAULT 0;
10173
- CREATE INDEX IF NOT EXISTS idx_machines_primary ON machines(is_primary);
10174
- CREATE TRIGGER IF NOT EXISTS machines_single_primary_insert
10175
- AFTER INSERT ON machines
10176
- WHEN NEW.is_primary = 1
10177
- BEGIN
10178
- UPDATE machines
10179
- SET is_primary = 0,
10180
- last_seen_at = COALESCE(NEW.last_seen_at, datetime('now'))
10181
- WHERE id != NEW.id AND is_primary = 1;
10182
- END;
10183
- CREATE TRIGGER IF NOT EXISTS machines_single_primary_update
10184
- AFTER UPDATE OF is_primary ON machines
10185
- WHEN NEW.is_primary = 1
10186
- BEGIN
10187
- UPDATE machines
10188
- SET is_primary = 0,
10189
- last_seen_at = COALESCE(NEW.last_seen_at, datetime('now'))
10190
- WHERE id != NEW.id AND is_primary = 1;
10191
- END;
10192
- CREATE TRIGGER IF NOT EXISTS machines_prevent_delete_primary
10193
- BEFORE DELETE ON machines
10194
- WHEN OLD.is_primary = 1
10195
- BEGIN
10196
- SELECT RAISE(ABORT, 'Primary machine cannot be deleted');
10197
- END;
10198
- INSERT OR IGNORE INTO _migrations (id) VALUES (33);
10173
+ CREATE TABLE IF NOT EXISTS tasks (
10174
+ id TEXT PRIMARY KEY,
10175
+ subject TEXT NOT NULL,
10176
+ description TEXT DEFAULT '',
10177
+ status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
10178
+ priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('critical', 'high', 'medium', 'low')),
10179
+ tags TEXT NOT NULL DEFAULT '[]',
10180
+ assigned_agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
10181
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
10182
+ session_id TEXT,
10183
+ parent_task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
10184
+ metadata TEXT NOT NULL DEFAULT '{}',
10185
+ progress REAL NOT NULL DEFAULT 0 CHECK(progress >= 0 AND progress <= 1),
10186
+ due_at TEXT,
10187
+ started_at TEXT,
10188
+ completed_at TEXT,
10189
+ failed_at TEXT,
10190
+ error TEXT,
10191
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
10192
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
10193
+ );
10194
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
10195
+ CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
10196
+ CREATE INDEX IF NOT EXISTS idx_tasks_agent ON tasks(assigned_agent_id);
10197
+ CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
10198
+ CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id);
10199
+ CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id);
10200
+
10201
+ CREATE TABLE IF NOT EXISTS task_comments (
10202
+ id TEXT PRIMARY KEY,
10203
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
10204
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
10205
+ body TEXT NOT NULL,
10206
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
10207
+ );
10208
+ CREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id);
10209
+ CREATE INDEX IF NOT EXISTS idx_task_comments_agent ON task_comments(agent_id);
10210
+ INSERT OR IGNORE INTO _migrations (id) VALUES (34);
10199
10211
  `
10200
10212
  ];
10201
10213
  });
@@ -10886,7 +10898,7 @@ function createMemory(input, dedupeMode = "merge", db) {
10886
10898
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, ?, ?, ?, ?, ?)`, [
10887
10899
  id,
10888
10900
  input.key,
10889
- input.value,
10901
+ safeValue,
10890
10902
  input.category || "knowledge",
10891
10903
  input.scope || "private",
10892
10904
  input.summary || null,
@@ -11793,6 +11805,10 @@ function buildFilterConditions(filter) {
11793
11805
  params.push(tag);
11794
11806
  }
11795
11807
  }
11808
+ if (filter.namespace) {
11809
+ conditions.push("m.namespace = ?");
11810
+ params.push(filter.namespace);
11811
+ }
11796
11812
  return { conditions, params };
11797
11813
  }
11798
11814
  function searchWithFts5(d, query, queryLower, filter, graphBoostedIds) {
@@ -13756,8 +13772,12 @@ function parseMachine(row) {
13756
13772
  is_primary: Boolean(row.is_primary)
13757
13773
  };
13758
13774
  }
13775
+ function normalizeHostname(host) {
13776
+ return host.replace(/\.(local|lan|home|internal)$/i, "");
13777
+ }
13759
13778
  function registerMachine(name, db = getDatabase()) {
13760
- const host = hostname();
13779
+ const rawHost = hostname();
13780
+ const host = normalizeHostname(rawHost);
13761
13781
  const plat = platform2();
13762
13782
  const machineName = name?.trim() || host;
13763
13783
  const existing = parseMachine(db.query("SELECT * FROM machines WHERE hostname = ?").get(host));
@@ -13795,7 +13815,7 @@ function touchMachine(id, db = getDatabase()) {
13795
13815
  db.run("UPDATE machines SET last_seen_at = ? WHERE id = ?", [now(), id]);
13796
13816
  }
13797
13817
  function getCurrentMachineId(db = getDatabase()) {
13798
- const host = hostname();
13818
+ const host = normalizeHostname(hostname());
13799
13819
  const m = db.query("SELECT id FROM machines WHERE hostname = ?").get(host);
13800
13820
  if (m) {
13801
13821
  touchMachine(m.id, db);
@@ -14841,6 +14861,374 @@ async function _processNext() {
14841
14861
  }
14842
14862
  }
14843
14863
 
14864
+ // src/lib/task-runner.ts
14865
+ init_database();
14866
+
14867
+ // src/db/tasks.ts
14868
+ init_database();
14869
+ function parseTask(row) {
14870
+ return {
14871
+ id: row.id,
14872
+ subject: row.subject,
14873
+ description: row.description ?? "",
14874
+ status: row.status,
14875
+ priority: row.priority,
14876
+ tags: JSON.parse(row.tags ?? "[]"),
14877
+ assigned_agent_id: row.assigned_agent_id ?? null,
14878
+ project_id: row.project_id ?? null,
14879
+ session_id: row.session_id ?? null,
14880
+ parent_task_id: row.parent_task_id ?? null,
14881
+ metadata: JSON.parse(row.metadata ?? "{}"),
14882
+ progress: row.progress,
14883
+ due_at: row.due_at ?? null,
14884
+ started_at: row.started_at ?? null,
14885
+ completed_at: row.completed_at ?? null,
14886
+ failed_at: row.failed_at ?? null,
14887
+ error: row.error ?? null,
14888
+ created_at: row.created_at,
14889
+ updated_at: row.updated_at
14890
+ };
14891
+ }
14892
+ function createTask(db, input) {
14893
+ const id = uuid();
14894
+ const ts = now();
14895
+ db.run(`INSERT INTO tasks (id, subject, description, status, priority, tags, assigned_agent_id, project_id, session_id, parent_task_id, metadata, due_at, created_at, updated_at)
14896
+ VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
14897
+ id,
14898
+ input.subject,
14899
+ input.description ?? "",
14900
+ input.priority ?? "medium",
14901
+ JSON.stringify(input.tags ?? []),
14902
+ input.assigned_agent_id ?? null,
14903
+ input.project_id ?? null,
14904
+ input.session_id ?? null,
14905
+ input.parent_task_id ?? null,
14906
+ JSON.stringify(input.metadata ?? {}),
14907
+ input.due_at ?? null,
14908
+ ts,
14909
+ ts
14910
+ ]);
14911
+ const row = db.query("SELECT * FROM tasks WHERE id = ?").get(id);
14912
+ if (!row) {
14913
+ throw new Error(`Failed to load task after create: ${id}`);
14914
+ }
14915
+ return parseTask(row);
14916
+ }
14917
+ function getTask(db, id) {
14918
+ const row = db.query("SELECT * FROM tasks WHERE id = ?").get(id);
14919
+ return row ? parseTask(row) : null;
14920
+ }
14921
+ function listTasks(db, filter) {
14922
+ let sql = "SELECT * FROM tasks WHERE 1=1";
14923
+ let countSql = "SELECT COUNT(*) as c FROM tasks WHERE 1=1";
14924
+ const params = [];
14925
+ const countParams = [];
14926
+ if (filter?.status) {
14927
+ sql += " AND status = ?";
14928
+ countSql += " AND status = ?";
14929
+ params.push(filter.status);
14930
+ countParams.push(filter.status);
14931
+ }
14932
+ if (filter?.priority) {
14933
+ sql += " AND priority = ?";
14934
+ countSql += " AND priority = ?";
14935
+ params.push(filter.priority);
14936
+ countParams.push(filter.priority);
14937
+ }
14938
+ if (filter?.assigned_agent_id) {
14939
+ sql += " AND assigned_agent_id = ?";
14940
+ countSql += " AND assigned_agent_id = ?";
14941
+ params.push(filter.assigned_agent_id);
14942
+ countParams.push(filter.assigned_agent_id);
14943
+ }
14944
+ if (filter?.project_id) {
14945
+ sql += " AND project_id = ?";
14946
+ countSql += " AND project_id = ?";
14947
+ params.push(filter.project_id);
14948
+ countParams.push(filter.project_id);
14949
+ }
14950
+ if (filter?.session_id) {
14951
+ sql += " AND session_id = ?";
14952
+ countSql += " AND session_id = ?";
14953
+ params.push(filter.session_id);
14954
+ countParams.push(filter.session_id);
14955
+ }
14956
+ if (filter?.parent_task_id !== undefined) {
14957
+ if (filter.parent_task_id === null) {
14958
+ sql += " AND parent_task_id IS NULL";
14959
+ countSql += " AND parent_task_id IS NULL";
14960
+ } else {
14961
+ sql += " AND parent_task_id = ?";
14962
+ countSql += " AND parent_task_id = ?";
14963
+ params.push(filter.parent_task_id);
14964
+ countParams.push(filter.parent_task_id);
14965
+ }
14966
+ }
14967
+ if (filter?.tags?.length) {
14968
+ const placeholders = filter.tags.map(() => "?").join(", ");
14969
+ sql += ` AND EXISTS (SELECT 1 FROM json_each(tags) WHERE value IN (${placeholders}))`;
14970
+ countSql += ` AND EXISTS (SELECT 1 FROM json_each(tags) WHERE value IN (${placeholders}))`;
14971
+ params.push(...filter.tags);
14972
+ countParams.push(...filter.tags);
14973
+ }
14974
+ sql += " ORDER BY CASE priority WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 WHEN 'low' THEN 4 END, created_at ASC";
14975
+ if (filter?.limit !== undefined) {
14976
+ sql += " LIMIT ?";
14977
+ params.push(filter.limit);
14978
+ }
14979
+ if (filter?.offset !== undefined) {
14980
+ sql += " OFFSET ?";
14981
+ params.push(filter.offset);
14982
+ }
14983
+ const rows = db.query(sql).all(...params);
14984
+ const countRow = db.query(countSql).get(...countParams);
14985
+ return { tasks: rows.map(parseTask), count: countRow.c };
14986
+ }
14987
+ function updateTask(db, id, input) {
14988
+ const existing = getTask(db, id);
14989
+ if (!existing)
14990
+ return null;
14991
+ const updates = [];
14992
+ const params = [];
14993
+ const ts = now();
14994
+ if (input.subject !== undefined) {
14995
+ updates.push("subject = ?");
14996
+ params.push(input.subject);
14997
+ }
14998
+ if (input.description !== undefined) {
14999
+ updates.push("description = ?");
15000
+ params.push(input.description);
15001
+ }
15002
+ if (input.status !== undefined) {
15003
+ updates.push("status = ?");
15004
+ params.push(input.status);
15005
+ if (input.status === "in_progress" && !existing.started_at) {
15006
+ updates.push("started_at = ?");
15007
+ params.push(ts);
15008
+ }
15009
+ if (input.status === "completed") {
15010
+ updates.push("completed_at = ?");
15011
+ params.push(ts);
15012
+ updates.push("progress = ?");
15013
+ params.push(1);
15014
+ }
15015
+ if (input.status === "failed") {
15016
+ updates.push("failed_at = ?");
15017
+ params.push(ts);
15018
+ }
15019
+ }
15020
+ if (input.priority !== undefined) {
15021
+ updates.push("priority = ?");
15022
+ params.push(input.priority);
15023
+ }
15024
+ if (input.tags !== undefined) {
15025
+ updates.push("tags = ?");
15026
+ params.push(JSON.stringify(input.tags));
15027
+ }
15028
+ if (input.assigned_agent_id !== undefined) {
15029
+ updates.push("assigned_agent_id = ?");
15030
+ params.push(input.assigned_agent_id);
15031
+ }
15032
+ if (input.metadata !== undefined) {
15033
+ updates.push("metadata = ?");
15034
+ params.push(JSON.stringify(input.metadata));
15035
+ }
15036
+ if (input.progress !== undefined) {
15037
+ updates.push("progress = ?");
15038
+ params.push(input.progress);
15039
+ }
15040
+ if (input.due_at !== undefined) {
15041
+ updates.push("due_at = ?");
15042
+ params.push(input.due_at);
15043
+ }
15044
+ if (input.error !== undefined) {
15045
+ updates.push("error = ?");
15046
+ params.push(input.error);
15047
+ }
15048
+ if (updates.length === 0)
15049
+ return existing;
15050
+ updates.push("updated_at = ?");
15051
+ params.push(ts);
15052
+ params.push(id);
15053
+ db.run(`UPDATE tasks SET ${updates.join(", ")} WHERE id = ?`, ...params);
15054
+ return getTask(db, id);
15055
+ }
15056
+ function deleteTask(db, id) {
15057
+ const result = db.run("DELETE FROM tasks WHERE id = ?", id);
15058
+ return result.changes > 0;
15059
+ }
15060
+ function addTaskComment(db, taskId, body, agentId) {
15061
+ const id = uuid();
15062
+ db.run("INSERT INTO task_comments (id, task_id, agent_id, body) VALUES (?, ?, ?, ?)", [id, taskId, agentId ?? null, body]);
15063
+ const row = db.query("SELECT * FROM task_comments WHERE id = ?").get(id);
15064
+ return parseTaskComment(row);
15065
+ }
15066
+ function parseTaskComment(row) {
15067
+ return {
15068
+ id: row.id,
15069
+ task_id: row.task_id,
15070
+ agent_id: row.agent_id ?? null,
15071
+ body: row.body,
15072
+ created_at: row.created_at
15073
+ };
15074
+ }
15075
+ function listTaskComments(db, taskId) {
15076
+ const rows = db.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at ASC").all(taskId);
15077
+ return { comments: rows.map(parseTaskComment), count: rows.length };
15078
+ }
15079
+ function deleteTaskComment(db, id) {
15080
+ const result = db.run("DELETE FROM task_comments WHERE id = ?", id);
15081
+ return result.changes > 0;
15082
+ }
15083
+ function getTaskStats(db, filter) {
15084
+ let where = "WHERE 1=1";
15085
+ const params = [];
15086
+ if (filter?.project_id) {
15087
+ where += " AND project_id = ?";
15088
+ params.push(filter.project_id);
15089
+ }
15090
+ if (filter?.agent_id) {
15091
+ where += " AND assigned_agent_id = ?";
15092
+ params.push(filter.agent_id);
15093
+ }
15094
+ const total = db.query(`SELECT COUNT(*) as c FROM tasks ${where}`).get(...params).c;
15095
+ const byStatus = {};
15096
+ const statusRows = db.query(`SELECT status, COUNT(*) as c FROM tasks ${where} GROUP BY status`).all(...params);
15097
+ for (const row of statusRows)
15098
+ byStatus[row.status] = row.c;
15099
+ const byPriority = {};
15100
+ const priorityRows = db.query(`SELECT priority, COUNT(*) as c FROM tasks ${where} GROUP BY priority`).all(...params);
15101
+ for (const row of priorityRows)
15102
+ byPriority[row.priority] = row.c;
15103
+ const overdue = db.query(`SELECT COUNT(*) as c FROM tasks ${where} AND status != 'completed' AND status != 'cancelled' AND due_at IS NOT NULL AND due_at < datetime('now')`).get(...params).c;
15104
+ return {
15105
+ total,
15106
+ by_status: {
15107
+ pending: byStatus["pending"] ?? 0,
15108
+ in_progress: byStatus["in_progress"] ?? 0,
15109
+ completed: byStatus["completed"] ?? 0,
15110
+ failed: byStatus["failed"] ?? 0,
15111
+ cancelled: byStatus["cancelled"] ?? 0
15112
+ },
15113
+ by_priority: {
15114
+ critical: byPriority["critical"] ?? 0,
15115
+ high: byPriority["high"] ?? 0,
15116
+ medium: byPriority["medium"] ?? 0,
15117
+ low: byPriority["low"] ?? 0
15118
+ },
15119
+ overdue
15120
+ };
15121
+ }
15122
+
15123
+ // src/lib/task-runner.ts
15124
+ var _handlers = new Map;
15125
+ var _defaultHandler = null;
15126
+ function resolveHandler(task) {
15127
+ for (const tag of task.tags) {
15128
+ const h = _handlers.get(tag);
15129
+ if (h)
15130
+ return h;
15131
+ }
15132
+ const metaType = task.metadata?.["type"];
15133
+ if (metaType) {
15134
+ const h = _handlers.get(metaType);
15135
+ if (h)
15136
+ return h;
15137
+ }
15138
+ return _defaultHandler;
15139
+ }
15140
+ var _workerStarted2 = false;
15141
+ var _processing = false;
15142
+ var _totalProcessed = 0;
15143
+ var _pollIntervalMs = 60000;
15144
+ function startTaskRunner(intervalMs) {
15145
+ if (_workerStarted2)
15146
+ return;
15147
+ _workerStarted2 = true;
15148
+ if (intervalMs)
15149
+ _pollIntervalMs = intervalMs;
15150
+ console.log(`[task-runner] Started, polling every ${_pollIntervalMs / 1000}s`);
15151
+ _tick();
15152
+ setInterval(() => {
15153
+ _tick();
15154
+ }, _pollIntervalMs);
15155
+ }
15156
+ async function _tick() {
15157
+ if (_processing)
15158
+ return;
15159
+ let task = null;
15160
+ try {
15161
+ const db = getDatabase();
15162
+ const result = listTasks(db, { status: "pending", limit: 1 });
15163
+ if (result.tasks.length === 0)
15164
+ return;
15165
+ task = result.tasks[0] ?? null;
15166
+ } catch (e) {
15167
+ console.error("[task-runner] Failed to list pending tasks:", e);
15168
+ return;
15169
+ }
15170
+ if (!task)
15171
+ return;
15172
+ _processing = true;
15173
+ try {
15174
+ const db = getDatabase();
15175
+ updateTask(db, task.id, { status: "in_progress" });
15176
+ task = getTask(db, task.id);
15177
+ } catch (e) {
15178
+ console.error("[task-runner] Failed to claim task:", e);
15179
+ _processing = false;
15180
+ return;
15181
+ }
15182
+ const handler = resolveHandler(task);
15183
+ if (!handler) {
15184
+ console.warn(`[task-runner] No handler for task: ${task.subject} (tags: ${JSON.stringify(task.tags)}, meta.type: ${task.metadata?.["type"]})`);
15185
+ try {
15186
+ const db = getDatabase();
15187
+ updateTask(db, task.id, {
15188
+ status: "failed",
15189
+ error: "No handler registered for this task type"
15190
+ });
15191
+ } catch {}
15192
+ _processing = false;
15193
+ _totalProcessed++;
15194
+ return;
15195
+ }
15196
+ const taskId = task.id;
15197
+ let progress = 0;
15198
+ const ctx = {
15199
+ task,
15200
+ updateProgress: (p) => {
15201
+ progress = Math.min(1, Math.max(0, p));
15202
+ try {
15203
+ const db = getDatabase();
15204
+ updateTask(db, taskId, { progress });
15205
+ } catch {}
15206
+ },
15207
+ addComment: (body, agentId) => {
15208
+ try {
15209
+ const db = getDatabase();
15210
+ addTaskComment(db, taskId, body, agentId);
15211
+ } catch {}
15212
+ }
15213
+ };
15214
+ try {
15215
+ await handler(ctx);
15216
+ const db = getDatabase();
15217
+ updateTask(db, taskId, { status: "completed", progress: 1 });
15218
+ console.log(`[task-runner] Completed: ${task.subject}`);
15219
+ } catch (e) {
15220
+ const errorMsg = e instanceof Error ? e.message : String(e);
15221
+ try {
15222
+ const db = getDatabase();
15223
+ updateTask(db, taskId, { status: "failed", error: errorMsg });
15224
+ } catch {}
15225
+ console.error(`[task-runner] Failed: ${task.subject} \u2014 ${errorMsg}`);
15226
+ } finally {
15227
+ _processing = false;
15228
+ _totalProcessed++;
15229
+ }
15230
+ }
15231
+
14844
15232
  // src/server/router.ts
14845
15233
  var routes = [];
14846
15234
  var nextRouteOrder = 0;
@@ -14917,8 +15305,13 @@ function errorResponse(message, status, details) {
14917
15305
  body["details"] = details;
14918
15306
  return json(body, status);
14919
15307
  }
15308
+ var MAX_BODY_BYTES = 1 * 1024 * 1024;
14920
15309
  async function readJson(req) {
14921
15310
  try {
15311
+ const contentLength = req.headers.get("content-length");
15312
+ if (contentLength && Number(contentLength) > MAX_BODY_BYTES) {
15313
+ throw Object.assign(new Error("Payload too large"), { status: 413 });
15314
+ }
14922
15315
  return await req.json();
14923
15316
  } catch {
14924
15317
  return null;
@@ -15057,6 +15450,8 @@ addRoute("GET", "/api/memories", (_req, url) => {
15057
15450
  filter.project_id = q["project_id"];
15058
15451
  if (q["session_id"])
15059
15452
  filter.session_id = q["session_id"];
15453
+ if (q["namespace"])
15454
+ filter.namespace = q["namespace"];
15060
15455
  if (q["status"])
15061
15456
  filter.status = q["status"];
15062
15457
  if (q["limit"])
@@ -15316,6 +15711,10 @@ addRoute("POST", "/api/memories/search", async (req) => {
15316
15711
  filter.category = body["category"];
15317
15712
  if (body["tags"])
15318
15713
  filter.tags = body["tags"];
15714
+ if (body["session_id"])
15715
+ filter.session_id = body["session_id"];
15716
+ if (body["namespace"])
15717
+ filter.namespace = body["namespace"];
15319
15718
  if (body["limit"])
15320
15719
  filter.limit = body["limit"];
15321
15720
  const results = searchMemories(body["query"], filter);
@@ -15337,6 +15736,10 @@ addRoute("POST", "/api/memories/search/hybrid", async (req) => {
15337
15736
  filter.agent_id = body["agent_id"];
15338
15737
  if (body["project_id"])
15339
15738
  filter.project_id = body["project_id"];
15739
+ if (body["session_id"])
15740
+ filter.session_id = body["session_id"];
15741
+ if (body["namespace"])
15742
+ filter.namespace = body["namespace"];
15340
15743
  const results = await hybridSearch(body["query"], {
15341
15744
  filter,
15342
15745
  semantic_threshold: body["semantic_threshold"] ?? undefined,
@@ -15360,6 +15763,10 @@ addRoute("POST", "/api/memories/search/bm25", async (req) => {
15360
15763
  filter.agent_id = body["agent_id"];
15361
15764
  if (body["project_id"])
15362
15765
  filter.project_id = body["project_id"];
15766
+ if (body["session_id"])
15767
+ filter.session_id = body["session_id"];
15768
+ if (body["namespace"])
15769
+ filter.namespace = body["namespace"];
15363
15770
  if (body["limit"])
15364
15771
  filter.limit = body["limit"];
15365
15772
  const results = searchWithBm25(body["query"], filter);
@@ -16248,6 +16655,81 @@ addRoute("GET", "/api/graph/:entityId", (_req, url, params) => {
16248
16655
  }
16249
16656
  });
16250
16657
 
16658
+ // src/server/routes/tasks.ts
16659
+ init_database();
16660
+ addRoute("POST", "/api/tasks", async (req, _url) => {
16661
+ const body = await readJson(req);
16662
+ if (!body)
16663
+ return errorResponse("Invalid JSON body", 400);
16664
+ if (!body.subject)
16665
+ return errorResponse("subject is required", 400);
16666
+ const task = createTask(getDatabase(), body);
16667
+ return json(task, 201);
16668
+ });
16669
+ addRoute("GET", "/api/tasks", async (_req, url) => {
16670
+ const q = getSearchParams(url);
16671
+ const filter = {};
16672
+ for (const key of ["status", "priority", "assigned_agent_id", "project_id", "session_id", "parent_task_id"]) {
16673
+ if (q[key])
16674
+ filter[key] = q[key];
16675
+ }
16676
+ if (q["tags"])
16677
+ filter.tags = q["tags"].split(",");
16678
+ if (q["limit"])
16679
+ filter.limit = parseInt(q["limit"]);
16680
+ if (q["offset"])
16681
+ filter.offset = parseInt(q["offset"]);
16682
+ return json(listTasks(getDatabase(), filter));
16683
+ });
16684
+ addRoute("GET", "/api/tasks/stats", async (_req, url) => {
16685
+ const q = getSearchParams(url);
16686
+ const filter = {};
16687
+ if (q["project_id"])
16688
+ filter.project_id = q["project_id"];
16689
+ if (q["agent_id"])
16690
+ filter.agent_id = q["agent_id"];
16691
+ return json(getTaskStats(getDatabase(), filter));
16692
+ });
16693
+ addRoute("GET", "/api/tasks/:id", async (_req, _url, params) => {
16694
+ const task = getTask(getDatabase(), params.id);
16695
+ if (!task)
16696
+ return errorResponse("Task not found", 404);
16697
+ return json(task);
16698
+ });
16699
+ addRoute("PATCH", "/api/tasks/:id", async (req, _url, params) => {
16700
+ const body = await readJson(req);
16701
+ if (!body)
16702
+ return errorResponse("Invalid JSON body", 400);
16703
+ const task = updateTask(getDatabase(), params.id, body);
16704
+ if (!task)
16705
+ return errorResponse("Task not found", 404);
16706
+ return json(task);
16707
+ });
16708
+ addRoute("DELETE", "/api/tasks/:id", async (_req, _url, params) => {
16709
+ const deleted = deleteTask(getDatabase(), params.id);
16710
+ if (!deleted)
16711
+ return errorResponse("Task not found", 404);
16712
+ return json({ deleted: true });
16713
+ });
16714
+ addRoute("GET", "/api/tasks/:id/comments", async (_req, _url, params) => {
16715
+ return json(listTaskComments(getDatabase(), params.id));
16716
+ });
16717
+ addRoute("POST", "/api/tasks/:id/comments", async (req, _url, params) => {
16718
+ const body = await readJson(req);
16719
+ if (!body)
16720
+ return errorResponse("Invalid JSON body", 400);
16721
+ if (!body.body)
16722
+ return errorResponse("body is required", 400);
16723
+ const comment = addTaskComment(getDatabase(), params.id, body.body, body.agent_id);
16724
+ return json(comment, 201);
16725
+ });
16726
+ addRoute("DELETE", "/api/tasks/:id/comments/:commentId", async (_req, _url, params) => {
16727
+ const deleted = deleteTaskComment(getDatabase(), params.commentId);
16728
+ if (!deleted)
16729
+ return errorResponse("Comment not found", 404);
16730
+ return json({ deleted: true });
16731
+ });
16732
+
16251
16733
  // src/server/routes/system-auto-memory.ts
16252
16734
  init_auto_memory();
16253
16735
  init_registry();
@@ -16295,14 +16777,18 @@ function registerSystemAutoMemoryRoutes() {
16295
16777
  const provider = providerName ? providerRegistry.getProvider(providerName) : providerRegistry.getAvailable();
16296
16778
  if (!provider)
16297
16779
  return errorResponse("No LLM provider configured. Set an API key (ANTHROPIC_API_KEY, OPENAI_API_KEY, CEREBRAS_API_KEY, or XAI_API_KEY).", 503);
16298
- const memories = await provider.extractMemories(turn, { agentId: agent_id, projectId: project_id });
16299
- return json({
16300
- provider: provider.name,
16301
- model: provider.config.model,
16302
- extracted: memories,
16303
- count: memories.length,
16304
- note: "DRY RUN \u2014 nothing was saved"
16305
- });
16780
+ try {
16781
+ const memories = await provider.extractMemories(turn, { agentId: agent_id, projectId: project_id });
16782
+ return json({
16783
+ provider: provider.name,
16784
+ model: provider.config.model,
16785
+ extracted: memories,
16786
+ count: memories.length,
16787
+ note: "DRY RUN \u2014 nothing was saved"
16788
+ });
16789
+ } catch (e) {
16790
+ return json({ error: e instanceof Error ? e.message : String(e) }, 500);
16791
+ }
16306
16792
  });
16307
16793
  }
16308
16794
 
@@ -17284,14 +17770,18 @@ init_profile_synthesizer();
17284
17770
  function registerSystemSynthesisRoutes() {
17285
17771
  addRoute("POST", "/api/synthesis/run", async (req) => {
17286
17772
  const body = await readJson(req) ?? {};
17287
- const result = await runSynthesis({
17288
- projectId: body.project_id,
17289
- agentId: body.agent_id,
17290
- dryRun: body.dry_run,
17291
- maxProposals: body.max_proposals,
17292
- provider: body.provider
17293
- });
17294
- return json(result, result.dryRun ? 200 : 201);
17773
+ try {
17774
+ const result = await runSynthesis({
17775
+ projectId: body.project_id,
17776
+ agentId: body.agent_id,
17777
+ dryRun: body.dry_run,
17778
+ maxProposals: body.max_proposals,
17779
+ provider: body.provider
17780
+ });
17781
+ return json(result, result.dryRun ? 200 : 201);
17782
+ } catch (e) {
17783
+ return json({ error: e instanceof Error ? e.message : String(e) }, 500);
17784
+ }
17295
17785
  });
17296
17786
  addRoute("GET", "/api/synthesis/runs", (_req, url) => {
17297
17787
  const projectId = url.searchParams.get("project_id") ?? undefined;
@@ -17305,20 +17795,28 @@ function registerSystemSynthesisRoutes() {
17305
17795
  return json(getSynthesisStatus(runId, projectId));
17306
17796
  });
17307
17797
  addRoute("POST", "/api/synthesis/rollback/:run_id", async (_req, _url, params) => {
17308
- const result = await rollbackSynthesis(params["run_id"]);
17309
- return json(result);
17798
+ try {
17799
+ const result = await rollbackSynthesis(params["run_id"]);
17800
+ return json(result);
17801
+ } catch (e) {
17802
+ return json({ error: e instanceof Error ? e.message : String(e) }, 500);
17803
+ }
17310
17804
  });
17311
17805
  addRoute("GET", "/api/profile/synthesize", async (_req, url) => {
17312
17806
  const q = getSearchParams(url);
17313
- const result = await synthesizeProfile({
17314
- project_id: q["project_id"] || undefined,
17315
- agent_id: q["agent_id"] || undefined,
17316
- force_refresh: q["force_refresh"] === "true"
17317
- });
17318
- if (!result) {
17319
- return json({ profile: null, message: "No preference/fact memories found to synthesize" });
17807
+ try {
17808
+ const result = await synthesizeProfile({
17809
+ project_id: q["project_id"] || undefined,
17810
+ agent_id: q["agent_id"] || undefined,
17811
+ force_refresh: q["force_refresh"] === "true"
17812
+ });
17813
+ if (!result) {
17814
+ return json({ profile: null, message: "No preference/fact memories found to synthesize" });
17815
+ }
17816
+ return json(result);
17817
+ } catch (e) {
17818
+ return json({ error: e instanceof Error ? e.message : String(e) }, 500);
17320
17819
  }
17321
- return json(result);
17322
17820
  });
17323
17821
  }
17324
17822
 
@@ -17582,126 +18080,116 @@ function initServer() {
17582
18080
  warnIfPrimaryMachineUnset();
17583
18081
  loadWebhooksFromDb();
17584
18082
  startSessionQueueWorker();
18083
+ startTaskRunner();
17585
18084
  }
17586
- function startServer(port, attempt = 0) {
17587
- const maxRetries = 100;
18085
+ function startServer(port) {
17588
18086
  initServer();
17589
18087
  const hostname2 = process.env["MEMENTOS_HOST"] ?? "127.0.0.1";
17590
- try {
17591
- Bun.serve({
17592
- port,
17593
- hostname: hostname2,
17594
- async fetch(req) {
17595
- const url = new URL(req.url);
17596
- const { pathname } = url;
17597
- if (req.method === "OPTIONS") {
17598
- const origin = req.headers.get("origin");
17599
- const allowedOrigin = process.env["MEMENTOS_CORS_ORIGIN"] ?? "http://localhost:19428";
17600
- if (origin && origin !== allowedOrigin) {
17601
- return new Response(null, { status: 403 });
17602
- }
17603
- return new Response(null, { status: 204, headers: getCorsHeaders(req) });
17604
- }
17605
- if (pathname === "/api/health" || pathname === "/health") {
17606
- const profile = getActiveProfile();
17607
- const { createRequire: createRequire2 } = await import("module");
17608
- const req2 = createRequire2(import.meta.url);
17609
- const pkg = req2("../../package.json");
17610
- const db = getDatabase();
17611
- const total = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active'").get().c;
17612
- const expired = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < datetime('now'))").get().c;
17613
- const pinned = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active' AND pinned = 1").get().c;
17614
- const agents = db.query("SELECT COUNT(*) as c FROM agents").get().c;
17615
- const projects = db.query("SELECT COUNT(*) as c FROM projects").get().c;
17616
- const status = expired > 50 ? "warn" : "ok";
17617
- return json({ status, version: pkg.version, profile: profile ?? "default", db_path: getDbPath(), hostname: hostname2, memories: { total, expired, pinned }, agents, projects });
17618
- }
17619
- if (pathname.startsWith("/api/") && pathname !== "/api/health") {
17620
- const authError = authenticateRequest(req);
17621
- if (authError)
17622
- return authError;
17623
- }
17624
- if (pathname === "/api/profile" && req.method === "GET") {
17625
- const profile = getActiveProfile();
17626
- return json({ active: profile ?? null, profiles: listProfiles(), db_path: getDbPath() });
17627
- }
17628
- if (pathname === "/api/memories/stream" && req.method === "GET") {
17629
- const stream = new ReadableStream({
17630
- start(controller) {
17631
- const encoder = new TextEncoder;
17632
- let lastSeen = new Date().toISOString();
17633
- const send = (data) => {
17634
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
18088
+ Bun.serve({
18089
+ port,
18090
+ hostname: hostname2,
18091
+ maxRequestBodySize: 1048576,
18092
+ async fetch(req) {
18093
+ const url = new URL(req.url);
18094
+ const { pathname } = url;
18095
+ if (req.method === "OPTIONS") {
18096
+ const origin = req.headers.get("origin");
18097
+ const allowedOrigin = process.env["MEMENTOS_CORS_ORIGIN"] ?? "http://localhost:19428";
18098
+ if (origin && origin !== allowedOrigin) {
18099
+ return new Response(null, { status: 403 });
18100
+ }
18101
+ return new Response(null, { status: 204, headers: getCorsHeaders(req) });
18102
+ }
18103
+ if (pathname === "/api/health" || pathname === "/health") {
18104
+ const profile = getActiveProfile();
18105
+ const { createRequire: createRequire2 } = await import("module");
18106
+ const req2 = createRequire2(import.meta.url);
18107
+ const pkg = req2("../../package.json");
18108
+ const db = getDatabase();
18109
+ const total = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active'").get().c;
18110
+ const expired = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'expired' OR (expires_at IS NOT NULL AND expires_at < datetime('now'))").get().c;
18111
+ const pinned = db.query("SELECT COUNT(*) as c FROM memories WHERE status = 'active' AND pinned = 1").get().c;
18112
+ const agents = db.query("SELECT COUNT(*) as c FROM agents").get().c;
18113
+ const projects = db.query("SELECT COUNT(*) as c FROM projects").get().c;
18114
+ const status = expired > 50 ? "warn" : "ok";
18115
+ return json({ status, version: pkg.version, profile: profile ?? "default", db_path: getDbPath(), hostname: hostname2, memories: { total, expired, pinned }, agents, projects });
18116
+ }
18117
+ if (pathname.startsWith("/api/") && pathname !== "/api/health") {
18118
+ const authError = authenticateRequest(req);
18119
+ if (authError)
18120
+ return authError;
18121
+ }
18122
+ if (pathname === "/api/profile" && req.method === "GET") {
18123
+ const profile = getActiveProfile();
18124
+ return json({ active: profile ?? null, profiles: listProfiles(), db_path: getDbPath() });
18125
+ }
18126
+ if (pathname === "/api/memories/stream" && req.method === "GET") {
18127
+ const stream = new ReadableStream({
18128
+ start(controller) {
18129
+ const encoder = new TextEncoder;
18130
+ let lastSeen = new Date().toISOString();
18131
+ const send = (data) => {
18132
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
17635
18133
 
17636
18134
  `));
17637
- };
17638
- send({ type: "connected", timestamp: lastSeen });
17639
- const interval = setInterval(() => {
17640
- try {
17641
- const db = getDatabase();
17642
- const rows = db.query("SELECT * FROM memories WHERE updated_at > ? OR created_at > ? ORDER BY updated_at DESC LIMIT 50").all(lastSeen, lastSeen);
17643
- if (rows.length > 0) {
17644
- lastSeen = new Date().toISOString();
17645
- send({ type: "memories", data: rows, count: rows.length });
17646
- }
17647
- } catch {}
17648
- }, 1000);
17649
- req.signal.addEventListener("abort", () => {
17650
- clearInterval(interval);
17651
- controller.close();
17652
- });
17653
- }
17654
- });
17655
- return new Response(stream, {
17656
- headers: {
17657
- "Content-Type": "text/event-stream",
17658
- "Cache-Control": "no-cache",
17659
- Connection: "keep-alive",
17660
- ...CORS_HEADERS
17661
- }
17662
- });
17663
- }
17664
- const matched = matchRoute(req.method, pathname);
17665
- if (!matched) {
17666
- if (pathname.startsWith("/api/")) {
17667
- return errorResponse("Not found", 404);
18135
+ };
18136
+ send({ type: "connected", timestamp: lastSeen });
18137
+ const interval = setInterval(() => {
18138
+ try {
18139
+ const db = getDatabase();
18140
+ const rows = db.query("SELECT * FROM memories WHERE updated_at > ? OR created_at > ? ORDER BY updated_at DESC LIMIT 50").all(lastSeen, lastSeen);
18141
+ if (rows.length > 0) {
18142
+ lastSeen = new Date().toISOString();
18143
+ send({ type: "memories", data: rows, count: rows.length });
18144
+ }
18145
+ } catch {}
18146
+ }, 1000);
18147
+ req.signal.addEventListener("abort", () => {
18148
+ clearInterval(interval);
18149
+ controller.close();
18150
+ });
17668
18151
  }
17669
- const dashDir = resolveDashboardDir();
17670
- if (existsSync7(dashDir) && (req.method === "GET" || req.method === "HEAD")) {
17671
- if (pathname !== "/") {
17672
- const resolvedDash = resolve3(dashDir) + sep;
17673
- const requestedPath = resolve3(join8(dashDir, pathname));
17674
- if (requestedPath.startsWith(resolvedDash)) {
17675
- const staticRes = serveStaticFile(requestedPath);
17676
- if (staticRes)
17677
- return staticRes;
17678
- }
17679
- }
17680
- const indexRes = serveStaticFile(join8(dashDir, "index.html"));
17681
- if (indexRes)
17682
- return indexRes;
18152
+ });
18153
+ return new Response(stream, {
18154
+ headers: {
18155
+ "Content-Type": "text/event-stream",
18156
+ "Cache-Control": "no-cache",
18157
+ Connection: "keep-alive",
18158
+ ...CORS_HEADERS
17683
18159
  }
18160
+ });
18161
+ }
18162
+ const matched = matchRoute(req.method, pathname);
18163
+ if (!matched) {
18164
+ if (pathname.startsWith("/api/")) {
17684
18165
  return errorResponse("Not found", 404);
17685
18166
  }
17686
- try {
17687
- return await matched.handler(req, url, matched.params);
17688
- } catch (e) {
17689
- console.error(`[mementos-serve] ${req.method} ${pathname}:`, e);
17690
- return errorResponse("Internal server error", 500);
18167
+ const dashDir = resolveDashboardDir();
18168
+ if (existsSync7(dashDir) && (req.method === "GET" || req.method === "HEAD")) {
18169
+ if (pathname !== "/") {
18170
+ const resolvedDash = resolve3(dashDir) + sep;
18171
+ const requestedPath = resolve3(join8(dashDir, pathname));
18172
+ if (requestedPath.startsWith(resolvedDash)) {
18173
+ const staticRes = serveStaticFile(requestedPath);
18174
+ if (staticRes)
18175
+ return staticRes;
18176
+ }
18177
+ }
18178
+ const indexRes = serveStaticFile(join8(dashDir, "index.html"));
18179
+ if (indexRes)
18180
+ return indexRes;
17691
18181
  }
18182
+ return errorResponse("Not found", 404);
18183
+ }
18184
+ try {
18185
+ return await matched.handler(req, url, matched.params);
18186
+ } catch (e) {
18187
+ console.error(`[mementos-serve] ${req.method} ${pathname}:`, e);
18188
+ return errorResponse("Internal server error", 500);
17692
18189
  }
17693
- });
17694
- console.log(`Mementos server listening on http://${hostname2}:${port}`);
17695
- } catch (e) {
17696
- const err = e;
17697
- if (err.code === "EADDRINUSE" && attempt < maxRetries) {
17698
- const nextPort = port + attempt + 1;
17699
- console.log(`Port ${port} in use, trying ${nextPort}`);
17700
- startServer(nextPort, attempt + 1);
17701
- } else {
17702
- throw e;
17703
18190
  }
17704
- }
18191
+ });
18192
+ console.log(`Mementos server listening on http://${hostname2}:${port}`);
17705
18193
  }
17706
18194
  async function main() {
17707
18195
  if (hasFlag("--help", "-h")) {