@hasna/todos 0.11.66 → 0.11.68

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.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/dashboard/dist/assets/{index-aJefI7kh.js → index-DVotjwab.js} +1 -1
  3. package/dashboard/dist/index.html +1 -1
  4. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/storage-commands.d.ts +5 -0
  6. package/dist/cli/commands/storage-commands.d.ts.map +1 -1
  7. package/dist/cli/index.js +8811 -2900
  8. package/dist/contracts.js +301 -71
  9. package/dist/db/agents.d.ts.map +1 -1
  10. package/dist/db/audit.d.ts.map +1 -1
  11. package/dist/db/migrations.d.ts.map +1 -1
  12. package/dist/db/plans.d.ts.map +1 -1
  13. package/dist/db/projects.d.ts.map +1 -1
  14. package/dist/db/schema.d.ts.map +1 -1
  15. package/dist/db/storage-tombstones.d.ts +26 -0
  16. package/dist/db/storage-tombstones.d.ts.map +1 -0
  17. package/dist/db/task-crud.d.ts.map +1 -1
  18. package/dist/db/task-lifecycle.d.ts.map +1 -1
  19. package/dist/db/task-lists.d.ts.map +1 -1
  20. package/dist/db/task-runs.d.ts.map +1 -1
  21. package/dist/db/templates.d.ts.map +1 -1
  22. package/dist/index.js +628 -160
  23. package/dist/lib/approval-gates.d.ts.map +1 -1
  24. package/dist/lib/event-emission-safety.d.ts +9 -0
  25. package/dist/lib/event-emission-safety.d.ts.map +1 -0
  26. package/dist/lib/event-hooks.d.ts +1 -0
  27. package/dist/lib/event-hooks.d.ts.map +1 -1
  28. package/dist/lib/feature-manifest.d.ts.map +1 -1
  29. package/dist/lib/project-panel.d.ts +8 -0
  30. package/dist/lib/project-panel.d.ts.map +1 -0
  31. package/dist/lib/review-queues.d.ts.map +1 -1
  32. package/dist/lib/shared-events.d.ts +1 -0
  33. package/dist/lib/shared-events.d.ts.map +1 -1
  34. package/dist/mcp/index.js +343 -104
  35. package/dist/registry.js +301 -71
  36. package/dist/release-provenance.json +3 -3
  37. package/dist/server/index.js +366 -127
  38. package/dist/storage/interfaces.d.ts +20 -0
  39. package/dist/storage/interfaces.d.ts.map +1 -1
  40. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  41. package/dist/storage/postgres-sync.d.ts +6 -1
  42. package/dist/storage/postgres-sync.d.ts.map +1 -1
  43. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  44. package/dist/storage.js +569 -94
  45. package/dist/types/index.d.ts +22 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/package.json +2 -1
package/dist/storage.js CHANGED
@@ -1312,6 +1312,22 @@ var init_migrations = __esm(() => {
1312
1312
  CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
1313
1313
  CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
1314
1314
  INSERT OR IGNORE INTO _migrations (id) VALUES (62);
1315
+ `,
1316
+ `
1317
+ CREATE TABLE IF NOT EXISTS storage_tombstones (
1318
+ id TEXT PRIMARY KEY,
1319
+ object_type TEXT NOT NULL,
1320
+ object_id TEXT NOT NULL,
1321
+ deleted_at TEXT NOT NULL,
1322
+ updated_at TEXT NOT NULL,
1323
+ source_machine_id TEXT,
1324
+ payload TEXT,
1325
+ version INTEGER,
1326
+ UNIQUE(object_type, object_id)
1327
+ );
1328
+ CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id);
1329
+ CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at);
1330
+ INSERT OR IGNORE INTO _migrations (id) VALUES (63);
1315
1331
  `
1316
1332
  ];
1317
1333
  });
@@ -1833,6 +1849,20 @@ function ensureSchema(db) {
1833
1849
  )`);
