@hasna/todos 0.15.35 → 0.15.46

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 (92) hide show
  1. package/dist/cli/cloud-router.d.ts +52 -18
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/agent-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  7. package/dist/cli/commands/task-subtree-transfer-commands.d.ts +3 -0
  8. package/dist/cli/commands/task-subtree-transfer-commands.d.ts.map +1 -0
  9. package/dist/cli/index.js +3430 -827
  10. package/dist/cli/stage-a.d.ts.map +1 -1
  11. package/dist/contracts.js +66 -58
  12. package/dist/db/plan-comments.d.ts +14 -0
  13. package/dist/db/plan-comments.d.ts.map +1 -0
  14. package/dist/db/schema.d.ts.map +1 -1
  15. package/dist/db/task-crud.d.ts.map +1 -1
  16. package/dist/db/task-graph.d.ts.map +1 -1
  17. package/dist/db/task-lifecycle.d.ts.map +1 -1
  18. package/dist/db/task-lists.d.ts.map +1 -1
  19. package/dist/db/task-relations.d.ts.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1897 -334
  23. package/dist/lib/comment-cursor.d.ts +19 -0
  24. package/dist/lib/comment-cursor.d.ts.map +1 -1
  25. package/dist/lib/import-export-bridge.d.ts.map +1 -1
  26. package/dist/lib/instant-compare.d.ts +35 -0
  27. package/dist/lib/instant-compare.d.ts.map +1 -0
  28. package/dist/mcp/index.js +2403 -501
  29. package/dist/mcp.js +8 -4
  30. package/dist/pr-groups/postgres.d.ts.map +1 -1
  31. package/dist/project-registration/postgres.d.ts.map +1 -1
  32. package/dist/project-registration.js +290 -81
  33. package/dist/registry.d.ts +1 -1
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/registry.js +73 -58
  36. package/dist/release-provenance.json +5 -5
  37. package/dist/sdk/index.d.ts +1 -1
  38. package/dist/sdk/index.d.ts.map +1 -1
  39. package/dist/sdk/index.js +35 -0
  40. package/dist/sdk/v1.generated.d.ts +141 -0
  41. package/dist/sdk/v1.generated.d.ts.map +1 -1
  42. package/dist/server/cloud.d.ts +12 -2
  43. package/dist/server/cloud.d.ts.map +1 -1
  44. package/dist/server/index.js +5161 -8425
  45. package/dist/server/openapi.d.ts +2714 -1937
  46. package/dist/server/openapi.d.ts.map +1 -1
  47. package/dist/server/v1.d.ts +2 -1
  48. package/dist/server/v1.d.ts.map +1 -1
  49. package/dist/storage/config.d.ts +12 -30
  50. package/dist/storage/config.d.ts.map +1 -1
  51. package/dist/storage/index.d.ts +2 -2
  52. package/dist/storage/index.d.ts.map +1 -1
  53. package/dist/storage/interfaces.d.ts +13 -2
  54. package/dist/storage/interfaces.d.ts.map +1 -1
  55. package/dist/storage/local-sqlite.d.ts.map +1 -1
  56. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  57. package/dist/storage/timestamp-backfill.d.ts +62 -0
  58. package/dist/storage/timestamp-backfill.d.ts.map +1 -0
  59. package/dist/storage.d.ts +2 -2
  60. package/dist/storage.d.ts.map +1 -1
  61. package/dist/storage.js +268 -74
  62. package/dist/task-manifest/postgres.d.ts.map +1 -1
  63. package/dist/task-manifest.js +15 -6
  64. package/dist/task-subtree-transfer/authority.d.ts +27 -0
  65. package/dist/task-subtree-transfer/authority.d.ts.map +1 -0
  66. package/dist/task-subtree-transfer/backend.d.ts +9 -0
  67. package/dist/task-subtree-transfer/backend.d.ts.map +1 -0
  68. package/dist/task-subtree-transfer/canonical.d.ts +14 -0
  69. package/dist/task-subtree-transfer/canonical.d.ts.map +1 -0
  70. package/dist/task-subtree-transfer/http.d.ts +16 -0
  71. package/dist/task-subtree-transfer/http.d.ts.map +1 -0
  72. package/dist/task-subtree-transfer/index.d.ts +6 -0
  73. package/dist/task-subtree-transfer/index.d.ts.map +1 -0
  74. package/dist/task-subtree-transfer/planner.d.ts +24 -0
  75. package/dist/task-subtree-transfer/planner.d.ts.map +1 -0
  76. package/dist/task-subtree-transfer/postgres.d.ts +19 -0
  77. package/dist/task-subtree-transfer/postgres.d.ts.map +1 -0
  78. package/dist/task-subtree-transfer/schema-sql.d.ts +5 -0
  79. package/dist/task-subtree-transfer/schema-sql.d.ts.map +1 -0
  80. package/dist/task-subtree-transfer/sqlite.d.ts +16 -0
  81. package/dist/task-subtree-transfer/sqlite.d.ts.map +1 -0
  82. package/dist/task-subtree-transfer/types.d.ts +148 -0
  83. package/dist/task-subtree-transfer/types.d.ts.map +1 -0
  84. package/dist/task-subtree-transfer.d.ts +2 -0
  85. package/dist/task-subtree-transfer.d.ts.map +1 -0
  86. package/dist/task-subtree-transfer.js +1878 -0
  87. package/dist/testing.d.ts +41 -7
  88. package/dist/testing.d.ts.map +1 -1
  89. package/dist/testing.js +44 -1
  90. package/dist/types/index.d.ts +24 -0
  91. package/dist/types/index.d.ts.map +1 -1
  92. package/package.json +8 -4
