@hasna/todos 0.11.65 → 0.11.67

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 (55) hide show
  1. package/README.md +78 -49
  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/query-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/storage-commands.d.ts +5 -0
  7. package/dist/cli/commands/storage-commands.d.ts.map +1 -1
  8. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  9. package/dist/cli/index.js +1034 -231
  10. package/dist/contracts.js +409 -129
  11. package/dist/db/agents.d.ts.map +1 -1
  12. package/dist/db/audit.d.ts.map +1 -1
  13. package/dist/db/migrations.d.ts.map +1 -1
  14. package/dist/db/plans.d.ts.map +1 -1
  15. package/dist/db/projects.d.ts.map +1 -1
  16. package/dist/db/schema.d.ts.map +1 -1
  17. package/dist/db/storage-tombstones.d.ts +26 -0
  18. package/dist/db/storage-tombstones.d.ts.map +1 -0
  19. package/dist/db/task-crud.d.ts.map +1 -1
  20. package/dist/db/task-lifecycle.d.ts.map +1 -1
  21. package/dist/db/task-lists.d.ts.map +1 -1
  22. package/dist/db/task-runs.d.ts.map +1 -1
  23. package/dist/db/templates.d.ts.map +1 -1
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +928 -219
  27. package/dist/lib/approval-gates.d.ts.map +1 -1
  28. package/dist/lib/event-emission-safety.d.ts +9 -0
  29. package/dist/lib/event-emission-safety.d.ts.map +1 -0
  30. package/dist/lib/event-hooks.d.ts +1 -0
  31. package/dist/lib/event-hooks.d.ts.map +1 -1
  32. package/dist/lib/feature-manifest.d.ts.map +1 -1
  33. package/dist/lib/project-bootstrap.d.ts +1 -0
  34. package/dist/lib/project-bootstrap.d.ts.map +1 -1
  35. package/dist/lib/review-queues.d.ts.map +1 -1
  36. package/dist/lib/shared-events.d.ts +1 -0
  37. package/dist/lib/shared-events.d.ts.map +1 -1
  38. package/dist/lib/task-route-contract.d.ts +33 -0
  39. package/dist/lib/task-route-contract.d.ts.map +1 -0
  40. package/dist/lib/task-routing.d.ts +55 -0
  41. package/dist/lib/task-routing.d.ts.map +1 -0
  42. package/dist/mcp/index.js +459 -162
  43. package/dist/registry.js +409 -129
  44. package/dist/release-provenance.json +3 -3
  45. package/dist/server/index.js +482 -185
  46. package/dist/storage/interfaces.d.ts +20 -0
  47. package/dist/storage/interfaces.d.ts.map +1 -1
  48. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  49. package/dist/storage/postgres-sync.d.ts +6 -1
  50. package/dist/storage/postgres-sync.d.ts.map +1 -1
  51. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  52. package/dist/storage.js +677 -152
  53. package/dist/types/index.d.ts +22 -0
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/package.json +1 -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) {
@@ -5176,39 +5357,9 @@ function ensureTaskList(name, slug, projectId, db) {
5176
5357
  return createTaskList({ name, slug, project_id: projectId }, d);
5177
5358
  }
5178
5359
 
5179
- // src/lib/shared-events.ts
5180
- var SOURCE = "todos";
5181
- function taskEventData(task, extra = {}) {
5182
- return {
5183
- id: task.id,
5184
- task_id: task.id,
5185
- short_id: task.short_id,
5186
- title: task.title,
5187
- description: task.description,
5188
- status: task.status,
5189
- priority: task.priority,
5190
- project_id: task.project_id,
5191
- parent_id: task.parent_id,
5192
- plan_id: task.plan_id,
5193
- task_list_id: task.task_list_id,
5194
- agent_id: task.agent_id,
5195
- assigned_to: task.assigned_to,
5196
- session_id: task.session_id,
5197
- working_dir: task.working_dir,
5198
- tags: task.tags,
5199
- metadata: task.metadata,
5200
- version: task.version,
5201
- created_at: task.created_at,
5202
- updated_at: task.updated_at,
5203
- started_at: task.started_at,
5204
- completed_at: task.completed_at,
5205
- due_at: task.due_at,
5206
- requires_approval: task.requires_approval,
5207
- approved_by: task.approved_by,
5208
- approved_at: task.approved_at,
5209
- ...extra
5210
- };
5211
- }
5360
+ // src/lib/task-route-contract.ts
5361
+ var TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION = "todos.task_route_state.v1";
5362
+ var TASK_WORKFLOW_POINTER_SCHEMA_VERSION = "todos.task_workflow_pointer.v1";
5212
5363
  function booleanField(value) {
5213
5364
  if (typeof value === "boolean")
5214
5365
  return value;
@@ -5230,21 +5381,31 @@ function booleanField(value) {
5230
5381
  function objectField(value) {
5231
5382
  return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
5232
5383
  }
5233
- function firstBoolean(records, keys) {
5384
+ function collectBooleans(records, keys) {
5385
+ const values = [];
5234
5386
  for (const record of records) {
5387
+ if (!record)
5388
+ continue;
5235
5389
  for (const key of keys) {
5236
5390
  const value = booleanField(record[key]);
5237
5391
  if (value !== undefined)
5238
- return value;
5392
+ values.push(value);
5239
5393
  }
5240
5394
  }
5241
- return;
5395
+ return values;
5242
5396
  }
5243
- function routingAutomationMetadata(task) {
5244
- const automation = objectField(task.metadata.automation);
5245
- const records = [task.metadata];
5246
- if (automation)
5247
- records.push(automation);
5397
+ function mergedBoolean(records, keys, trueWins) {
5398
+ const values = collectBooleans(records, keys);
5399
+ if (values.length === 0)
5400
+ return;
5401
+ if (trueWins)
5402
+ return values.some(Boolean);
5403
+ return values.includes(false) ? false : true;
5404
+ }
5405
+ function routingAutomationMetadata(task, taskList) {
5406
+ const taskAutomation = objectField(task.metadata.automation);
5407
+ const taskListAutomation = taskList ? objectField(taskList.metadata.automation) : undefined;
5408
+ const records = [task.metadata, taskAutomation, taskList?.metadata, taskListAutomation];
5248
5409
  const result = {};
5249
5410
  const aliases = [
5250
5411
  ["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
@@ -5255,7 +5416,7 @@ function routingAutomationMetadata(task) {
5255
5416
  ["approval_required", ["approval_required", "approvalRequired"]]
5256
5417
  ];
5257
5418
  for (const [canonical, keys] of aliases) {
5258
- const value = firstBoolean(records, keys);
5419
+ const value = mergedBoolean(records, keys, canonical !== "allowed");
5259
5420
  if (value !== undefined)
5260
5421
  result[canonical] = value;
5261
5422
  }
@@ -5263,23 +5424,91 @@ function routingAutomationMetadata(task) {
5263
5424
  result.requires_approval = true;
5264
5425
  return Object.keys(result).length > 0 ? result : undefined;
5265
5426
  }
5427
+ function routeEnabledForTask(task, taskList) {
5428
+ const explicit = booleanField(task.metadata.route_enabled);
5429
+ if (explicit !== undefined)
5430
+ return explicit;
5431
+ if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
5432
+ return true;
5433
+ const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
5434
+ if (taskListDefault !== undefined)
5435
+ return taskListDefault;
5436
+ return;
5437
+ }
5438
+ function stringField(value) {
5439
+ return typeof value === "string" && value.trim() ? value : undefined;
5440
+ }
5441
+ function workflowPointersFromMetadata(metadata) {
5442
+ const nested = objectField(metadata.workflow_invocation) ?? objectField(metadata.workflow) ?? {};
5443
+ return {
5444
+ current_workflow_invocation_id: stringField(metadata.current_workflow_invocation_id) ?? stringField(nested.current_workflow_invocation_id) ?? stringField(nested.invocation_id),
5445
+ current_run_id: stringField(metadata.current_run_id) ?? stringField(nested.current_run_id) ?? stringField(nested.run_id),
5446
+ latest_manifest_path: stringField(metadata.latest_manifest_path) ?? stringField(nested.latest_manifest_path) ?? stringField(nested.manifest_path),
5447
+ latest_evaluation_path: stringField(metadata.latest_evaluation_path) ?? stringField(nested.latest_evaluation_path) ?? stringField(nested.evaluation_path),
5448
+ workflow_state: stringField(metadata.workflow_state) ?? stringField(nested.workflow_state) ?? stringField(nested.state)
5449
+ };
5450
+ }
5451
+ function compactWorkflowPointers(pointers) {
5452
+ return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
5453
+ }
5454
+ function classifyProjectKind(path) {
5455
+ return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
5456
+ }
5457
+ function isWorktreePath(path) {
5458
+ return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
5459
+ }
5460
+ function inferRootProjectId(project) {
5461
+ return isWorktreePath(project.path) ? null : project.id;
5462
+ }
5463
+
5464
+ // src/lib/shared-events.ts
5465
+ var SOURCE = "todos";
5466
+ function taskEventData(task, extra = {}) {
5467
+ return {
5468
+ id: task.id,
5469
+ task_id: task.id,
5470
+ short_id: task.short_id,
5471
+ title: task.title,
5472
+ description: task.description,
5473
+ status: task.status,
5474
+ priority: task.priority,
5475
+ project_id: task.project_id,
5476
+ parent_id: task.parent_id,
5477
+ plan_id: task.plan_id,
5478
+ task_list_id: task.task_list_id,
5479
+ agent_id: task.agent_id,
5480
+ assigned_to: task.assigned_to,
5481
+ session_id: task.session_id,
5482
+ working_dir: task.working_dir,
5483
+ tags: task.tags,
5484
+ metadata: task.metadata,
5485
+ version: task.version,
5486
+ created_at: task.created_at,
5487
+ updated_at: task.updated_at,
5488
+ started_at: task.started_at,
5489
+ completed_at: task.completed_at,
5490
+ due_at: task.due_at,
5491
+ requires_approval: task.requires_approval,
5492
+ approved_by: task.approved_by,
5493
+ approved_at: task.approved_at,
5494
+ ...extra
5495
+ };
5496
+ }
5266
5497
  function taskEventMetadata(task) {
5267
5498
  const metadata = {
5268
5499
  package: "@hasna/todos",
5269
5500
  todos_event_schema_version: 1,
5501
+ route_state_schema_version: TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
5270
5502
  task_id: task.id,
5271
5503
  task_short_id: task.short_id,
5272
5504
  project_id: task.project_id,
5273
5505
  task_list_id: task.task_list_id,
5274
5506
  working_dir: task.working_dir
5275
5507
  };
5276
- const routeEnabled = booleanField(task.metadata.route_enabled);
5277
- if (routeEnabled !== undefined) {
5278
- metadata.route_enabled = routeEnabled;
5279
- }
5280
- const automation = routingAutomationMetadata(task);
5281
- if (automation) {
5282
- metadata.automation = automation;
5508
+ const pointers = workflowPointersFromMetadata(task.metadata);
5509
+ for (const [key, value] of Object.entries(pointers)) {
5510
+ if (value)
5511
+ metadata[key] = value;
5283
5512
  }
5284
5513
  try {
5285
5514
  const project = task.project_id ? getProject(task.project_id) : null;
@@ -5308,18 +5537,20 @@ function taskEventMetadata(task) {
5308
5537
  metadata.task_list_project_id = taskList.project_id;
5309
5538
  metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
5310
5539
  }
5540
+ const routeEnabled = routeEnabledForTask(task, taskList);
5541
+ if (routeEnabled !== undefined) {
5542
+ metadata.route_enabled = routeEnabled;
5543
+ }
5544
+ const automation = routingAutomationMetadata(task, taskList);
5545
+ if (automation) {
5546
+ metadata.automation = automation;
5547
+ metadata.route_blocked_by_no_auto = automation.no_auto === true;
5548
+ metadata.route_blocked_by_manual = automation.manual === true || automation.manual_required === true;
5549
+ metadata.route_blocked_by_approval = (automation.requires_approval === true || automation.approval_required === true) && !task.approved_by;
5550
+ }
5311
5551
  } catch {}
5312
5552
  return metadata;
5313
5553
  }
5314
- function classifyProjectKind(path) {
5315
- return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
5316
- }
5317
- function isWorktreePath(path) {
5318
- return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
5319
- }
5320
- function inferRootProjectId(project) {
5321
- return isWorktreePath(project.path) ? null : project.id;
5322
- }
5323
5554
  function readMachineLocalPath(project) {
5324
5555
  const machineId = process.env["TODOS_MACHINE_ID"];
5325
5556
  if (!machineId)
@@ -5332,6 +5563,8 @@ function readMachineLocalPath(project) {
5332
5563
  }
5333
5564
  }
5334
5565
  async function emitSharedTaskEvent(input) {
5566
+ if (!shouldEmitSharedTaskEvents(input.databasePath))
5567
+ return;
5335
5568
  const data = taskEventData(input.task, input.data);
5336
5569
  await new EventsClient().emit({
5337
5570
  source: SOURCE,
@@ -5356,8 +5589,9 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
5356
5589
  const d = db || getDatabase();
5357
5590
  const id = uuid();
5358
5591
  const timestamp = now();
5359
- d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at)
5360
- 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]);
5361
5595
  try {
5362
5596
  const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
5363
5597
  logActivity2({
@@ -5370,7 +5604,7 @@ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
5370
5604
  actor_id: agentId ?? undefined
5371
5605
  }, d);
5372
5606
  } catch {}
5373
- 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 };
5374
5608
  }
5375
5609
  function getTaskHistory(taskId, db) {
5376
5610
  const d = db || getDatabase();
@@ -5719,13 +5953,14 @@ function createTask(input, db) {
5719
5953
  const d = db || getDatabase();
5720
5954
  const timestamp = now();
5721
5955
  const tags = input.tags || [];
5956
+ const machineId = currentStorageMachineId(d);
5722
5957
  const assignedBy = input.assigned_by || input.agent_id;
5723
5958
  const assignedFromProject = input.assigned_from_project || null;
5724
5959
  let id = uuid();
5725
5960
  for (let attempt = 0;attempt < 3; attempt++) {
5726
5961
  try {
5727
- 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)
5728
- 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, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5729
5964
  id,
5730
5965
  null,
5731
5966
  input.project_id || null,
@@ -5762,7 +5997,8 @@ function createTask(input, db) {
5762
5997
  input.spawned_from_session || null,
5763
5998
  assignedBy || null,
5764
5999
  assignedFromProject || null,
5765
- input.task_type || null
6000
+ input.task_type || null,
6001
+ machineId
5766
6002
  ]);
5767
6003
  break;
5768
6004
  } catch (e) {
@@ -5778,9 +6014,10 @@ function createTask(input, db) {
5778
6014
  }
5779
6015
  const task = getTask(id, d);
5780
6016
  const payload = taskEventData(task);
6017
+ const databasePath = databasePathFromDatabase(d);
5781
6018
  dispatchWebhook2("task.created", payload, d).catch(() => {});
5782
- emitLocalEventHooksQuiet({ type: "task.created", payload });
5783
- emitSharedTaskEventQuiet({ type: "task.created", task });
6019
+ emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
6020
+ emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
5784
6021
  return task;
5785
6022
  }
5786
6023
  function getTask(id, db) {
@@ -6198,29 +6435,39 @@ function updateTask(id, input, db) {
6198
6435
  approved_by: input.approved_by ?? task.approved_by,
6199
6436
  approved_at: input.approved_by ? timestamp : task.approved_at
6200
6437
  };
6438
+ const databasePath = databasePathFromDatabase(d);
6201
6439
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
6202
6440
  const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
6203
6441
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
6204
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
6205
- 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 });
6206
6444
  }
6207
6445
  if (input.status !== undefined && input.status !== task.status) {
6208
6446
  const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
6209
6447
  dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
6210
- emitLocalEventHooksQuiet({ type: "task.status_changed", payload });
6211
- 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 });
6212
6450
  }
6213
6451
  if (input.approved_by !== undefined) {
6214
- 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 });
6215
6453
  }
6216
6454
  const updatePayload = taskEventData(updatedTask);
6217
6455
  dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
6218
- emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
6219
- emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
6456
+ emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
6457
+ emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
6220
6458
  return updatedTask;
6221
6459
  }
6222
6460
  function deleteTask(id, db) {
6223
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);
6224
6471
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
6225
6472
  return result.changes > 0;
6226
6473
  }