1834
1850
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
1835
1851
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
1852
+ ensureTable("storage_tombstones", `
1853
+ CREATE TABLE storage_tombstones (
1854
+ id TEXT PRIMARY KEY,
1855
+ object_type TEXT NOT NULL,
1856
+ object_id TEXT NOT NULL,
1857
+ deleted_at TEXT NOT NULL,
1858
+ updated_at TEXT NOT NULL,
1859
+ source_machine_id TEXT,
1860
+ payload TEXT,
1861
+ version INTEGER,
1862
+ UNIQUE(object_type, object_id)
1863
+ )`);
1864
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_object ON storage_tombstones(object_type, object_id)");
1865
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_storage_tombstones_updated ON storage_tombstones(updated_at)");
1836
1866
  ensureTable("machines", `
1837
1867
  CREATE TABLE machines (
1838
1868
  id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, hostname TEXT, platform TEXT,
@@ -3616,6 +3646,80 @@ init_config();
3616
3646
  init_types();
3617
3647
  init_database();
3618
3648
  init_machines();
3649
+
3650
+ // src/db/storage-tombstones.ts
3651
+ init_database();
3652
+ init_machines();
3653
+ function recordStorageTombstone(input, db) {
3654
+ const d = db ?? getDatabase();
3655
+ const deletedAt = input.deleted_at ?? now();
3656
+ const machineId = input.source_machine_id ?? currentStorageMachineId(d);
3657
+ d.run(`INSERT INTO storage_tombstones (
3658
+ id, object_type, object_id, deleted_at, updated_at, source_machine_id, payload, version
3659
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3660
+ ON CONFLICT(object_type, object_id) DO UPDATE SET
3661
+ deleted_at = excluded.deleted_at,
3662
+ updated_at = excluded.updated_at,
3663
+ source_machine_id = excluded.source_machine_id,
3664
+ payload = excluded.payload,
3665
+ version = excluded.version
3666
+ WHERE storage_tombstones.updated_at IS NULL OR storage_tombstones.updated_at <= excluded.updated_at`, [
3667
+ uuid(),
3668
+ input.object_type,
3669
+ input.object_id,
3670
+ deletedAt,
3671
+ deletedAt,
3672
+ machineId,
3673
+ input.payload ? JSON.stringify(input.payload) : null,
3674
+ input.version ?? null
3675
+ ]);
3676
+ return getStorageTombstone(input.object_type, input.object_id, d);
3677
+ }
3678
+ function getStorageTombstone(objectType, objectId, db) {
3679
+ const d = db ?? getDatabase();
3680
+ const row = d.query("SELECT * FROM storage_tombstones WHERE object_type = ? AND object_id = ?").get(objectType, objectId);
3681
+ return row ? rowToStorageTombstone(row) : null;
3682
+ }
3683
+ function listStorageTombstones(db) {
3684
+ const d = db ?? getDatabase();
3685
+ return d.query("SELECT * FROM storage_tombstones ORDER BY updated_at ASC, object_type ASC, object_id ASC").all().map(rowToStorageTombstone);
3686
+ }
3687
+ function shouldApplyStorageTombstone(tombstone, existingUpdatedAt) {
3688
+ const tombstoneClock = Date.parse(tombstone.updated_at || tombstone.deleted_at);
3689
+ if (!existingUpdatedAt)
3690
+ return true;
3691
+ const existingClock = Date.parse(existingUpdatedAt);
3692
+ if (Number.isNaN(tombstoneClock))
3693
+ return true;
3694
+ if (Number.isNaN(existingClock))
3695
+ return true;
3696
+ return tombstoneClock >= existingClock;
3697
+ }
3698
+ function rowToStorageTombstone(row) {
3699
+ return {
3700
+ ...row,
3701
+ payload: parsePayload(row.payload)
3702
+ };
3703
+ }
3704
+ function parsePayload(value) {
3705
+ if (!value)
3706
+ return null;
3707
+ try {
3708
+ const parsed = JSON.parse(value);
3709
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
3710
+ } catch {
3711
+ return null;
3712
+ }
3713
+ }
3714
+ function currentStorageMachineId(db) {
3715
+ try {
3716
+ return getMachineId(db);
3717
+ } catch {
3718
+ return null;
3719
+ }
3720
+ }
3721
+
3722
+ // src/db/projects.ts
3619
3723
  function slugify(name) {
3620
3724
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
3621
3725
  }
@@ -3645,8 +3749,9 @@ function createProject(input, db) {
3645
3749
  const timestamp = now();
3646
3750
  const taskListId = input.task_list_id ?? `todos-${slugify(input.name)}`;
3647
3751
  const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
3648
- d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at)
3649
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp]);
3752
+ const machineId = currentStorageMachineId(d);
3753
+ d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
3754
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp, machineId]);
3650
3755
  return getProject(id, d);
3651
3756
  }
3652
3757
  function getProject(id, db) {
@@ -3725,6 +3830,14 @@ function renameProject(id, input, db) {
3725
3830
  }
3726
3831
  function deleteProject(id, db) {
3727
3832
  const d = db || getDatabase();
3833
+ const project = getProject(id, d);
3834
+ if (!project)
3835
+ return false;
3836
+ recordStorageTombstone({
3837
+ object_type: "projects",
3838
+ object_id: id,
3839
+ payload: project
3840
+ }, d);
3728
3841
  const result = d.run("DELETE FROM projects WHERE id = ?", [id]);
3729
3842
  return result.changes > 0;
3730
3843
  }
@@ -3829,6 +3942,14 @@ function listMachineLocalPaths(projectId, db) {
3829
3942
  function removeMachineLocalPath(projectId, machineId, db) {
3830
3943
  const d = db || getDatabase();
3831
3944
  const mid = machineId ?? getMachineId(d);
3945
+ const existing = d.query("SELECT * FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(projectId, mid);
3946
+ if (!existing)
3947
+ return false;
3948
+ recordStorageTombstone({
3949
+ object_type: "project_machine_paths",
3950
+ object_id: existing.id,
3951
+ payload: existing
3952
+ }, d);
3832
3953
  const result = d.run("DELETE FROM project_machine_paths WHERE project_id = ? AND machine_id = ?", [projectId, mid]);
3833
3954
  return result.changes > 0;
3834
3955
  }
@@ -3877,20 +3998,69 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
3877
3998
  }
3878
3999
  }
3879
4000
 
4001
+ // src/lib/event-emission-safety.ts
4002
+ init_database();
4003
+ init_sync_utils();
4004
+ import { tmpdir } from "os";
4005
+ import { resolve as resolve3, sep } from "path";
4006
+ function envFlag(name) {
4007
+ const value = process.env[name];
4008
+ return value === "1" || value === "true" || value === "yes";
4009
+ }
4010
+ function isUnder(parent, child) {
4011
+ const normalizedParent = resolve3(parent);
4012
+ const normalizedChild = resolve3(child);
4013
+ return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${sep}`);
4014
+ }
4015
+ function databasePathFromDatabase(db) {
4016
+ const filename = db?.filename;
4017
+ return typeof filename === "string" && filename.trim() ? filename : undefined;
4018
+ }
4019
+ function isEphemeralTodosDatabase(dbPath) {
4020
+ const resolvedPath = dbPath ?? getDatabasePath();
4021
+ if (resolvedPath === ":memory:" || resolvedPath.startsWith("file::memory:"))
4022
+ return true;
4023
+ return isUnder(tmpdir(), resolvedPath);
4024
+ }
4025
+ function hasExplicitSharedEventsStore() {
4026
+ return Boolean(process.env["HASNA_EVENTS_DIR"] || process.env["HASNA_EVENTS_HOME"]);
4027
+ }
4028
+ function usesIsolatedTodosHome() {
4029
+ return isUnder(tmpdir(), getTodosGlobalDir());
4030
+ }
4031
+ function shouldEmitSharedTaskEvents(dbPath) {
4032
+ if (envFlag("TODOS_DISABLE_SHARED_EVENTS"))
4033
+ return false;
4034
+ if (envFlag("TODOS_ALLOW_EPHEMERAL_SHARED_EVENTS"))
4035
+ return true;
4036
+ if (!isEphemeralTodosDatabase(dbPath))
4037
+ return true;
4038
+ return hasExplicitSharedEventsStore();
4039
+ }
4040
+ function shouldDeliverLocalLifecycleHooks(dbPath) {
4041
+ if (envFlag("TODOS_DISABLE_LOCAL_EVENT_HOOKS"))
4042
+ return false;
4043
+ if (envFlag("TODOS_ALLOW_EPHEMERAL_LOCAL_EVENT_HOOKS"))
4044
+ return true;
4045
+ if (!isEphemeralTodosDatabase(dbPath))
4046
+ return true;
4047
+ return usesIsolatedTodosHome();
4048
+ }
4049
+
3880
4050
  // src/lib/event-hooks.ts
3881
4051
  init_redaction();
3882
4052
  import { createHash, randomUUID } from "crypto";
3883
4053
  import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
3884
- import { dirname as dirname3, resolve as resolve5 } from "path";
4054
+ import { dirname as dirname3, resolve as resolve6 } from "path";
3885
4055
  import { createConnection } from "net";
3886
4056
 
3887
4057
  // src/lib/runner-sandbox.ts
3888
4058
  init_config();
3889
- import { relative as relative2, resolve as resolve4 } from "path";
4059
+ import { relative as relative2, resolve as resolve5 } from "path";
3890
4060
 
3891
4061
  // src/lib/workspace-trust.ts
3892
4062
  init_config();
3893
- import { relative, resolve as resolve3 } from "path";
4063
+ import { relative, resolve as resolve4 } from "path";
3894
4064
  var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
3895
4065
  var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
3896
4066
  var PRESET_DEFAULTS = {
@@ -3936,7 +4106,7 @@ var PRESET_DEFAULTS = {
3936
4106
  }
3937
4107
  };
3938
4108
  function normalizePath(path) {
3939
- return resolve3(path);
4109
+ return resolve4(path);
3940
4110
  }