package/dist/storage.js CHANGED
@@ -42,20 +42,16 @@ var __require = import.meta.require;
42
42
  // src/storage/config.ts
43
43
  var exports_config = {};
44
44
  __export(exports_config, {
45
- parseStorageMode: () => parseStorageMode,
46
45
  parseStorageBackend: () => parseStorageBackend,
47
46
  loadTodosStorageConfig: () => loadTodosStorageConfig,
48
47
  loadStorageConfig: () => loadStorageConfig,
49
48
  isTodosShadowEnabled: () => isTodosShadowEnabled,
50
- isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
51
49
  isTodosPostgresBackend: () => isTodosPostgresBackend,
52
50
  getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
53
- getTodosStorageMode: () => getTodosStorageMode,
54
51
  getTodosStorageEnvName: () => getTodosStorageEnvName,
55
52
  getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
56
53
  getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
57
54
  getTodosStorageBackend: () => getTodosStorageBackend,
58
- getStorageMode: () => getStorageMode,
59
55
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
60
56
  getStorageDatabaseEnv: () => getStorageDatabaseEnv,
61
57
  getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
@@ -65,7 +61,6 @@ __export(exports_config, {
65
61
  TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
66
62
  TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
67
63
  STORAGE_TABLES: () => STORAGE_TABLES,
68
- REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
69
64
  CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
70
65
  CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
71
66
  CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
@@ -120,9 +115,6 @@ function loadStorageConfig(env = process.env) {
120
115
  function isTodosPostgresBackend(config) {
121
116
  return config.mode === "postgres";
122
117
  }
123
- function isTodosRemoteStorageEnabled(config) {
124
- return isTodosPostgresBackend(config);
125
- }
126
118
  function assertTodosRemoteStorageConfig(config) {
127
119
  if (!isTodosPostgresBackend(config))
128
120
  return;
@@ -138,32 +130,15 @@ function parseStorageBackend(value) {
138
130
  return "sqlite";
139
131
  if (normalized === "postgres" || normalized === "postgresql")
140
132
  return "postgres";
141
- if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
142
- throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} selects the backend. Deployment modes no longer exist: ` + `delete the storage-mode variable and set ${TODOS_STORAGE_ENV.databaseUrl} to select the ` + `postgresql backend, or leave it unset for sqlite.`);
143
- }
144
133
  throw new Error(`Storage backend must be sqlite or postgres`);
145
134
  }
146
- function parseStorageMode(value) {
147
- return parseStorageBackend(value);
148
- }
149
135
  function getTodosStorageBackend(env = process.env) {
150
- for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
151
- if (Object.hasOwn(env, key) && env[key] !== undefined) {
152
- throw new Error(`${key} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `Set ${TODOS_STORAGE_ENV.databaseUrl} to select the postgresql backend, ` + `or leave it unset for sqlite.`);
153
- }
154
- }
155
136
  if (!getTodosStorageDatabaseUrl(env))
156
137
  return "sqlite";
157
138
  if (isTodosShadowEnabled(env))
158
139
  return "sqlite";
159
140
  return "postgres";
160
141
  }
161
- function getTodosStorageMode(env = process.env) {
162
- return getTodosStorageBackend(env);
163
- }
164
- function getStorageMode(env = process.env) {
165
- return getTodosStorageBackend(env);
166
- }
167
142
  function isTodosShadowEnabled(env = process.env) {
168
143
  return parseBoolean(readStorageEnv(env, "shadow").value, false);
169
144
  }
@@ -230,7 +205,7 @@ function parsePositiveInteger(value, fallback) {
230
205
  }
231
206
  return parsed;
232
207
  }
233
- var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, REMOVED_STORAGE_MODE_ENV_KEYS, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
208
+ var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
234
209
  var init_config = __esm(() => {
235
210
  TODOS_STORAGE_TABLES = [
236
211
  "todos_sync_records",
@@ -269,12 +244,6 @@ var init_config = __esm(() => {
269
244
  syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
270
245
  syncDryRun: "TODOS_SYNC_DRY_RUN"
271
246
  };
272
- REMOVED_STORAGE_MODE_ENV_KEYS = [
273
- "HASNA_TODOS_STORAGE_MODE",
274
- "HASNA_TODOS_MODE",
275
- "TODOS_STORAGE_MODE",
276
- "TODOS_MODE"
277
- ];
278
247
  });
279
248
 
280
249
  // src/types/index.ts
@@ -3127,6 +3096,20 @@ function ensureSchema(db) {
3127
3096
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
3128
3097
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
3129
3098
  ensureColumn("task_comments", "progress_pct", "INTEGER");
3099
+ ensureTable("plan_comments", `
3100
+ CREATE TABLE plan_comments (
3101
+ id TEXT PRIMARY KEY,
3102
+ plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
3103
+ agent_id TEXT,
3104
+ session_id TEXT,
3105
+ content TEXT NOT NULL,
3106
+ type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note')),
3107
+ progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100)),
3108
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3109
+ machine_id TEXT,
3110
+ synced_at TEXT
3111
+ )`);
3112
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_plan_comments_plan ON plan_comments(plan_id)");
3130
3113
  ensureColumn("projects", "machine_id", "TEXT");
3131
3114
  ensureColumn("projects", "synced_at", "TEXT");
3132
3115
  ensureColumn("tasks", "machine_id", "TEXT");
@@ -4108,6 +4091,7 @@ var init_machines = __esm(() => {
4108
4091
  "task_lists",
4109
4092
  "plans",
4110
4093
  "task_comments",
4094
+ "plan_comments",
4111
4095
  "sessions",
4112
4096
  "task_history",
4113
4097
  "webhooks",
@@ -6857,22 +6841,31 @@ function updateTaskList(id, input, db) {
6857
6841
  throw new TaskListNotFoundError(id);
6858
6842
  const sets = ["updated_at = ?"];
6859
6843
  const params = [now()];
6860
- if (input.slug !== undefined) {
6861
- const slug = slugify(input.slug);
6862
- if (!slug)
6863
- throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
6864
- const duplicate = existing.project_id ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(existing.project_id, slug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(slug, id);
6844
+ const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? existing.project_id : input.project_id;
6845
+ const scopeChanged = projectId !== existing.project_id;
6846
+ const effectiveSlug = input.slug !== undefined ? slugify(input.slug) : existing.slug;
6847
+ if (input.slug !== undefined && !effectiveSlug) {
6848
+ throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
6849
+ }
6850
+ if (scopeChanged || input.slug !== undefined && effectiveSlug !== existing.slug) {
6851
+ const duplicate = projectId ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(projectId, effectiveSlug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(effectiveSlug, id);
6865
6852
  if (duplicate) {
6866
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
6853
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
6867
6854
  }
6868
- if (slug !== existing.slug) {
6855
+ if (scopeChanged || effectiveSlug !== existing.slug) {
6869
6856
  releaseCanonicalSlugClaims("task_list", id, d);
6870
- if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(existing.project_id), slug, id, d)) {
6871
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
6857
+ if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(projectId), effectiveSlug, id, d)) {
6858
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
6872
6859
  }
6873
6860
  }
6861
+ }
6862
+ if (input.slug !== undefined) {
6874
6863
  sets.push("slug = ?");
6875
- params.push(slug);
6864
+ params.push(effectiveSlug);
6865
+ }
6866
+ if (projectId !== existing.project_id) {
6867
+ sets.push("project_id = ?");
6868
+ params.push(projectId);
6876
6869
  }
6877
6870
  if (input.name !== undefined) {
6878
6871
  sets.push("name = ?");
@@ -8658,7 +8651,7 @@ function getTaskGraph(taskId, direction = "both", db) {
8658
8651
  const deps = getTaskDependencies(t.id, d);
8659
8652
  const hasUnfinishedDeps = deps.some((dep) => {
8660
8653
  const depTask = getTask(dep.depends_on, d);
8661
- return depTask && depTask.status !== "completed";
8654
+ return depTask && isBlockingDependencyStatus(depTask.status);
8662
8655
  });
8663
8656
  return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
8664
8657
  }
@@ -8918,7 +8911,7 @@ function getBlockingDeps(id, db) {
8918
8911
  const blocking = [];
8919
8912
  for (const dep of deps) {
8920
8913
  const task = getTask(dep.depends_on, d);
8921
- if (task && task.status !== "completed")
8914
+ if (task && isBlockingDependencyStatus(task.status))
8922
8915
  blocking.push(task);
8923
8916
  }
8924
8917
  return blocking;
@@ -9242,7 +9235,7 @@ function getNextTask(agentId, filters, db) {
9242
9235
  conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
9243
9236
  params.push(...filters.tags);
9244
9237
  }
9245
- conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')");
9238
+ conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status NOT IN ('completed', 'cancelled'))");
9246
9239
  const where = conditions.join(" AND ");
9247
9240
  let recentProjectIds = [];
9248
9241
  const assignedAliasParams = [];
@@ -9286,7 +9279,7 @@ function getActiveWork(filters, db) {
9286
9279
  }
9287
9280
  function getTasksChangedSince(since, filters, db) {
9288
9281
  const d = db || getDatabase();
9289
- const conditions = ["updated_at > ?"];
9282
+ const conditions = ["(julianday(updated_at) IS NULL OR julianday(updated_at) > julianday(?))"];
9290
9283
  const params = [since];
9291
9284
  if (filters?.project_id) {
9292
9285
  conditions.push("project_id = ?");
@@ -9321,8 +9314,8 @@ function failTask(id, agentId, reason, options, db) {
9321
9314
  const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
9322
9315
  const timestamp2 = now();
9323
9316
  const failTx = d.transaction(() => {
9324
- const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
9325
- WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
9317
+ const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, completed_at = COALESCE(completed_at, ?), metadata = ?, version = version + 1, updated_at = ?
9318
+ WHERE id = ? AND version = ?`, [safeReason, timestamp2, JSON.stringify(safeMeta), timestamp2, id, task.version]);
9326
9319
  if (res.changes === 0) {
9327
9320
  const current = getTask(id, d);
9328
9321
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -9334,6 +9327,7 @@ function failTask(id, agentId, reason, options, db) {
9334
9327
  status: "failed",
9335
9328
  locked_by: null,
9336
9329
  locked_at: null,
9330
+ completed_at: task.completed_at ?? timestamp2,
9337
9331
  reason: safeReason,
9338
9332
  metadata: safeMeta,
9339
9333
  version: task.version + 1,
@@ -9982,13 +9976,20 @@ function updateTaskStored(id, input, db) {
9982
9976
  if (isTerminalStatus(input.status)) {
9983
9977
  sets.push("locked_by = NULL");
9984
9978
  sets.push("locked_at = NULL");
9985
- }
9986
- if (input.status === "completed") {
9987
- sets.push("completed_at = ?");
9988
- params.push(completionTimestamp);
9979
+ if (input.status === "completed") {
9980
+ sets.push("completed_at = ?");
9981
+ params.push(completionTimestamp);
9982
+ } else {
9983
+ sets.push("completed_at = COALESCE(completed_at, ?)");
9984
+ params.push(completionTimestamp);
9985
+ }
9989
9986
  } else if (task.status === "completed" && input.completed_at === undefined) {
9990
9987
  sets.push("completed_at = NULL");
9991
9988
  }
9989
+ if (input.status === "in_progress") {
9990
+ sets.push("started_at = COALESCE(started_at, ?)");
9991
+ params.push(timestamp2);
9992
+ }
9992
9993
  }
9993
9994
  if (input.priority !== undefined) {
9994
9995
  sets.push("priority = ?");
@@ -10126,7 +10127,6 @@ function updateTaskStored(id, input, db) {
10126
10127
  if (input.approved_by !== undefined)
10127
10128
  logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
10128
10129
  const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
10129
- const completedNow = input.status === "completed";
10130
10130
  const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
10131
10131
  const updatedTask = {
10132
10132
  ...task,
@@ -10137,7 +10137,8 @@ function updateTaskStored(id, input, db) {
10137
10137
  updated_at: timestamp2,
10138
10138
  locked_by: terminalNow ? null : task.locked_by,
10139
10139
  locked_at: terminalNow ? null : task.locked_at,
10140
- completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
10140
+ completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : terminalNow ? task.completed_at ?? completionTimestamp : reopened ? null : task.completed_at,
10141
+ started_at: input.status === "in_progress" ? task.started_at ?? timestamp2 : task.started_at,
10141
10142
  sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
10142
10143
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
10143
10144
  confidence: input.confidence !== undefined ? input.confidence : task.confidence,
@@ -10198,6 +10199,9 @@ function deleteTask(id, db) {
10198
10199
  version: row.version
10199
10200
  }, d);
10200
10201
  const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
10202
+ if (result.changes > 0) {
10203
+ d.run("DELETE FROM task_dependencies WHERE task_id = ? OR depends_on = ?", [id, id]);
10204
+ }
10201
10205
  return result.changes > 0;
10202
10206
  }
10203
10207
  var init_task_crud = __esm(() => {
@@ -12967,6 +12971,40 @@ init_tasks();
12967
12971
  init_projects();
12968
12972
  init_plans();
12969
12973
 
12974
+ // src/db/plan-comments.ts
12975
+ init_types();
12976
+ init_database();
12977
+ init_plans();
12978
+ init_prewrite_secrets();
12979
+ function addPlanComment(input, db) {
12980
+ const d = db || getDatabase();
12981
+ if (!getPlan(input.plan_id, d)) {
12982
+ throw new PlanNotFoundError(input.plan_id);
12983
+ }
12984
+ const id = uuid();
12985
+ const timestamp2 = now();
12986
+ d.run(`INSERT INTO plan_comments (id, plan_id, agent_id, session_id, content, type, progress_pct, created_at)
12987
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
12988
+ id,
12989
+ input.plan_id,
12990
+ input.agent_id || null,
12991
+ input.session_id || null,
12992
+ sanitizePreWriteText(input.content, "comment.content"),
12993
+ input.type || "comment",
12994
+ input.progress_pct ?? null,
12995
+ timestamp2
12996
+ ]);
12997
+ return getPlanComment(id, d);
12998
+ }
12999
+ function getPlanComment(id, db) {
13000
+ const d = db || getDatabase();
13001
+ return d.query("SELECT * FROM plan_comments WHERE id = ?").get(id);
13002
+ }
13003
+ function listPlanComments(planId, db) {
13004
+ const d = db || getDatabase();
13005
+ return d.query("SELECT * FROM plan_comments WHERE plan_id = ? ORDER BY created_at, rowid").all(planId);
13006
+ }
13007
+
12970
13008
  // src/db/agents.ts
12971
13009
  init_types();
12972
13010
  init_database();
@@ -13624,6 +13662,20 @@ init_task_lists();
13624
13662
  init_templates();
13625
13663
  init_audit();
13626
13664
  init_comments();
13665
+
13666
+ // src/lib/comment-cursor.ts
13667
+ function isStrictlyOlder(comment, before) {
13668
+ return comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id;
13669
+ }
13670
+ function compareCommentKeyset(left, right) {
13671
+ if (isStrictlyOlder(left, right))
13672
+ return -1;
13673
+ if (isStrictlyOlder(right, left))
13674
+ return 1;
13675
+ return 0;
13676
+ }
13677
+
13678
+ // src/storage/local-sqlite.ts
13627
13679
  init_database();
13628
13680
 
13629
13681
  // src/db/integrity.ts
@@ -14644,7 +14696,7 @@ function matchesExtraFilters(task, filter) {
14644
14696
  }
14645
14697
  if (filter.tags?.length) {
14646
14698
  const taskTags = new Set(task.tags ?? []);
14647
- if (!filter.tags.every((tag) => taskTags.has(tag)))
14699
+ if (!filter.tags.some((tag) => taskTags.has(tag)))
14648
14700
  return false;
14649
14701
  }
14650
14702
  if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
@@ -14720,7 +14772,22 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
14720
14772
  list: (projectId) => listPlans(projectId, database()),
14721
14773
  update: (id, input) => updatePlan(id, input, database()),
14722
14774
  completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
14723
- delete: (id) => deletePlan(id, database())
14775
+ delete: (id) => deletePlan(id, database()),
14776
+ addComment: (input) => addPlanComment(input, database()),
14777
+ getComments: (planId) => listPlanComments(planId, database()),
14778
+ getCommentsPage: (planId, options2) => {
14779
+ if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
14780
+ throw new Error("Plan comment limit must be an integer between 1 and 1001");
14781
+ }
14782
+ let comments = listPlanComments(planId, database());
14783
+ comments = comments.sort(compareCommentKeyset);
14784
+ if (options2?.before) {
14785
+ const before = options2.before;
14786
+ comments = comments.filter((comment) => isStrictlyOlder(comment, before));
14787
+ }
14788
+ const limit = options2?.limit ?? 100;
14789
+ return comments.slice(-limit);
14790
+ }
14724
14791
  },
14725
14792
  planProjectLinks: {
14726
14793
  apply: (input) => applyPlanProjectLinkSqlite(input, database()),
@@ -14761,10 +14828,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
14761
14828
  throw new Error("Comment limit must be an integer between 1 and 1001");
14762
14829
  }
14763
14830
  let comments = listComments(taskId, database());
14764
- comments = comments.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
14831
+ comments = comments.sort(compareCommentKeyset);
14765
14832
  if (options2?.before) {
14766
14833
  const before = options2.before;
14767
- comments = comments.filter((comment) => comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id);
14834
+ comments = comments.filter((comment) => isStrictlyOlder(comment, before));
14768
14835
  }
14769
14836
  if (options2?.limit !== undefined)
14770
14837
  comments = comments.slice(-options2.limit);
@@ -14793,6 +14860,59 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
14793
14860
  init_types();
14794
14861
  import { randomUUID as randomUUID3 } from "crypto";
14795
14862
  init_creator_identity();
14863
+
14864
+ // src/lib/instant-compare.ts
14865
+ var SQLITE_STAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:Z|([+-])(\d{2}):(\d{2}))?)?$/;
14866
+ var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
14867
+ function isLeapYear(year) {
14868
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
14869
+ }
14870
+ function sqliteJulianDay(value) {
14871
+ const m = SQLITE_STAMP.exec(value);
14872
+ if (!m)
14873
+ return null;
14874
+ const year = Number(m[1]);
14875
+ const month = Number(m[2]);
14876
+ const day = Number(m[3]);
14877
+ const hour = m[4] === undefined ? 0 : Number(m[4]);
14878
+ const minute = m[5] === undefined ? 0 : Number(m[5]);
14879
+ const second = m[6] === undefined ? 0 : Number(m[6]);
14880
+ const frac = m[7];
14881
+ const sign = m[8];
14882
+ const offsetHour = m[9];
14883
+ const offsetMinute = m[10];
14884
+ if (month < 1 || month > 12)
14885
+ return null;
14886
+ if (hour > 23 || minute > 59 || second > 59)
14887
+ return null;
14888
+ const maxDay = (DAYS_IN_MONTH[month - 1] ?? 0) + (month === 2 && isLeapYear(year) ? 1 : 0);
14889
+ if (day < 1 || day > maxDay)
14890
+ return null;
14891
+ let offsetMinutes = 0;
14892
+ if (sign !== undefined) {
14893
+ const oh = Number(offsetHour ?? "0");
14894
+ const om = Number(offsetMinute ?? "0");
14895
+ if (oh > 23 || om > 59)
14896
+ return null;
14897
+ offsetMinutes = oh * 60 + om;
14898
+ if (sign === "-")
14899
+ offsetMinutes = -offsetMinutes;
14900
+ }
14901
+ const micros = frac === undefined ? 0 : Number((frac + "000000").slice(0, 6));
14902
+ const ms = Date.UTC(year, month - 1, day, hour, minute, second);
14903
+ return (ms + micros / 1000) / 86400000 + 2440587.5 - offsetMinutes / 1440;
14904
+ }
14905
+ function changedSinceStampNewer(stamp, since) {
14906
+ const stampJd = sqliteJulianDay(stamp);
14907
+ if (stampJd === null)
14908
+ return true;
14909
+ const sinceJd = sqliteJulianDay(since);
14910
+ if (sinceJd === null)
14911
+ return false;
14912
+ return stampJd > sinceJd;
14913
+ }
14914
+
14915
+ // src/storage/postgres-adapter.ts
14796
14916
  init_stale_lock_handoff();
14797
14917
 
14798
14918
  // src/storage/postgres-sync.ts
@@ -15294,7 +15414,26 @@ function createPostgresTodosStorageAdapter(options) {
15294
15414
  list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
15295
15415
  update: (id, input) => updatePlan2(id, input, store),
15296
15416
  completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
15297
- delete: (id, context) => store.deletePlan(id, context)
15417
+ delete: (id, context) => store.deletePlan(id, context),
15418
+ addComment: (input, context) => addPlanComment2(input, store, context),
15419
+ getComments: async (planId) => {
15420
+ const pages = [];
15421
+ let before;
15422
+ while (true) {
15423
+ const page = await store.listPlanComments(planId, { limit: 1000, ...before ? { before } : {} });
15424
+ if (page.length === 0)
15425
+ break;
15426
+ pages.unshift(page);
15427
+ if (page.length < 1000)
15428
+ break;
15429
+ const oldest = page[0];
15430
+ before = { created_at: oldest.created_at, id: oldest.id };
15431
+ }
15432
+ return pages.flat().map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
15433
+ },
15434
+ getCommentsPage: async (planId, options2) => {
15435
+ return (await store.listPlanComments(planId, options2)).map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
15436
+ }
15298
15437
  },
15299
15438
  planProjectLinks: {
15300
15439
  apply: (input, context) => store.applyPlanProjectLink(input, context),
@@ -15388,11 +15527,16 @@ class PostgresJsonRecordStore {
15388
15527
  return context?.requestId ?? this.sourceMachineId ?? null;
15389
15528
  }
15390
15529
  async ensureSchema() {
15391
- this.schemaReady ??= (async () => {
15392
- for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
15393
- await this.options.client.query(sql);
15394
- }
15395
- })();
15530
+ if (!this.schemaReady) {
15531
+ this.schemaReady = (async () => {
15532
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
15533
+ await this.options.client.query(sql);
15534
+ }
15535
+ })().catch((error) => {
15536
+ this.schemaReady = null;
15537
+ throw error;
15538
+ });
15539
+ }
15396
15540
  await this.schemaReady;
15397
15541
  }
15398
15542
  async get(type, id) {
@@ -15512,6 +15656,27 @@ class PostgresJsonRecordStore {
15512
15656
  LIMIT $${params.length}`, params);
15513
15657
  return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
15514
15658
  }
15659
+ async listPlanComments(planId, options = {}) {
15660
+ await this.ensureSchema();
15661
+ const limit = options.limit ?? 100;
15662
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
15663
+ throw new Error("Postgres plan comment limit must be an integer between 1 and 1001");
15664
+ }
15665
+ const params = [this.service, planId];
15666
+ let cursorPredicate = "";
15667
+ if (options.before) {
15668
+ params.push(options.before.created_at, options.before.id);
15669
+ cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
15670
+ }
15671
+ params.push(limit);
15672
+ const result = await this.options.client.query(`/* todos:list-plan-comments */ SELECT payload FROM ${this.tableName}
15673
+ WHERE service = $1 AND object_type = 'plan_comments' AND deleted_at IS NULL
15674
+ AND payload->>'plan_id' = $2
15675
+ ${cursorPredicate}
15676
+ ORDER BY payload->>'created_at' DESC, object_id DESC
15677
+ LIMIT $${params.length}`, params);
15678
+ return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
15679
+ }
15515
15680
  async listRecords(type) {
15516
15681
  await this.ensureSchema();
15517
15682
  const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
@@ -16673,6 +16838,15 @@ class PostgresJsonRecordStore {
16673
16838
  WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
16674
16839
  AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
16675
16840
  RETURNING 1
16841
+ ), deleted_plan_comments AS (
16842
+ UPDATE ${this.tableName} r SET
16843
+ deleted_at = $3::timestamptz,
16844
+ updated_at = $3::timestamptz,
16845
+ source_machine_id = COALESCE($4, r.source_machine_id),
16846
+ version = COALESCE(r.version, 0) + 1
16847
+ WHERE r.service = $1 AND r.object_type = 'plan_comments' AND r.deleted_at IS NULL
16848
+ AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
16849
+ RETURNING 1
16676
16850
  ), deleted_plan AS (
16677
16851
  UPDATE ${this.tableName} r SET
16678
16852
  deleted_at = $3::timestamptz,
@@ -16848,7 +17022,8 @@ async function updateTask2(id, input, store, context) {
16848
17022
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
16849
17023
  parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
16850
17024
  created_by: existing.created_by,
16851
- completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
17025
+ completed_at: input.status === "completed" ? input.completed_at ?? new Date().toISOString() : input.completed_at !== undefined ? input.completed_at : input.status !== undefined && isTerminalStatus(input.status) ? existing.completed_at ?? new Date().toISOString() : reopened ? null : existing.completed_at,
17026
+ started_at: input.status === "in_progress" ? existing.started_at ?? new Date().toISOString() : existing.started_at
16852
17027
  };
16853
17028
  const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
16854
17029
  operation: "update",
@@ -16893,7 +17068,8 @@ async function failTask2(id, agentId, reason, options, store) {
16893
17068
  status: "failed",
16894
17069
  assigned_to: task.assigned_to ?? agentId ?? null,
16895
17070
  reason: reason ?? task.reason,
16896
- retry_after: options?.retry_after ?? task.retry_after
17071
+ retry_after: options?.retry_after ?? task.retry_after,
17072
+ completed_at: task.completed_at ?? new Date().toISOString()
16897
17073
  }, store);
16898
17074
  if (!options?.retry)
16899
17075
  return { task: failed };
@@ -17125,7 +17301,7 @@ async function getActiveWork2(filters, store) {
17125
17301
  }));
17126
17302
  }
17127
17303
  async function getChangedSince(since, filters, store) {
17128
- return (await listTasks2(filters ?? {}, store)).filter((task) => task.updated_at > since);
17304
+ return (await listTasks2(filters ?? {}, store)).filter((task) => changedSinceStampNewer(task.updated_at ?? "", since));
17129
17305
  }
17130
17306
  async function createProject2(input, store, context) {
17131
17307
  const timestamp2 = new Date().toISOString();
@@ -17341,16 +17517,22 @@ async function createTaskList2(input, store, context) {
17341
17517
  async function updateTaskList2(id, input, store) {
17342
17518
  const list = await requireRecord("task_lists", id, store);
17343
17519
  const patch = definedPatch(input);
17520
+ const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? list.project_id : input.project_id;
17344
17521
  if (input.slug !== undefined) {
17345
17522
  const slug = slugifyRaw(input.slug);
17346
17523
  if (!slug)
17347
17524
  throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
17348
- const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === list.project_id && candidate.slug === slug);
17525
+ patch.slug = slug;
17526
+ }
17527
+ const effectiveSlug = patch.slug ?? list.slug;
17528
+ if (projectId !== list.project_id || effectiveSlug !== list.slug) {
17529
+ const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === projectId && candidate.slug === effectiveSlug);
17349
17530
  if (duplicate) {
17350
- throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
17531
+ throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
17351
17532
  }
17352
- patch.slug = slug;
17353
17533
  }
17534
+ if (projectId !== list.project_id)
17535
+ patch.project_id = projectId;
17354
17536
  return store.upsert("task_lists", {
17355
17537
  ...list,
17356
17538
  ...patch,
@@ -17443,6 +17625,22 @@ async function addComment2(input, store, context) {
17443
17625
  function redactComment(comment) {
17444
17626
  return { ...comment, content: redactEvidenceText(comment.content) };
17445
17627
  }
17628
+ async function addPlanComment2(input, store, context) {
17629
+ const comment = {
17630
+ id: randomUUID3(),
17631
+ plan_id: input.plan_id,
17632
+ agent_id: input.agent_id ?? context?.agentId ?? null,
17633
+ session_id: input.session_id ?? context?.sessionId ?? null,
17634
+ content: redactEvidenceText(input.content),
17635
+ type: input.type ?? "comment",
17636
+ progress_pct: input.progress_pct ?? null,
17637
+ created_at: new Date().toISOString()
17638
+ };
17639
+ return store.upsert("plan_comments", comment, context);
17640
+ }
17641
+ function redactPlanComment(comment) {
17642
+ return { ...comment, content: redactEvidenceText(comment.content) };
17643
+ }
17446
17644
  async function exportSnapshot(store) {
17447
17645
  return {
17448
17646
  exportedAt: new Date().toISOString(),
@@ -19051,24 +19249,20 @@ export {
19051
19249
  postgresTodosScopedSlugIndexStatusSql,
19052
19250
  postgresTodosCommentCursorIndexSql,
19053
19251
  planRunArtifactsS3Sync,
19054
- parseStorageMode,
19055
19252
  parseStorageBackend,
19056
19253
  maybeInstallShadowCapture2 as maybeInstallShadowCapture,
19057
19254
  loadTodosStorageConfig,
19058
19255
  loadStorageConfig,
19059
19256
  isTodosShadowEnabled,
19060
- isTodosRemoteStorageEnabled,
19061
19257
  isTodosPostgresBackend,
19062
19258
  isCommentRedactionBackfillComplete,
19063
19259
  installShadowOutboxSchema,
19064
19260
  importSqliteTodosStorageSnapshot,
19065
19261
  getTodosStorageShadowEnvName,
19066
- getTodosStorageMode,
19067
19262
  getTodosStorageEnvName,
19068
19263
  getTodosStorageDatabaseUrl,
19069
19264
  getTodosStorageDatabaseEnv,
19070
19265
  getTodosStorageBackend,
19071
- getStorageMode,
19072
19266
  getStorageDatabaseUrl,
19073
19267
  getStorageDatabaseEnv,
19074
19268
  getRuntimeShadowOutbox,
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/task-manifest/postgres.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,EAE/B,KAAK,wBAAwB,EAC9B,MAAM,cAAc,CAAC;AAItB,OAAO,EAEL,KAAK,yCAAyC,EAE9C,KAAK,4BAA4B,EACjC,KAAK,kCAAkC,EACvC,KAAK,mCAAmC,EAExC,KAAK,+BAA+B,EACpC,KAAK,wBAAwB,EAE9B,MAAM,YAAY,CAAC;AAgKpB,qBAAa,gCAAiC,YAAW,wBAAwB;IAQ7E,OAAO,CAAC,QAAQ,CAAC,MAAM;IAPzB,QAAQ,CAAC,IAAI,EAAG,YAAY,CAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,WAAW,CAA8B;gBAG9B,MAAM,EAAE,+BAA+B,EACxD,OAAO,GAAE,yCAA8C;YAO3C,YAAY;YAQZ,UAAU;YAcV,eAAe;IAyCvB,KAAK,CAAC,KAAK,EAAE,sBAAsB,EAAE,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAwJ/G,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAenE,qBAAqB,CAAC,MAAM,EAAE,MAAM;IA8BpC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0DzE,UAAU,CACd,KAAK,EAAE,kCAAkC,EACzC,OAAO,EAAE,wBAAwB,EACjC,qBAAqB,EAAE,MAAM,EAC7B,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,mCAAmC,CAAC;YAyPjC,QAAQ;CAcvB"}
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/task-manifest/postgres.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,EAE/B,KAAK,wBAAwB,EAC9B,MAAM,cAAc,CAAC;AAItB,OAAO,EAEL,KAAK,yCAAyC,EAE9C,KAAK,4BAA4B,EACjC,KAAK,kCAAkC,EACvC,KAAK,mCAAmC,EAExC,KAAK,+BAA+B,EACpC,KAAK,wBAAwB,EAE9B,MAAM,YAAY,CAAC;AAgKpB,qBAAa,gCAAiC,YAAW,wBAAwB;IAQ7E,OAAO,CAAC,QAAQ,CAAC,MAAM;IAPzB,QAAQ,CAAC,IAAI,EAAG,YAAY,CAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,WAAW,CAA8B;gBAG9B,MAAM,EAAE,+BAA+B,EACxD,OAAO,GAAE,yCAA8C;YAO3C,YAAY;YAqBZ,UAAU;YAcV,eAAe;IAyCvB,KAAK,CAAC,KAAK,EAAE,sBAAsB,EAAE,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAwJ/G,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAenE,qBAAqB,CAAC,MAAM,EAAE,MAAM;IA8BpC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0DzE,UAAU,CACd,KAAK,EAAE,kCAAkC,EACzC,OAAO,EAAE,wBAAwB,EACjC,qBAAqB,EAAE,MAAM,EAC7B,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,mCAAmC,CAAC;YAyPjC,QAAQ;CAcvB"}
@@ -6613,12 +6613,21 @@ class PostgresTodosTaskManifestBackend {
6613
6613
  this.tenantId = options.tenantId ?? "default";
6614
6614
  }
6615
6615
  async ensureSchema() {
6616
- this.schemaReady ??= (async () => {
6617
- for (const sql of postgresTodosSyncSchemaSql(this.tableName))
6618
- await this.client.query(sql);
6619
- for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
6620
- await this.client.query(sql);
6621
- })();
6616
+ if (this.schemaReady === null) {
6617
+ const attempt = (async () => {
6618
+ for (const sql of postgresTodosSyncSchemaSql(this.tableName))
6619
+ await this.client.query(sql);
6620
+ for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
6621
+ await this.client.query(sql);
6622
+ })();
6623
+ this.schemaReady = attempt;
6624
+ try {
6625
+ await attempt;
6626
+ } catch (error) {
6627
+ this.schemaReady = null;
6628
+ throw error;
6629
+ }
6630
+ }
6622
6631
  await this.schemaReady;
6623
6632
  }
6624
6633
  async insertSync(tx, objectType2, objectId, payload, now) {