@@ -6352,8 +6599,9 @@ function resolveTemplateId(id, d) {
6352
6599
  function createTemplate(input, db) {
6353
6600
  const d = db || getDatabase();
6354
6601
  const id = uuid();
6355
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
6356
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6357
6605
  id,
6358
6606
  input.name,
6359
6607
  input.title_pattern,
@@ -6364,7 +6612,8 @@ function createTemplate(input, db) {
6364
6612
  input.project_id || null,
6365
6613
  input.plan_id || null,
6366
6614
  JSON.stringify(input.metadata || {}),
6367
- now()
6615
+ now(),
6616
+ machineId
6368
6617
  ]);
6369
6618
  if (input.tasks && input.tasks.length > 0) {
6370
6619
  addTemplateTasks(id, input.tasks, d);
@@ -6388,6 +6637,15 @@ function deleteTemplate(id, db) {
6388
6637
  const resolved = resolveTemplateId(id, d);
6389
6638
  if (!resolved)
6390
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);
6391
6649
  return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
6392
6650
  }
6393
6651
  function updateTemplate(id, updates, db) {
@@ -6917,6 +7175,7 @@ function getBlockingDeps(id, db) {
6917
7175
  }
6918
7176
  function startTask(id, agentId, db) {
6919
7177
  const d = db || getDatabase();
7178
+ const databasePath = databasePathFromDatabase(d);
6920
7179
  const task = getTask(id, d);
6921
7180
  if (!task)
6922
7181
  throw new TaskNotFoundError(id);
@@ -6931,7 +7190,8 @@ function startTask(id, agentId, db) {
6931
7190
  agent_id: agentId,
6932
7191
  title: task.title,
6933
7192
  blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
6934
- }
7193
+ },
7194
+ databasePath
6935
7195
  });