3941
4111
  function unique2(values) {
3942
4112
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -4073,7 +4243,7 @@ function checkWorkspacePermission(input = {}) {
4073
4243
  var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
4074
4244
  var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
4075
4245
  function normalizePath2(path) {
4076
- return resolve4(path);
4246
+ return resolve5(path);
4077
4247
  }
4078
4248
  function unique3(values) {
4079
4249
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
@@ -4419,7 +4589,7 @@ async function deliverHook(hook, envelope) {
4419
4589
  if (hook.target === "stdout") {
4420
4590
  output = line.trim();
4421
4591
  } else if (hook.target === "file") {
4422
- const filePath = resolve5(hook.file_path);
4592
+ const filePath = resolve6(hook.file_path);
4423
4593
  mkdirSync3(dirname3(filePath), { recursive: true });
4424
4594
  appendFileSync(filePath, line);
4425
4595
  } else if (hook.target === "socket") {
@@ -4494,6 +4664,8 @@ async function emitLocalEventHooks(input) {
4494
4664
  return Promise.all(hooks.map((hook) => deliverHook(hook, envelope)));
4495
4665
  }
4496
4666
  function emitLocalEventHooksQuiet(input) {
4667
+ if (!shouldDeliverLocalLifecycleHooks(input.databasePath))
4668
+ return;
4497
4669
  emitLocalEventHooks(input).catch(() => {});
4498
4670
  }
4499
4671
  async function testLocalEventHook(name, input) {
@@ -4810,7 +4982,7 @@ async function dispatchCommand(event, channel) {
4810
4982
  HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
4811
4983
  HASNA_EVENT_JSON: eventJson
4812
4984
  };
4813
- return new Promise((resolve6) => {
4985
+ return new Promise((resolve7) => {
4814
4986
  const child = spawn(channel.command.command, channel.command.args ?? [], {
4815
4987
  cwd: channel.command.cwd,
4816
4988
  env,
@@ -4828,7 +5000,7 @@ async function dispatchCommand(event, channel) {
4828
5000
  });
4829
5001
  child.on("error", (error) => {
4830
5002
  clearTimeout(timeout);
4831
- resolve6({
5003
+ resolve7({
4832
5004
  attempt: 1,
4833
5005
  status: "failed",
4834
5006
  startedAt,
@@ -4841,7 +5013,7 @@ async function dispatchCommand(event, channel) {
4841
5013
  child.on("close", (code, signal) => {
4842
5014
  clearTimeout(timeout);
4843
5015
  const success = code === 0;
4844
- resolve6({
5016
+ resolve7({
4845
5017
  attempt: 1,
4846
5018
  status: success ? "success" : "failed",
4847
5019
  startedAt,
@@ -5109,14 +5281,15 @@ function createTaskList(input, db) {
5109
5281
  const id = uuid();
5110
5282
  const timestamp = now();
5111
5283
  const slug = input.slug || slugify(input.name);
5284
+ const machineId = currentStorageMachineId(d);
5112
5285
  if (!input.project_id) {
5113
5286
  const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
5114
5287
  if (existing) {
5115
5288
  throw new Error(`Standalone task list with slug "${slug}" already exists`);
5116
5289
  }
5117
5290
  }
5118
- d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
5119
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
5291
+ d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at, machine_id)
5292
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp, machineId]);
5120
5293
  return getTaskList(id, d);
5121
5294
  }
5122
5295
  function getTaskList(id, db) {
@@ -5166,6 +5339,14 @@ function updateTaskList(id, input, db) {
5166
5339
  }
5167
5340
  function deleteTaskList(id, db) {
5168
5341
  const d = db || getDatabase();
5342
+ const list = getTaskList(id, d);
5343
+ if (!list)
5344
+ return false;
5345
+ recordStorageTombstone({
5346
+ object_type: "task_lists",
5347
+ object_id: id,
5348
+ payload: list
5349
+ }, d);
5169
5350
  return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
5170
5351
  }
5171
5352
  function ensureTaskList(name, slug, projectId, db) {
@@ -5382,6 +5563,8 @@ function readMachineLocalPath(project) {
5382
5563
  }
5383
5564
  }
5384
5565
  async function emitSharedTaskEvent(input) {
5566
+ if (!shouldEmitSharedTaskEvents(input.databasePath))
5567
+ return;
5385
5568
  const data = taskEventData(input.task, input.data);
5386
5569
  await new EventsClient().emit({
5387
5570
  source: SOURCE,
@@ -5406,8 +5589,9 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
5406
5589
  const d = db || getDatabase();
5407
5590
  const id = uuid();
5408
5591
  const timestamp = now();
5409
- d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
5410
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp]);
5592
+ const machineId = currentStorageMachineId(d);
5593
+ d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
5594
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, oldValue ?? null, newValue ?? null, agentId || null, timestamp, machineId]);
5411
5595
  try {
5412
5596
  const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
5413
5597
  logActivity2({
@@ -5420,7 +5604,7 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
5420
5604
  actor_id: agentId ?? undefined
5421
5605
  }, d);
5422
5606
  } catch {}
5423
- return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp };
5607
+ return { id, task_id: taskId, action, field: field || null, old_value: oldValue ?? null, new_value: newValue ?? null, agent_id: agentId || null, created_at: timestamp, machine_id: machineId };
5424
5608
  }
5425
5609
  function getTaskHistory(taskId, db) {
5426
5610
  const d = db || getDatabase();
@@ -5769,13 +5953,14 @@ function createTask(input, db) {
5769
5953
  const d = db || getDatabase();
5770
5954
  const timestamp = now();
5771
5955
  const tags = input.tags || [];
5956
+ const machineId = currentStorageMachineId(d);
5772
5957
  const assignedBy = input.assigned_by || input.agent_id;
5773
5958
  const assignedFromProject = input.assigned_from_project || null;
5774
5959
  let id = uuid();
5775
5960
  for (let attempt = 0;attempt < 3; attempt++) {
5776
5961
  try {
5777
- d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
5778
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5962
+ d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type, machine_id)
5963
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5779
5964
  id,
5780
5965
  null,
5781
5966
  input.project_id || null,
@@ -5812,7 +5997,8 @@ function createTask(input, db) {
5812
5997
  input.spawned_from_session || null,
5813
5998
  assignedBy || null,
5814
5999
  assignedFromProject || null,
5815
- input.task_type || null
6000
+ input.task_type || null,
6001
+ machineId
5816
6002
  ]);
5817
6003
  break;
5818
6004
  } catch (e) {
@@ -5828,9 +6014,10 @@ function createTask(input, db) {
5828
6014
  }
5829
6015
  const task = getTask(id, d);
5830
6016
  const payload = taskEventData(task);
6017
+ const databasePath = databasePathFromDatabase(d);
5831
6018
  dispatchWebhook2("task.created", payload, d).catch(() => {});
5832
- emitLocalEventHooksQuiet({ type: "task.created", payload });
5833
- emitSharedTaskEventQuiet({ type: "task.created", task });
6019
+ emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
6020
+ emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
5834
6021
  return task;
5835
6022
  }
5836
6023
  function getTask(id, db) {
@@ -6248,29 +6435,39 @@ function updateTask(id, input, db) {
6248
6435
  approved_by: input.approved_by ?? task.approved_by,
6249
6436
  approved_at: input.approved_by ? timestamp : task.approved_at
6250
6437
  };
6438
+ const databasePath = databasePathFromDatabase(d);
6251
6439
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
6252
6440
  const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
6253
6441
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
6254
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
6255
- emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to } });
6442
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
6443
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
6256
6444
  }
6257
6445
  if (input.status !== undefined && input.status !== task.status) {
6258
6446
  const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
6259
6447
  dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
6260
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
6261
- emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status } });
6448
+ emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
6449
+ emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
6262
6450
  }
6263
6451
  if (input.approved_by !== undefined) {
6264
- emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
6452
+ emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
6265
6453
  }
6266
6454
  const updatePayload = taskEventData(updatedTask);
6267
6455
  dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
6268
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
6269
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
6456
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
6457
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
6270
6458
  return updatedTask;
6271
6459
  }
6272
6460
  function deleteTask(id, db) {
6273
6461
  const d = db || getDatabase();
6462
+ const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
6463
+ if (!row)
6464
+ return false;
6465
+ recordStorageTombstone({
6466
+ object_type: "tasks",
6467
+ object_id: id,
6468
+ payload: rowToTask(row),
6469
+ version: row.version
6470
+ }, d);
6274
6471
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
6275
6472
  return result.changes > 0;
6276
6473
  }
@@ -6402,8 +6599,9 @@ function resolveTemplateId(id, d) {
6402
6599
  function createTemplate(input, db) {
6403
6600
  const d = db || getDatabase();
6404
6601
  const id = uuid();
6405
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
6406
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6602
+ const machineId = currentStorageMachineId(d);
6603
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
6604
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6407
6605
  id,
6408
6606
  input.name,
6409
6607
  input.title_pattern,
@@ -6414,7 +6612,8 @@ function createTemplate(input, db) {
6414
6612
  input.project_id || null,
6415
6613
  input.plan_id || null,
6416
6614
  JSON.stringify(input.metadata || {}),
6417
- now()
6615
+ now(),
6616
+ machineId
6418
6617
  ]);
6419
6618
  if (input.tasks && input.tasks.length > 0) {
6420
6619
  addTemplateTasks(id, input.tasks, d);
@@ -6438,6 +6637,15 @@ function deleteTemplate(id, db) {
6438
6637
  const resolved = resolveTemplateId(id, d);
6439
6638
  if (!resolved)
6440
6639
  return false;
6640
+ const template = getTemplate(resolved, d);
6641
+ if (!template)
6642
+ return false;
6643
+ recordStorageTombstone({
6644
+ object_type: "templates",
6645
+ object_id: resolved,
6646
+ payload: template,
6647
+ version: template.version
6648
+ }, d);
6441
6649
  return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
6442
6650
  }
6443
6651
  function updateTemplate(id, updates, db) {
@@ -6967,6 +7175,7 @@ function getBlockingDeps(id, db) {
6967
7175
  }
6968
7176
  function startTask(id, agentId, db) {
6969
7177
  const d = db || getDatabase();
7178
+ const databasePath = databasePathFromDatabase(d);
6970
7179
  const task = getTask(id, d);
6971
7180
  if (!task)
6972
7181
  throw new TaskNotFoundError(id);
@@ -6981,7 +7190,8 @@ function startTask(id, agentId, db) {
6981
7190
  agent_id: agentId,
6982
7191
  title: task.title,
6983
7192
  blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
6984
- }
7193
+ },
7194
+ databasePath
6985
7195
  });
6986
7196
  throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
6987
7197
  }
@@ -7003,12 +7213,13 @@ function startTask(id, agentId, db) {
7003
7213
  const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
7004
7214
  const payload = taskEventData(startedTask, { agent_id: agentId });
7005
7215
  dispatchWebhook2("task.started", payload, d).catch(() => {});
7006
- emitLocalEventHooksQuiet({ type: "task.started", payload });
7007
- emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId } });
7216
+ emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
7217
+ emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
7008
7218
  return startedTask;
7009
7219
  }
7010
7220
  function completeTask(id, agentId, db, options) {
7011
7221
  const d = db || getDatabase();
7222
+ const databasePath = databasePathFromDatabase(d);
7012
7223
  const task = getTask(id, d);
7013
7224
  if (!task)
7014
7225
  throw new TaskNotFoundError(id);
@@ -7054,8 +7265,8 @@ function completeTask(id, agentId, db, options) {
7054
7265
  };
7055
7266
  const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
7056
7267
  dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
7057
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
7058
- emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp } });
7268
+ emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
7269
+ emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
7059
7270
  let spawnedTask = null;
7060
7271
  if (task.recurrence_rule && !options?.skip_recurrence) {
7061
7272
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -7099,9 +7310,9 @@ function completeTask(id, agentId, db, options) {
7099
7310
  const depTask = getTask(dep.id, d);
7100
7311
  const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
7101
7312
  dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
7102
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
7313
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
7103
7314
  if (depTask)
7104
- emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
7315
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
7105
7316
  }
7106
7317
  }
7107
7318
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -7271,6 +7482,7 @@ function getTasksChangedSince(since, filters, db) {
7271
7482
  }
7272
7483
  function failTask(id, agentId, reason, options, db) {
7273
7484
  const d = db || getDatabase();
7485
+ const databasePath = databasePathFromDatabase(d);
7274
7486
  const task = getTask(id, d);
7275
7487
  if (!task)
7276
7488
  throw new TaskNotFoundError(id);
@@ -7299,8 +7511,8 @@ function failTask(id, agentId, reason, options, db) {
7299
7511
  logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
7300
7512
  const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
7301
7513
  dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
7302
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
7303
- emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning" });
7514
+ emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
7515
+ emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
7304
7516
  let retryTask;
7305
7517
  if (options?.retry) {
7306
7518
  const retryCount = (task.retry_count || 0) + 1;
@@ -7360,6 +7572,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
7360
7572
  }
7361
7573
  function stealTask(agentId, opts, db) {
7362
7574
  const d = db || getDatabase();
7575
+ const databasePath = databasePathFromDatabase(d);
7363
7576
  const staleMinutes = opts?.stale_minutes ?? 30;
7364
7577
  const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
7365
7578
  if (staleTasks.length === 0)
@@ -7378,8 +7591,8 @@ function stealTask(agentId, opts, db) {
7378
7591
  const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
7379
7592
  const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
7380
7593
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
7381
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
7382
- emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to } });
7594
+ emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
7595
+ emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
7383
7596
  return stolenTask;
7384
7597
  }
7385
7598
  function claimOrSteal(agentId, filters, db) {
@@ -7970,8 +8183,9 @@ function createPlan(input, db) {
7970
8183
  const d = db || getDatabase();
7971
8184
  const id = uuid();
7972
8185
  const timestamp = now();
7973
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
7974
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
8186
+ const machineId = currentStorageMachineId(d);
8187
+ d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at, machine_id)
8188
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
7975
8189
  id,
7976
8190
  input.project_id || null,
7977
8191
  input.task_list_id || null,
@@ -7980,7 +8194,8 @@ function createPlan(input, db) {
7980
8194
  input.description || null,
7981
8195
  input.status || "active",
7982
8196
  timestamp,
7983
- timestamp
8197
+ timestamp,
8198
+ machineId
7984
8199
  ]);
7985
8200
  return getPlan(id, d);
7986
8201
  }
@@ -8028,12 +8243,21 @@ function updatePlan(id, input, db) {
8028
8243
  const updated = getPlan(id, d);
8029
8244
  emitLocalEventHooksQuiet({
8030
8245
  type: "plan.updated",
8031
- payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
8246
+ payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id },
8247
+ databasePath: databasePathFromDatabase(d)
8032
8248
  });
8033
8249
  return updated;
8034
8250
  }
8035
8251
  function deletePlan(id, db) {
8036
8252
  const d = db || getDatabase();
8253
+ const plan = getPlan(id, d);
8254
+ if (!plan)
8255
+ return false;
8256
+ recordStorageTombstone({
8257
+ object_type: "plans",
8258
+ object_id: id,
8259
+ payload: plan
8260
+ }, d);
8037
8261
  const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
8038
8262
  return result.changes > 0;
8039
8263
  }
@@ -8440,20 +8664,20 @@ init_database();
8440
8664
  init_redaction();
8441
8665
  import { createHash as createHash2 } from "crypto";
8442
8666
  import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
8443
- import { basename, dirname as dirname4, join as join5, resolve as resolve6 } from "path";
8444
- import { tmpdir } from "os";
8667
+ import { basename, dirname as dirname4, join as join5, resolve as resolve7 } from "path";
8668
+ import { tmpdir as tmpdir2 } from "os";
8445
8669
  function isInMemoryDb2(path) {
8446
8670
  return path === ":memory:" || path.startsWith("file::memory:");
8447
8671
  }
8448
8672
  function artifactStoreRoot() {
8449
8673
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
8450
- return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
8674
+ return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
8451
8675
  if (process.env["TODOS_ARTIFACTS_DIR"])
8452
- return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
8676
+ return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
8453
8677
  const dbPath = getDatabasePath();
8454
8678
  if (isInMemoryDb2(dbPath))
8455
- return join5(tmpdir(), "hasna-todos-artifacts");
8456
- return join5(dirname4(resolve6(dbPath)), "artifacts");
8679
+ return join5(tmpdir2(), "hasna-todos-artifacts");
8680
+ return join5(dirname4(resolve7(dbPath)), "artifacts");
8457
8681
  }
8458
8682
  function artifactStorePath(relativePath) {
8459
8683
  const normalized = relativePath.replace(/\\/g, "/");
@@ -8500,7 +8724,7 @@ function mediaTypeFor(path, textLike) {
8500
8724
  return "application/octet-stream";
8501
8725
  }
8502
8726
  function storeArtifactContent(input) {
8503
- const sourcePath = resolve6(input.path);
8727
+ const sourcePath = resolve7(input.path);
8504
8728
  if (!existsSync7(sourcePath))
8505
8729
  return null;
8506
8730
  const sourceStat = statSync2(sourcePath);
@@ -8656,19 +8880,19 @@ function importStoredArtifactContent(content) {
8656
8880
  var DEFAULT_DELETED_RETENTION_DAYS = 30;
8657
8881
  function getArtifactStoreRoot(dbPath) {
8658
8882
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
8659
- return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
8883
+ return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
8660
8884
  if (process.env["TODOS_ARTIFACTS_DIR"])
8661
- return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
8885
+ return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
8662
8886
  const path = dbPath ?? getDatabasePath();
8663
8887
  if (isInMemoryDb2(path))
8664
- return join5(tmpdir(), "hasna-todos-artifacts");
8665
- return join5(dirname4(resolve6(path)), "artifacts");
8888
+ return join5(tmpdir2(), "hasna-todos-artifacts");
8889
+ return join5(dirname4(resolve7(path)), "artifacts");
8666
8890
  }
8667
8891
  function computeContentHash(path) {
8668
- return sha256(readFileSync3(resolve6(path)));
8892
+ return sha256(readFileSync3(resolve7(path)));
8669
8893
  }
8670
8894
  function storeArtifactFile(input) {
8671
- const sourcePath = resolve6(input.sourcePath);
8895
+ const sourcePath = resolve7(input.sourcePath);
8672
8896
  if (!existsSync7(sourcePath)) {
8673
8897
  throw new Error(`Source file not found: ${input.sourcePath}`);
8674
8898
  }
@@ -8717,7 +8941,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
8717
8941
  };
8718
8942
  }
8719
8943
  function writeArtifactExportManifest(manifest, outputPath) {
8720
- const destination = resolve6(outputPath);
8944
+ const destination = resolve7(outputPath);
8721
8945
  mkdirSync4(dirname4(destination), { recursive: true });
8722
8946
  writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
8723
8947
  `);
@@ -9104,7 +9328,11 @@ function startTaskRun(input, db) {
9104
9328
  }, d);
9105
9329
  }
9106
9330
  const run = getTaskRun(id, d);
9107
- emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
9331
+ emitLocalEventHooksQuiet({
9332
+ type: "run.started",
9333
+ payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title },
9334
+ databasePath: databasePathFromDatabase(d)
9335
+ });
9108
9336
  return run;
9109
9337
  }
9110
9338
  function beginTaskRunTransaction(input, db) {
@@ -9373,7 +9601,8 @@ function finishTaskRun(input, db) {
9373
9601
  const updated = getTaskRun(run.id, d);
9374
9602
  emitLocalEventHooksQuiet({
9375
9603
  type: `run.${input.status}`,
9376
- payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp }
9604
+ payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp },
9605
+ databasePath: databasePathFromDatabase(d)
9377
9606
  });
9378
9607
  return updated;
9379
9608
  }
@@ -10205,6 +10434,7 @@ function rowToAgent(row) {
10205
10434
  }
10206
10435
  function registerAgent(input, db) {
10207
10436
  const d = db || getDatabase();
10437
+ const machineId = currentStorageMachineId(d);
10208
10438
  const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
10209
10439
  const normalizedName = validateAgentName(input.name, existingNames);
10210
10440
  const existing = getAgentByName(normalizedName, d);
@@ -10245,14 +10475,18 @@ function registerAgent(input, db) {
10245
10475
  updates.push("active_project_id = ?");
10246
10476
  params.push(input.project_id);
10247
10477
  }
10478
+ if (!existing.machine_id && machineId) {
10479
+ updates.push("machine_id = ?");
10480
+ params.push(machineId);
10481
+ }
10248
10482
  params.push(existing.id);
10249
10483
  d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
10250
10484
  return getAgent(existing.id, d);
10251
10485
  }
10252
10486
  const id = shortUuid();
10253
10487
  const timestamp = now();
10254
- d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, active_project_id)
10255
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10488
+ d.run(`INSERT INTO agents (id, name, description, role, title, level, permissions, capabilities, reports_to, org_id, metadata, created_at, last_seen_at, session_id, working_dir, active_project_id, machine_id)
10489
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10256
10490
  id,
10257
10491
  normalizedName,
10258
10492
  input.description || null,
@@ -10268,7 +10502,8 @@ function registerAgent(input, db) {
10268
10502
  timestamp,
10269
10503
  input.session_id || null,
10270
10504
  input.working_dir || null,
10271
- input.project_id && input.session_id ? input.project_id : null
10505
+ input.project_id && input.session_id ? input.project_id : null,
10506
+ machineId
10272
10507
  ]);
10273
10508
  return getAgent(id, d);
10274
10509
  }
@@ -10468,6 +10703,14 @@ var PROJECT_COLUMNS = [
10468
10703
  "machine_id",
10469
10704
  "synced_at"
10470
10705
  ];
10706
+ var PROJECT_MACHINE_PATH_COLUMNS = [
10707
+ "id",
10708
+ "project_id",
10709
+ "machine_id",
10710
+ "path",
10711
+ "created_at",
10712
+ "updated_at"
10713
+ ];
10471
10714
  var TASK_LIST_COLUMNS = [
10472
10715
  "id",
10473
10716
  "project_id",
@@ -10607,11 +10850,13 @@ function exportSqliteTodosStorageSnapshot(db) {
10607
10850
  source: "sqlite",
10608
10851
  tasks: listTasks({ include_archived: true }, d),
10609
10852
  projects: listProjects(d),
10853
+ projectMachinePaths: listProjectMachinePaths(d),
10610
10854
  plans: listPlans(undefined, d),
10611
10855
  agents: listAgents({ include_archived: true }, d),
10612
10856
  taskLists: listTaskLists(undefined, d),
10613
10857
  templates: listTemplates(d),
10614
- auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d)
10858
+ auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
10859
+ tombstones: listStorageTombstones(d)
10615
10860
  };
10616
10861
  }
10617
10862
  function importSqliteTodosStorageSnapshot(snapshot, db) {
@@ -10619,13 +10864,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
10619
10864
  const result = {
10620
10865
  inserted: 0,
10621
10866
  updated: 0,
10867
+ deleted: 0,
10622
10868
  skipped: 0,
10623
10869
  errors: []
10624
10870
  };
10625
- const applyRows = (table, columns, rows, updateClockColumn, afterUpsert) => {
10871
+ const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
10626
10872
  for (const row of rows) {
10627
10873
  try {
10628
10874
  const record = asRecord(row);
10875
+ const tombstone = typeof record["id"] === "string" ? getStorageTombstone(objectType, record["id"], d) : null;
10876
+ if (tombstone && shouldApplyStorageTombstone(tombstone, rowClock(record, updateClockColumn))) {
10877
+ result.skipped += 1;
10878
+ continue;
10879
+ }
10629
10880
  const state = upsertById(d, table, columns, record, updateClockColumn);
10630
10881
  if (state === "inserted")
10631
10882
  result.inserted += 1;
@@ -10639,17 +10890,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
10639
10890
  }
10640
10891
  }
10641
10892
  };
10642
- applyRows("projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
10643
- applyRows("agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
10644
- applyRows("task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
10645
- applyRows("plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
10646
- applyRows("task_templates", TEMPLATE_COLUMNS, snapshot.templates);
10647
- applyRows("tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
10893
+ applyRows("projects", "projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
10894
+ applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
10895
+ applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
10896
+ applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
10897
+ applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
10898
+ applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
10899
+ applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
10648
10900
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
10649
10901
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
10650
10902
  }
10651
10903
  });
10652
- applyRows("task_history", AUDIT_COLUMNS, snapshot.auditHistory);
10904
+ applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
10905
+ applyTombstones(d, snapshot.tombstones ?? [], result);
10653
10906
  return result;
10654
10907
  }
10655
10908
  function upsertById(db, table, columns, row, updateClockColumn) {
@@ -10704,6 +10957,85 @@ function sortedTasks(tasks) {
10704
10957
  visit(task);
10705
10958
  return result;
10706
10959
  }
10960
+ function applyTombstones(db, tombstones, result) {
10961
+ for (const tombstone of tombstones) {
10962
+ try {
10963
+ recordStorageTombstone({
10964
+ object_type: tombstone.object_type,
10965
+ object_id: tombstone.object_id,
10966
+ deleted_at: tombstone.deleted_at,
10967
+ source_machine_id: tombstone.source_machine_id ?? null,
10968
+ payload: tombstone.payload ?? null,
10969
+ version: tombstone.version ?? null
10970
+ }, db);
10971
+ const table = tableForTombstone(tombstone.object_type);
10972
+ const existing = existingClock(db, table, tombstone.object_id);
10973
+ if (!shouldApplyStorageTombstone(tombstone, existing)) {
10974
+ result.skipped += 1;
10975
+ continue;
10976
+ }
10977
+ const deletedTags = table === "tasks" ? db.run("DELETE FROM task_tags WHERE task_id = ?", [tombstone.object_id]).changes : 0;
10978
+ const deleted = db.run(`DELETE FROM ${table} WHERE id = ?`, [tombstone.object_id]).changes;
10979
+ if (deleted > 0 || deletedTags > 0)
10980
+ result.deleted = (result.deleted ?? 0) + 1;
10981
+ else
10982
+ result.skipped += 1;
10983
+ } catch (error) {
10984
+ result.errors.push(error instanceof Error ? error.message : String(error));
10985
+ }
10986
+ }
10987
+ }
10988
+ function tableForTombstone(objectType) {
10989
+ if (objectType === "tasks")
10990
+ return "tasks";
10991
+ if (objectType === "projects")
10992
+ return "projects";
10993
+ if (objectType === "project_machine_paths")
10994
+ return "project_machine_paths";
10995
+ if (objectType === "plans")
10996
+ return "plans";
10997
+ if (objectType === "agents")
10998
+ return "agents";
10999
+ if (objectType === "task_lists")
11000
+ return "task_lists";
11001
+ if (objectType === "templates")
11002
+ return "task_templates";
11003
+ return "task_history";
11004
+ }
11005
+ function listRows(db, table, columns) {
11006
+ return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
11007
+ }
11008
+ function listProjectMachinePaths(db) {
11009
+ return listRows(db, "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS).map((row) => ({
11010
+ id: String(row.id),
11011
+ project_id: String(row.project_id),
11012
+ machine_id: String(row.machine_id),
11013
+ path: String(row.path),
11014
+ created_at: String(row.created_at),
11015
+ updated_at: String(row.updated_at)
11016
+ }));
11017
+ }
11018
+ function existingClock(db, table, id) {
11019
+ const clockColumns = clockColumnsForTable(table);
11020
+ const row = db.query(`SELECT ${clockColumns.join(", ")} FROM ${table} WHERE id = ?`).get(id);
11021
+ return row?.updated_at ?? row?.last_seen_at ?? row?.created_at ?? null;
11022
+ }
11023
+ function rowClock(row, updateClockColumn) {
11024
+ const value = updateClockColumn ? row[updateClockColumn] : null;
11025
+ return stringClock(value) ?? stringClock(row["updated_at"]) ?? stringClock(row["last_seen_at"]) ?? stringClock(row["created_at"]);
11026
+ }
11027
+ function stringClock(value) {
11028
+ return typeof value === "string" && value ? value : null;
11029
+ }
11030
+ function clockColumnsForTable(table) {
11031
+ if (table === "agents")
11032
+ return ["last_seen_at", "created_at"];
11033
+ if (table === "task_templates")
11034
+ return ["created_at"];
11035
+ if (table === "task_history")
11036
+ return ["created_at"];
11037
+ return ["updated_at", "created_at"];
11038
+ }
10707
11039
 
10708
11040
  // src/storage/local-sqlite.ts
10709
11041
  function createLocalSqliteTodosStorageAdapter(options = {}) {
@@ -10870,7 +11202,7 @@ class PostgresTodosSyncStore {
10870
11202
  }
10871
11203
  async pullSnapshot(options = {}) {
10872
11204
  const params = [this.service];
10873
- const filters = ["service = $1", "deleted_at IS NULL"];
11205
+ const filters = ["service = $1"];
10874
11206
  if (options.since) {
10875
11207
  params.push(options.since);
10876
11208
  filters.push(`updated_at > $${params.length}::timestamptz`);
@@ -10879,7 +11211,7 @@ class PostgresTodosSyncStore {
10879
11211
  params.push(options.objectTypes);
10880
11212
  filters.push(`object_type = ANY($${params.length}::text[])`);
10881
11213
  }
10882
- const response = await this.client.query(`SELECT object_type, payload FROM ${this.tableName}
11214
+ const response = await this.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version FROM ${this.tableName}
10883
11215
  WHERE ${filters.join(" AND ")}
10884
11216
  ORDER BY updated_at ASC, object_type ASC, object_id ASC`, params);
10885
11217
  return rowsToSnapshot(response.rows);
@@ -10903,11 +11235,20 @@ function snapshotEntries(snapshot) {
10903
11235
  return [
10904
11236
  ...snapshot.tasks.map((payload) => entry("tasks", payload, snapshot.exportedAt)),
10905
11237
  ...snapshot.projects.map((payload) => entry("projects", payload, snapshot.exportedAt)),
11238
+ ...(snapshot.projectMachinePaths ?? []).map((payload) => entry("project_machine_paths", payload, snapshot.exportedAt)),
10906
11239
  ...snapshot.plans.map((payload) => entry("plans", payload, snapshot.exportedAt)),
10907
11240
  ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
10908
11241
  ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
10909
11242
  ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
10910
- ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt))
11243
+ ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
11244
+ ...(snapshot.tombstones ?? []).map((tombstone) => ({
11245
+ type: tombstone.object_type,
11246
+ id: tombstone.object_id,
11247
+ payload: tombstone.payload ?? { id: tombstone.object_id, deleted_at: tombstone.deleted_at },
11248
+ updatedAt: tombstone.updated_at || tombstone.deleted_at,
11249
+ deletedAt: tombstone.deleted_at,
11250
+ version: tombstone.version ?? null
11251
+ }))
10911
11252
  ];
10912
11253
  }
10913
11254
  function entry(type, payload, fallbackUpdatedAt) {
@@ -10929,19 +11270,38 @@ function rowsToSnapshot(rows) {
10929
11270
  source: "postgres",
10930
11271
  tasks: [],
10931
11272
  projects: [],
11273
+ projectMachinePaths: [],
10932
11274
  plans: [],
10933
11275
  agents: [],
10934
11276
  taskLists: [],
10935
11277
  templates: [],
10936
- auditHistory: []
11278
+ auditHistory: [],
11279
+ tombstones: []
10937
11280
  };
10938
11281
  for (const row of rows) {
10939
11282
  const payload = payloadRecord(row.payload);
11283
+ const deletedAt = stringValue(row.deleted_at);
11284
+ if (deletedAt) {
11285
+ snapshot.tombstones ??= [];
11286
+ snapshot.tombstones.push({
11287
+ object_type: row.object_type,
11288
+ object_id: stringValue(row.object_id) ?? stringValue(payload["id"]) ?? "",
11289
+ deleted_at: deletedAt,
11290
+ updated_at: stringValue(row.updated_at) ?? deletedAt,
11291
+ source_machine_id: stringValue(row.source_machine_id),
11292
+ payload,
11293
+ version: numberValue(row.version)
11294
+ });
11295
+ continue;
11296
+ }
10940
11297
  if (row.object_type === "tasks")
10941
11298
  snapshot.tasks.push(payload);
10942
11299
  else if (row.object_type === "projects")
10943
11300
  snapshot.projects.push(payload);
10944
- else if (row.object_type === "plans")
11301
+ else if (row.object_type === "project_machine_paths") {
11302
+ snapshot.projectMachinePaths ??= [];
11303
+ snapshot.projectMachinePaths.push(payload);
11304
+ } else if (row.object_type === "plans")
10945
11305
  snapshot.plans.push(payload);
10946
11306
  else if (row.object_type === "agents")
10947
11307
  snapshot.agents.push(payload);
@@ -10962,6 +11322,8 @@ function payloadRecord(value) {
10962
11322
  throw new Error("Postgres sync payload must be a JSON object");
10963
11323
  }
10964
11324
  function stringValue(value) {
11325
+ if (value instanceof Date)
11326
+ return value.toISOString();
10965
11327
  return typeof value === "string" && value ? value : null;
10966
11328
  }
10967
11329
  function numberValue(value) {
@@ -11116,6 +11478,9 @@ class PostgresJsonRecordStore {
11116
11478
  this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
11117
11479
  this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
11118
11480
  }
11481
+ machineId(context) {
11482
+ return context?.requestId ?? this.sourceMachineId ?? null;
11483
+ }
11119
11484
  async ensureSchema() {
11120
11485
  this.schemaReady ??= (async () => {
11121
11486
  for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
@@ -11147,6 +11512,25 @@ class PostgresJsonRecordStore {
11147
11512
  updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
11148
11513
  }));
11149
11514
  }
11515
+ async listTombstones() {
11516
+ await this.ensureSchema();
11517
+ const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
11518
+ FROM ${this.tableName}
11519
+ WHERE service = $1 AND deleted_at IS NOT NULL
11520
+ ORDER BY updated_at ASC, object_type ASC, object_id ASC`, [this.service]);
11521
+ return result.rows.map((row) => {
11522
+ const deletedAt = stringValue2(row.deleted_at) ?? stringValue2(row.updated_at) ?? new Date().toISOString();
11523
+ return {
11524
+ object_type: row.object_type,
11525
+ object_id: row.object_id,
11526
+ deleted_at: deletedAt,
11527
+ updated_at: stringValue2(row.updated_at) ?? deletedAt,
11528
+ source_machine_id: stringValue2(row.source_machine_id),
11529
+ payload: payloadRecord2(row.payload),
11530
+ version: numberValue2(row.version)
11531
+ };
11532
+ });
11533
+ }
11150
11534
  async upsert(type, value, context = {}) {
11151
11535
  await this.ensureSchema();
11152
11536
  const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
@@ -11159,7 +11543,8 @@ class PostgresJsonRecordStore {
11159
11543
  updated_at = EXCLUDED.updated_at,
11160
11544
  deleted_at = NULL,
11161
11545
  source_machine_id = EXCLUDED.source_machine_id,
11162
- version = EXCLUDED.version`, [
11546
+ version = EXCLUDED.version
11547
+ WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
11163
11548
  this.service,
11164
11549
  type,
11165
11550
  value.id,
@@ -11176,13 +11561,58 @@ class PostgresJsonRecordStore {
11176
11561
  if (!existing)
11177
11562
  return false;
11178
11563
  const timestamp = new Date().toISOString();
11179
- await this.options.client.query(`UPDATE ${this.tableName}
11180
- SET deleted_at = $4::timestamptz,
11181
- updated_at = $4::timestamptz,
11182
- source_machine_id = $5
11183
- WHERE service = $1 AND object_type = $2 AND object_id = $3`, [this.service, type, id, timestamp, context.requestId ?? this.sourceMachineId ?? null]);
11564
+ return this.tombstone({
11565
+ object_type: type,
11566
+ object_id: id,
11567
+ deleted_at: timestamp,
11568
+ updated_at: timestamp,
11569
+ payload: existing,
11570
+ version: numberValue2(existing["version"])
11571
+ }, context);
11572
+ }
11573
+ async tombstone(tombstone, context = {}) {
11574
+ await this.ensureSchema();
11575
+ const deletedAt = stringValue2(tombstone.deleted_at) ?? new Date().toISOString();
11576
+ const updatedAt = stringValue2(tombstone.updated_at) ?? deletedAt;
11577
+ const existing = await this.clock(tombstone.object_type, tombstone.object_id);
11578
+ if (existing && compareClock(existing.updatedAt, updatedAt) > 0)
11579
+ return false;
11580
+ await this.options.client.query(`INSERT INTO ${this.tableName} (
11581
+ service, object_type, object_id, payload, updated_at,
11582
+ deleted_at, source_machine_id, version
11583
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
11584
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
11585
+ payload = EXCLUDED.payload,
11586
+ updated_at = EXCLUDED.updated_at,
11587
+ deleted_at = EXCLUDED.deleted_at,
11588
+ source_machine_id = EXCLUDED.source_machine_id,
11589
+ version = EXCLUDED.version
11590
+ WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
11591
+ this.service,
11592
+ tombstone.object_type,
11593
+ tombstone.object_id,
11594
+ JSON.stringify(tombstone.payload ?? { id: tombstone.object_id, deleted_at: deletedAt }),
11595
+ updatedAt,
11596
+ deletedAt,
11597
+ tombstone.source_machine_id ?? context.requestId ?? this.sourceMachineId ?? null,
11598
+ tombstone.version ?? null
11599
+ ]);
11184
11600
  return true;
11185
11601
  }
11602
+ async clock(type, id) {
11603
+ await this.ensureSchema();
11604
+ const result = await this.options.client.query(`SELECT object_type, object_id, updated_at, deleted_at
11605
+ FROM ${this.tableName}
11606
+ WHERE service = $1 AND object_type = $2 AND object_id = $3
11607
+ LIMIT 1`, [this.service, type, id]);
11608
+ const row = result.rows[0];
11609
+ if (!row)
11610
+ return null;
11611
+ return {
11612
+ updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString(),
11613
+ deletedAt: stringValue2(row.deleted_at)
11614
+ };
11615
+ }
11186
11616
  async getCursor(name) {
11187
11617
  await this.ensureSchema();
11188
11618
  const result = await this.options.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
@@ -11251,7 +11681,10 @@ async function createTask2(input, store, context) {
11251
11681
  runner_started_at: null,
11252
11682
  runner_completed_at: null,
11253
11683
  current_step: null,
11254
- total_steps: null
11684
+ total_steps: null,
11685
+ machine_id: store.machineId(context),
11686
+ synced_at: null,
11687
+ archived_at: null
11255
11688
  };
11256
11689
  await store.upsert("tasks", task, context);
11257
11690
  await logTaskChange2(task.id, "created", "status", null, task.status, task.assigned_by ?? task.agent_id, store, context);
@@ -11376,7 +11809,9 @@ async function createProject2(input, store, context) {
11376
11809
  task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
11377
11810
  task_counter: 0,
11378
11811
  created_at: timestamp,
11379
- updated_at: timestamp
11812
+ updated_at: timestamp,
11813
+ machine_id: store.machineId(context),
11814
+ synced_at: null
11380
11815
  };
11381
11816
  return store.upsert("projects", project, context);
11382
11817
  }
@@ -11396,7 +11831,9 @@ async function createPlan2(input, store, context) {
11396
11831
  description: input.description ?? null,
11397
11832
  status: input.status ?? "active",
11398
11833
  created_at: timestamp,
11399
- updated_at: timestamp
11834
+ updated_at: timestamp,
11835
+ machine_id: store.machineId(context),
11836
+ synced_at: null
11400
11837
  }, context);
11401
11838
  }
11402
11839
  async function updatePlan2(id, input, store) {
@@ -11426,7 +11863,9 @@ async function registerAgent2(input, store, context) {
11426
11863
  last_seen_at: timestamp,
11427
11864
  session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
11428
11865
  working_dir: input.working_dir ?? existing?.working_dir ?? null,
11429
- active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null
11866
+ active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null,
11867
+ machine_id: existing?.machine_id ?? store.machineId(context),
11868
+ synced_at: existing?.synced_at ?? null
11430
11869
  };
11431
11870
  return store.upsert("agents", agent, context);
11432
11871
  }
@@ -11453,7 +11892,9 @@ async function createTaskList2(input, store, context) {
11453
11892
  description: input.description ?? null,
11454
11893
  metadata: input.metadata ?? {},
11455
11894
  created_at: timestamp,
11456
- updated_at: timestamp
11895
+ updated_at: timestamp,
11896
+ machine_id: store.machineId(context),
11897
+ synced_at: null
11457
11898
  }, context);
11458
11899
  }
11459
11900
  async function updateTaskList2(id, input, store) {
@@ -11479,7 +11920,9 @@ async function createTemplate2(input, store, context) {
11479
11920
  project_id: input.project_id ?? context?.projectId ?? null,
11480
11921
  plan_id: input.plan_id ?? null,
11481
11922
  metadata: input.metadata ?? {},
11482
- created_at: timestamp
11923
+ created_at: timestamp,
11924
+ machine_id: store.machineId(context),
11925
+ synced_at: null
11483
11926
  }, context);
11484
11927
  }
11485
11928
  async function updateTemplate2(id, input, store) {
@@ -11504,7 +11947,8 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
11504
11947
  old_value: oldValue ?? null,
11505
11948
  new_value: newValue ?? null,
11506
11949
  agent_id: agentId ?? context?.agentId ?? null,
11507
- created_at: new Date().toISOString()
11950
+ created_at: new Date().toISOString(),
11951
+ machine_id: store.machineId(context)
11508
11952
  };
11509
11953
  return store.upsert("audit_history", entry2, context);
11510
11954
  }
@@ -11527,18 +11971,21 @@ async function exportSnapshot(store) {
11527
11971
  source: "postgres",
11528
11972
  tasks: await store.list("tasks"),
11529
11973
  projects: await store.list("projects"),
11974
+ projectMachinePaths: await store.list("project_machine_paths"),
11530
11975
  plans: await store.list("plans"),
11531
11976
  agents: await store.list("agents"),
11532
11977
  taskLists: await store.list("task_lists"),
11533
11978
  templates: await store.list("templates"),
11534
- auditHistory: await store.list("audit_history")
11979
+ auditHistory: await store.list("audit_history"),
11980
+ tombstones: await store.listTombstones()
11535
11981
  };
11536
11982
  }
11537
11983
  async function importSnapshot(snapshot, store, context) {
11538
- const result = { inserted: 0, updated: 0, skipped: 0, errors: [] };
11984
+ const result = { inserted: 0, updated: 0, deleted: 0, skipped: 0, errors: [] };
11539
11985
  const entries = [
11540
11986
  ...snapshot.tasks.map((row) => ["tasks", row]),
11541
11987
  ...snapshot.projects.map((row) => ["projects", row]),
11988
+ ...(snapshot.projectMachinePaths ?? []).map((row) => ["project_machine_paths", row]),
11542
11989
  ...snapshot.plans.map((row) => ["plans", row]),
11543
11990
  ...snapshot.agents.map((row) => ["agents", row]),
11544
11991
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
@@ -11557,6 +12004,25 @@ async function importSnapshot(snapshot, store, context) {
11557
12004
  result.errors.push(error instanceof Error ? error.message : String(error));
11558
12005
  }
11559
12006
  }
12007
+ for (const tombstone of snapshot.tombstones ?? []) {
12008
+ try {
12009
+ const deleted = await store.tombstone({
12010
+ object_type: tombstone.object_type,
12011
+ object_id: tombstone.object_id,
12012
+ deleted_at: tombstone.deleted_at,
12013
+ updated_at: tombstone.updated_at,
12014
+ source_machine_id: tombstone.source_machine_id ?? null,
12015
+ payload: tombstone.payload ?? null,
12016
+ version: tombstone.version ?? null
12017
+ }, context);
12018
+ if (deleted)
12019
+ result.deleted = (result.deleted ?? 0) + 1;
12020
+ else
12021
+ result.skipped += 1;
12022
+ } catch (error) {
12023
+ result.errors.push(error instanceof Error ? error.message : String(error));
12024
+ }
12025
+ }
11560
12026
  return result;
11561
12027
  }
11562
12028
  async function requireRecord(type, id, store) {
@@ -11639,8 +12105,17 @@ function payloadRecord2(value) {
11639
12105
  throw new Error("Postgres storage payload must be a JSON object");
11640
12106
  }
11641
12107
  function stringValue2(value) {
12108
+ if (value instanceof Date)
12109
+ return value.toISOString();
11642
12110
  return typeof value === "string" && value ? value : null;
11643
12111
  }
12112
+ function compareClock(left, right) {
12113
+ const leftClock = Date.parse(left);
12114
+ const rightClock = Date.parse(right);
12115
+ if (Number.isNaN(leftClock) || Number.isNaN(rightClock))
12116
+ return left.localeCompare(right);
12117
+ return leftClock - rightClock;
12118
+ }
11644
12119
  function numberValue2(value) {
11645
12120
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
11646
12121
  }