6936
7196
  throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
6937
7197
  }
@@ -6953,12 +7213,13 @@ function startTask(id, agentId, db) {
6953
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 };
6954
7214
  const payload = taskEventData(startedTask, { agent_id: agentId });
6955
7215
  dispatchWebhook2("task.started", payload, d).catch(() => {});
6956
- emitLocalEventHooksQuiet({ type: "task.started", payload });
6957
- 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 });
6958
7218
  return startedTask;
6959
7219
  }
6960
7220
  function completeTask(id, agentId, db, options) {
6961
7221
  const d = db || getDatabase();
7222
+ const databasePath = databasePathFromDatabase(d);
6962
7223
  const task = getTask(id, d);
6963
7224
  if (!task)
6964
7225
  throw new TaskNotFoundError(id);
@@ -7004,8 +7265,8 @@ function completeTask(id, agentId, db, options) {
7004
7265
  };
7005
7266
  const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
7006
7267
  dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
7007
- emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload });
7008
- 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 });
7009
7270
  let spawnedTask = null;
7010
7271
  if (task.recurrence_rule && !options?.skip_recurrence) {
7011
7272
  spawnedTask = spawnNextRecurrence(task, d, timestamp);
@@ -7049,9 +7310,9 @@ function completeTask(id, agentId, db, options) {
7049
7310
  const depTask = getTask(dep.id, d);
7050
7311
  const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
7051
7312
  dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
7052
- emitLocalEventHooksQuiet({ type: "task.unblocked", payload });
7313
+ emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
7053
7314
  if (depTask)
7054
- emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id } });
7315
+ emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
7055
7316
  }
7056
7317
  }
7057
7318
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
@@ -7221,6 +7482,7 @@ function getTasksChangedSince(since, filters, db) {
7221
7482
  }
7222
7483
  function failTask(id, agentId, reason, options, db) {
7223
7484
  const d = db || getDatabase();
7485
+ const databasePath = databasePathFromDatabase(d);
7224
7486
  const task = getTask(id, d);
7225
7487
  if (!task)
7226
7488
  throw new TaskNotFoundError(id);
@@ -7249,8 +7511,8 @@ function failTask(id, agentId, reason, options, db) {
7249
7511
  logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
7250
7512
  const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
7251
7513
  dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
7252
- emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload });
7253
- 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 });
7254
7516
  let retryTask;
7255
7517
  if (options?.retry) {
7256
7518
  const retryCount = (task.retry_count || 0) + 1;
@@ -7310,6 +7572,7 @@ function getStaleTasks(staleQuery = 30, filters, db) {
7310
7572
  }
7311
7573
  function stealTask(agentId, opts, db) {
7312
7574
  const d = db || getDatabase();
7575
+ const databasePath = databasePathFromDatabase(d);
7313
7576
  const staleMinutes = opts?.stale_minutes ?? 30;
7314
7577
  const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
7315
7578
  if (staleTasks.length === 0)
@@ -7328,8 +7591,8 @@ function stealTask(agentId, opts, db) {
7328
7591
  const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
7329
7592
  const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
7330
7593
  dispatchWebhook2("task.assigned", payload, d).catch(() => {});
7331
- emitLocalEventHooksQuiet({ type: "task.assigned", payload });
7332
- 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 });
7333
7596
  return stolenTask;
7334
7597
  }
7335
7598
  function claimOrSteal(agentId, filters, db) {
@@ -7920,8 +8183,9 @@ function createPlan(input, db) {
7920
8183
  const d = db || getDatabase();
7921
8184
  const id = uuid();
7922
8185
  const timestamp = now();
7923
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
7924
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
7925
8189
  id,
7926
8190
  input.project_id || null,
7927
8191
  input.task_list_id || null,
@@ -7930,7 +8194,8 @@ function createPlan(input, db) {
7930
8194
  input.description || null,
7931
8195
  input.status || "active",
7932
8196
  timestamp,
7933
- timestamp
8197
+ timestamp,
8198
+ machineId
7934
8199
  ]);
7935
8200
  return getPlan(id, d);
7936
8201
  }
@@ -7978,12 +8243,21 @@ function updatePlan(id, input, db) {
7978
8243
  const updated = getPlan(id, d);
7979
8244
  emitLocalEventHooksQuiet({
7980
8245
  type: "plan.updated",
7981
- 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)
7982
8248
  });
7983
8249
  return updated;
7984
8250
  }
7985
8251
  function deletePlan(id, db) {
7986
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);
7987
8261
  const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
7988
8262
  return result.changes > 0;
7989
8263
  }
@@ -8390,20 +8664,20 @@ init_database();
8390
8664
  init_redaction();
8391
8665
  import { createHash as createHash2 } from "crypto";
8392
8666
  import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
8393
- import { basename, dirname as dirname4, join as join5, resolve as resolve6 } from "path";
8394
- 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";
8395
8669
  function isInMemoryDb2(path) {
8396
8670
  return path === ":memory:" || path.startsWith("file::memory:");
8397
8671
  }
8398
8672
  function artifactStoreRoot() {
8399
8673
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
8400
- return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
8674
+ return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
8401
8675
  if (process.env["TODOS_ARTIFACTS_DIR"])
8402
- return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
8676
+ return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
8403
8677
  const dbPath = getDatabasePath();
8404
8678
  if (isInMemoryDb2(dbPath))
8405
- return join5(tmpdir(), "hasna-todos-artifacts");
8406
- return join5(dirname4(resolve6(dbPath)), "artifacts");
8679
+ return join5(tmpdir2(), "hasna-todos-artifacts");
8680
+ return join5(dirname4(resolve7(dbPath)), "artifacts");
8407
8681
  }
8408
8682
  function artifactStorePath(relativePath) {
8409
8683
  const normalized = relativePath.replace(/\\/g, "/");
@@ -8450,7 +8724,7 @@ function mediaTypeFor(path, textLike) {
8450
8724
  return "application/octet-stream";
8451
8725
  }
8452
8726
  function storeArtifactContent(input) {
8453
- const sourcePath = resolve6(input.path);
8727
+ const sourcePath = resolve7(input.path);
8454
8728
  if (!existsSync7(sourcePath))
8455
8729
  return null;
8456
8730
  const sourceStat = statSync2(sourcePath);
@@ -8606,19 +8880,19 @@ function importStoredArtifactContent(content) {
8606
8880
  var DEFAULT_DELETED_RETENTION_DAYS = 30;
8607
8881
  function getArtifactStoreRoot(dbPath) {
8608
8882
  if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
8609
- return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
8883
+ return resolve7(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
8610
8884
  if (process.env["TODOS_ARTIFACTS_DIR"])
8611
- return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
8885
+ return resolve7(process.env["TODOS_ARTIFACTS_DIR"]);
8612
8886
  const path = dbPath ?? getDatabasePath();
8613
8887
  if (isInMemoryDb2(path))
8614
- return join5(tmpdir(), "hasna-todos-artifacts");
8615
- return join5(dirname4(resolve6(path)), "artifacts");
8888
+ return join5(tmpdir2(), "hasna-todos-artifacts");
8889
+ return join5(dirname4(resolve7(path)), "artifacts");
8616
8890
  }
8617
8891
  function computeContentHash(path) {
8618
- return sha256(readFileSync3(resolve6(path)));
8892
+ return sha256(readFileSync3(resolve7(path)));
8619
8893
  }
8620
8894
  function storeArtifactFile(input) {
8621
- const sourcePath = resolve6(input.sourcePath);
8895
+ const sourcePath = resolve7(input.sourcePath);
8622
8896
  if (!existsSync7(sourcePath)) {
8623
8897
  throw new Error(`Source file not found: ${input.sourcePath}`);
8624
8898
  }
@@ -8667,7 +8941,7 @@ function buildArtifactExportManifest(artifacts, dbPath) {
8667
8941
  };
8668
8942
  }
8669
8943
  function writeArtifactExportManifest(manifest, outputPath) {
8670
- const destination = resolve6(outputPath);
8944
+ const destination = resolve7(outputPath);
8671
8945
  mkdirSync4(dirname4(destination), { recursive: true });
8672
8946
  writeFileSync2(destination, `${JSON.stringify(manifest, null, 2)}
8673
8947
  `);
@@ -9054,7 +9328,11 @@ function startTaskRun(input, db) {
9054
9328
  }, d);
9055
9329
  }
9056
9330
  const run = getTaskRun(id, d);
9057
- 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
+ });
9058
9336
  return run;
9059
9337
  }
9060
9338
  function beginTaskRunTransaction(input, db) {
@@ -9323,7 +9601,8 @@ function finishTaskRun(input, db) {
9323
9601
  const updated = getTaskRun(run.id, d);
9324
9602
  emitLocalEventHooksQuiet({
9325
9603
  type: `run.${input.status}`,
9326
- 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)
9327
9606
  });
9328
9607
  return updated;
9329
9608
  }
@@ -10155,6 +10434,7 @@ function rowToAgent(row) {
10155
10434
  }
10156
10435
  function registerAgent(input, db) {
10157
10436
  const d = db || getDatabase();
10437
+ const machineId = currentStorageMachineId(d);
10158
10438
  const existingNames = d.query("SELECT name FROM agents").all().map((row) => row.name);
10159
10439
  const normalizedName = validateAgentName(input.name, existingNames);
10160
10440
  const existing = getAgentByName(normalizedName, d);
@@ -10195,14 +10475,18 @@ function registerAgent(input, db) {
10195
10475
  updates.push("active_project_id = ?");
10196
10476
  params.push(input.project_id);
10197
10477
  }
10478
+ if (!existing.machine_id && machineId) {
10479
+ updates.push("machine_id = ?");
10480
+ params.push(machineId);
10481
+ }
10198
10482
  params.push(existing.id);
10199
10483
  d.run(`UPDATE agents SET ${updates.join(", ")} WHERE id = ?`, params);
10200
10484
  return getAgent(existing.id, d);
10201
10485
  }
10202
10486
  const id = shortUuid();
10203
10487
  const timestamp = now();
10204
- 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)
10205
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10206
10490
  id,
10207
10491
  normalizedName,
10208
10492
  input.description || null,
@@ -10218,7 +10502,8 @@ function registerAgent(input, db) {
10218
10502
  timestamp,
10219
10503
  input.session_id || null,
10220
10504
  input.working_dir || null,
10221
- input.project_id && input.session_id ? input.project_id : null
10505
+ input.project_id && input.session_id ? input.project_id : null,
10506
+ machineId
10222
10507
  ]);
10223
10508
  return getAgent(id, d);
10224
10509
  }
@@ -10418,6 +10703,14 @@ var PROJECT_COLUMNS = [
10418
10703
  "machine_id",
10419
10704
  "synced_at"
10420
10705
  ];
10706
+ var PROJECT_MACHINE_PATH_COLUMNS = [
10707
+ "id",
10708
+ "project_id",
10709
+ "machine_id",
10710
+ "path",
10711
+ "created_at",
10712
+ "updated_at"
10713
+ ];
10421
10714
  var TASK_LIST_COLUMNS = [
10422
10715
  "id",
10423
10716
  "project_id",
@@ -10557,11 +10850,13 @@ function exportSqliteTodosStorageSnapshot(db) {
10557
10850
  source: "sqlite",
10558
10851
  tasks: listTasks({ include_archived: true }, d),
10559
10852
  projects: listProjects(d),
10853
+ projectMachinePaths: listProjectMachinePaths(d),
10560
10854
  plans: listPlans(undefined, d),
10561
10855
  agents: listAgents({ include_archived: true }, d),
10562
10856
  taskLists: listTaskLists(undefined, d),
10563
10857
  templates: listTemplates(d),
10564
- auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d)
10858
+ auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
10859
+ tombstones: listStorageTombstones(d)
10565
10860
  };
10566
10861
  }
10567
10862
  function importSqliteTodosStorageSnapshot(snapshot, db) {
@@ -10569,13 +10864,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
10569
10864
  const result = {
10570
10865
  inserted: 0,
10571
10866
  updated: 0,
10867
+ deleted: 0,
10572
10868
  skipped: 0,
10573
10869
  errors: []
10574
10870
  };
10575
- const applyRows = (table, columns, rows, updateClockColumn, afterUpsert) => {
10871
+ const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
10576
10872
  for (const row of rows) {
10577
10873
  try {
10578
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
+ }
10579
10880
  const state = upsertById(d, table, columns, record, updateClockColumn);
10580
10881
  if (state === "inserted")
10581
10882
  result.inserted += 1;
@@ -10589,17 +10890,19 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
10589
10890
  }
10590
10891
  }
10591
10892
  };
10592
- applyRows("projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
10593
- applyRows("agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
10594
- applyRows("task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
10595
- applyRows("plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
10596
- applyRows("task_templates", TEMPLATE_COLUMNS, snapshot.templates);
10597
- 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) => {
10598
10900
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
10599
10901
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
10600
10902
  }
10601
10903
  });
10602
- applyRows("task_history", AUDIT_COLUMNS, snapshot.auditHistory);
10904
+ applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
10905
+ applyTombstones(d, snapshot.tombstones ?? [], result);
10603
10906
  return result;
10604
10907
  }
10605
10908
  function upsertById(db, table, columns, row, updateClockColumn) {
@@ -10654,6 +10957,85 @@ function sortedTasks(tasks) {
10654
10957
  visit(task);
10655
10958
  return result;
10656
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
+ }
10657
11039
 
10658
11040
  // src/storage/local-sqlite.ts
10659
11041
  function createLocalSqliteTodosStorageAdapter(options = {}) {
@@ -10820,7 +11202,7 @@ class PostgresTodosSyncStore {
10820
11202
  }
10821
11203
  async pullSnapshot(options = {}) {
10822
11204
  const params = [this.service];
10823
- const filters = ["service = $1", "deleted_at IS NULL"];
11205
+ const filters = ["service = $1"];
10824
11206
  if (options.since) {
10825
11207
  params.push(options.since);
10826
11208
  filters.push(`updated_at > $${params.length}::timestamptz`);
@@ -10829,7 +11211,7 @@ class PostgresTodosSyncStore {
10829
11211
  params.push(options.objectTypes);
10830
11212
  filters.push(`object_type = ANY($${params.length}::text[])`);
10831
11213
  }
10832
- 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}
10833
11215
  WHERE ${filters.join(" AND ")}
10834
11216
  ORDER BY updated_at ASC, object_type ASC, object_id ASC`, params);
10835
11217
  return rowsToSnapshot(response.rows);
@@ -10853,11 +11235,20 @@ function snapshotEntries(snapshot) {
10853
11235
  return [
10854
11236
  ...snapshot.tasks.map((payload) => entry("tasks", payload, snapshot.exportedAt)),
10855
11237
  ...snapshot.projects.map((payload) => entry("projects", payload, snapshot.exportedAt)),
11238
+ ...(snapshot.projectMachinePaths ?? []).map((payload) => entry("project_machine_paths", payload, snapshot.exportedAt)),
10856
11239
  ...snapshot.plans.map((payload) => entry("plans", payload, snapshot.exportedAt)),
10857
11240
  ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
10858
11241
  ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
10859
11242
  ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
10860
- ...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
+ }))
10861
11252
  ];
10862
11253
  }
10863
11254
  function entry(type, payload, fallbackUpdatedAt) {
@@ -10879,19 +11270,38 @@ function rowsToSnapshot(rows) {
10879
11270
  source: "postgres",
10880
11271
  tasks: [],
10881
11272
  projects: [],
11273
+ projectMachinePaths: [],
10882
11274
  plans: [],
10883
11275
  agents: [],
10884
11276
  taskLists: [],
10885
11277
  templates: [],
10886
- auditHistory: []
11278
+ auditHistory: [],
11279
+ tombstones: []
10887
11280
  };
10888
11281
  for (const row of rows) {
10889
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
+ }
10890
11297
  if (row.object_type === "tasks")
10891
11298
  snapshot.tasks.push(payload);
10892
11299
  else if (row.object_type === "projects")
10893
11300
  snapshot.projects.push(payload);
10894
- 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")
10895
11305
  snapshot.plans.push(payload);
10896
11306
  else if (row.object_type === "agents")
10897
11307
  snapshot.agents.push(payload);
@@ -10912,6 +11322,8 @@ function payloadRecord(value) {
10912
11322
  throw new Error("Postgres sync payload must be a JSON object");
10913
11323
  }
10914
11324
  function stringValue(value) {
11325
+ if (value instanceof Date)
11326
+ return value.toISOString();
10915
11327
  return typeof value === "string" && value ? value : null;
10916
11328
  }
10917
11329
  function numberValue(value) {
@@ -11066,6 +11478,9 @@ class PostgresJsonRecordStore {
11066
11478
  this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
11067
11479
  this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
11068
11480
  }
11481
+ machineId(context) {
11482
+ return context?.requestId ?? this.sourceMachineId ?? null;
11483
+ }
11069
11484
  async ensureSchema() {
11070
11485
  this.schemaReady ??= (async () => {
11071
11486
  for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
@@ -11097,6 +11512,25 @@ class PostgresJsonRecordStore {
11097
11512
  updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
11098
11513
  }));
11099
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
+ }
11100
11534
  async upsert(type, value, context = {}) {
11101
11535
  await this.ensureSchema();
11102
11536
  const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
@@ -11109,7 +11543,8 @@ class PostgresJsonRecordStore {
11109
11543
  updated_at = EXCLUDED.updated_at,
11110
11544
  deleted_at = NULL,
11111
11545
  source_machine_id = EXCLUDED.source_machine_id,
11112
- version = EXCLUDED.version`, [
11546
+ version = EXCLUDED.version
11547
+ WHERE ${this.tableName}.updated_at IS NULL OR ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
11113
11548
  this.service,
11114
11549
  type,
11115
11550
  value.id,
@@ -11126,13 +11561,58 @@ class PostgresJsonRecordStore {
11126
11561
  if (!existing)
11127
11562
  return false;
11128
11563
  const timestamp = new Date().toISOString();
11129
- await this.options.client.query(`UPDATE ${this.tableName}
11130
- SET deleted_at = $4::timestamptz,
11131
- updated_at = $4::timestamptz,
11132
- source_machine_id = $5
11133
- 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
+ ]);
11134
11600
  return true;
11135
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
+ }
11136
11616
  async getCursor(name) {
11137
11617
  await this.ensureSchema();
11138
11618
  const result = await this.options.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
@@ -11201,7 +11681,10 @@ async function createTask2(input, store, context) {
11201
11681
  runner_started_at: null,
11202
11682
  runner_completed_at: null,
11203
11683
  current_step: null,
11204
- total_steps: null
11684
+ total_steps: null,
11685
+ machine_id: store.machineId(context),
11686
+ synced_at: null,
11687
+ archived_at: null
11205
11688
  };
11206
11689
  await store.upsert("tasks", task, context);
11207
11690
  await logTaskChange2(task.id, "created", "status", null, task.status, task.assigned_by ?? task.agent_id, store, context);
@@ -11326,7 +11809,9 @@ async function createProject2(input, store, context) {
11326
11809
  task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
11327
11810
  task_counter: 0,
11328
11811
  created_at: timestamp,
11329
- updated_at: timestamp
11812
+ updated_at: timestamp,
11813
+ machine_id: store.machineId(context),
11814
+ synced_at: null
11330
11815
  };
11331
11816
  return store.upsert("projects", project, context);
11332
11817
  }
@@ -11346,7 +11831,9 @@ async function createPlan2(input, store, context) {
11346
11831
  description: input.description ?? null,
11347
11832
  status: input.status ?? "active",
11348
11833
  created_at: timestamp,
11349
- updated_at: timestamp
11834
+ updated_at: timestamp,
11835
+ machine_id: store.machineId(context),
11836
+ synced_at: null
11350
11837
  }, context);
11351
11838
  }
11352
11839
  async function updatePlan2(id, input, store) {
@@ -11376,7 +11863,9 @@ async function registerAgent2(input, store, context) {
11376
11863
  last_seen_at: timestamp,
11377
11864
  session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
11378
11865
  working_dir: input.working_dir ?? existing?.working_dir ?? null,
11379
- 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
11380
11869
  };
11381
11870
  return store.upsert("agents", agent, context);
11382
11871
  }
@@ -11403,7 +11892,9 @@ async function createTaskList2(input, store, context) {
11403
11892
  description: input.description ?? null,
11404
11893
  metadata: input.metadata ?? {},
11405
11894
  created_at: timestamp,
11406
- updated_at: timestamp
11895
+ updated_at: timestamp,
11896
+ machine_id: store.machineId(context),
11897
+ synced_at: null
11407
11898
  }, context);
11408
11899
  }
11409
11900
  async function updateTaskList2(id, input, store) {
@@ -11429,7 +11920,9 @@ async function createTemplate2(input, store, context) {
11429
11920
  project_id: input.project_id ?? context?.projectId ?? null,
11430
11921
  plan_id: input.plan_id ?? null,
11431
11922
  metadata: input.metadata ?? {},
11432
- created_at: timestamp
11923
+ created_at: timestamp,
11924
+ machine_id: store.machineId(context),
11925
+ synced_at: null
11433
11926
  }, context);
11434
11927
  }
11435
11928
  async function updateTemplate2(id, input, store) {
@@ -11454,7 +11947,8 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
11454
11947
  old_value: oldValue ?? null,
11455
11948
  new_value: newValue ?? null,
11456
11949
  agent_id: agentId ?? context?.agentId ?? null,
11457
- created_at: new Date().toISOString()
11950
+ created_at: new Date().toISOString(),
11951
+ machine_id: store.machineId(context)
11458
11952
  };
11459
11953
  return store.upsert("audit_history", entry2, context);
11460
11954
  }
@@ -11477,18 +11971,21 @@ async function exportSnapshot(store) {
11477
11971
  source: "postgres",
11478
11972
  tasks: await store.list("tasks"),
11479
11973
  projects: await store.list("projects"),
11974
+ projectMachinePaths: await store.list("project_machine_paths"),
11480
11975
  plans: await store.list("plans"),
11481
11976
  agents: await store.list("agents"),
11482
11977
  taskLists: await store.list("task_lists"),
11483
11978
  templates: await store.list("templates"),
11484
- auditHistory: await store.list("audit_history")
11979
+ auditHistory: await store.list("audit_history"),
11980
+ tombstones: await store.listTombstones()
11485
11981
  };
11486
11982
  }
11487
11983
  async function importSnapshot(snapshot, store, context) {
11488
- const result = { inserted: 0, updated: 0, skipped: 0, errors: [] };
11984
+ const result = { inserted: 0, updated: 0, deleted: 0, skipped: 0, errors: [] };
11489
11985
  const entries = [
11490
11986
  ...snapshot.tasks.map((row) => ["tasks", row]),
11491
11987
  ...snapshot.projects.map((row) => ["projects", row]),
11988
+ ...(snapshot.projectMachinePaths ?? []).map((row) => ["project_machine_paths", row]),
11492
11989
  ...snapshot.plans.map((row) => ["plans", row]),
11493
11990
  ...snapshot.agents.map((row) => ["agents", row]),
11494
11991
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
@@ -11507,6 +12004,25 @@ async function importSnapshot(snapshot, store, context) {
11507
12004
  result.errors.push(error instanceof Error ? error.message : String(error));
11508
12005
  }
11509
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
+ }
11510
12026
  return result;
11511
12027
  }
11512
12028
  async function requireRecord(type, id, store) {
@@ -11589,8 +12105,17 @@ function payloadRecord2(value) {
11589
12105
  throw new Error("Postgres storage payload must be a JSON object");
11590
12106
  }
11591
12107
  function stringValue2(value) {
12108
+ if (value instanceof Date)
12109
+ return value.toISOString();
11592
12110
  return typeof value === "string" && value ? value : null;
11593
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
+ }
11594
12119
  function numberValue2(value) {
11595
12120
  return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
11596
12121
  }