@hasna/todos 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +59 -22
  3. package/dist/cli/cloud-router.d.ts +63 -21
  4. package/dist/cli/cloud-router.d.ts.map +1 -1
  5. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/template-commands.d.ts.map +1 -1
  7. package/dist/cli/index.js +2591 -503
  8. package/dist/cli/stage-a.d.ts +2 -12
  9. package/dist/cli/stage-a.d.ts.map +1 -1
  10. package/dist/cli/template-remote.d.ts +90 -0
  11. package/dist/cli/template-remote.d.ts.map +1 -0
  12. package/dist/cli-mcp-parity.d.ts.map +1 -1
  13. package/dist/contracts.js +77 -30
  14. package/dist/db/agents.d.ts +8 -0
  15. package/dist/db/agents.d.ts.map +1 -1
  16. package/dist/db/database.d.ts +6 -0
  17. package/dist/db/database.d.ts.map +1 -1
  18. package/dist/db/identity-mapping.d.ts +25 -0
  19. package/dist/db/identity-mapping.d.ts.map +1 -1
  20. package/dist/db/task-crud.d.ts.map +1 -1
  21. package/dist/index.js +496 -58
  22. package/dist/lib/feature-manifest.d.ts.map +1 -1
  23. package/dist/lib/local-opt-in.d.ts +14 -0
  24. package/dist/lib/local-opt-in.d.ts.map +1 -1
  25. package/dist/lib/paths.d.ts +11 -1
  26. package/dist/lib/paths.d.ts.map +1 -1
  27. package/dist/mcp/index.d.ts +52 -0
  28. package/dist/mcp/index.d.ts.map +1 -1
  29. package/dist/mcp/index.js +15162 -30695
  30. package/dist/mcp/tools/agents.d.ts.map +1 -1
  31. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
  32. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  33. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  34. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  35. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  36. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  37. package/dist/mcp/tools/templates.d.ts.map +1 -1
  38. package/dist/mcp.js +3 -3
  39. package/dist/project-registration.js +466 -35
  40. package/dist/registry.js +77 -30
  41. package/dist/release-provenance.json +5 -5
  42. package/dist/sdk/client.d.ts +3 -1
  43. package/dist/sdk/client.d.ts.map +1 -1
  44. package/dist/sdk/index.js +48 -12
  45. package/dist/sdk/resolve.d.ts +23 -13
  46. package/dist/sdk/resolve.d.ts.map +1 -1
  47. package/dist/sdk/types.d.ts +17 -2
  48. package/dist/sdk/types.d.ts.map +1 -1
  49. package/dist/sdk/v1.generated.d.ts +115 -0
  50. package/dist/sdk/v1.generated.d.ts.map +1 -1
  51. package/dist/server/index.js +3972 -916
  52. package/dist/server/openapi.d.ts +837 -50
  53. package/dist/server/openapi.d.ts.map +1 -1
  54. package/dist/server/rate-limit-config.d.ts +6 -0
  55. package/dist/server/rate-limit-config.d.ts.map +1 -0
  56. package/dist/server/serve.d.ts.map +1 -1
  57. package/dist/server/v1.d.ts.map +1 -1
  58. package/dist/storage/index.d.ts +1 -1
  59. package/dist/storage/index.d.ts.map +1 -1
  60. package/dist/storage/interfaces.d.ts +74 -3
  61. package/dist/storage/interfaces.d.ts.map +1 -1
  62. package/dist/storage/local-sqlite.d.ts.map +1 -1
  63. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  64. package/dist/storage.js +464 -33
  65. package/dist/task-manifest.js +8 -2
  66. package/dist/types/index.d.ts +5 -2
  67. package/dist/types/index.d.ts.map +1 -1
  68. package/package.json +2 -2
  69. package/dist/mcp/tools/code-tools.d.ts +0 -15
  70. package/dist/mcp/tools/code-tools.d.ts.map +0 -1
@@ -336,7 +336,7 @@ var init_types = __esm(() => {
336
336
  // src/lib/paths.ts
337
337
  import { existsSync } from "fs";
338
338
  import { homedir } from "os";
339
- import { join as join2, resolve } from "path";
339
+ import { isAbsolute, join as join2, resolve } from "path";
340
340
  import { homedir as pathsResolverHomedir } from "os";
341
341
  import { join as pathsResolverJoin } from "path";
342
342
  function pathsResolverAssertApp(app) {
@@ -393,8 +393,14 @@ function dataDir(options) {
393
393
  function effectiveHome(env = process.env) {
394
394
  return env.HOME || env.USERPROFILE || homedir();
395
395
  }
396
+ function hasnaHomeRoot(env = process.env) {
397
+ const override = env["HASNA_HOME"]?.trim();
398
+ if (override && isAbsolute(override))
399
+ return override;
400
+ return join2(effectiveHome(env), ".hasna");
401
+ }
396
402
  function legacyHomeDir(env = process.env) {
397
- return join2(effectiveHome(env), ".hasna", "todos");
403
+ return join2(hasnaHomeRoot(env), "todos");
398
404
  }
399
405
  function resolverHome(env = process.env) {
400
406
  return dataDir({ app: "todos", home: effectiveHome(env), env });
@@ -4995,20 +5001,17 @@ function ensureAgentIdentitySchema(db) {
4995
5001
  SELECT RAISE(ABORT, 'IDENTITY_SOURCE_LINEAGE_IMMUTABLE');
4996
5002
  END;
4997
5003
 
5004
+ -- Drop first so an older definition is replaced rather than silently
5005
+ -- retained (the upgrade path). AGENT_IDENTITY_HISTORY_TRIGGER_DDL then
5006
+ -- re-creates them with CREATE TRIGGER IF NOT EXISTS. The two statements
5007
+ -- do different jobs and both are kept: the DROP is what refreshes a
5008
+ -- stale definition, and the IF NOT EXISTS is what keeps the CREATE safe
5009
+ -- on its own \u2014 so a caller that reaches it without a preceding DROP, in
5010
+ -- its own connection or transaction, gets a no-op instead of
5011
+ -- "trigger ... already exists".
4998
5012
  DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_immutable;
4999
- CREATE TRIGGER trg_agent_identity_mapping_history_immutable
5000
- BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
5001
- ON agent_identity_source_mappings
5002
- BEGIN
5003
- SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
5004
- END;
5005
-
5006
5013
  DROP TRIGGER IF EXISTS trg_agent_identity_mapping_history_append_only;
5007
- CREATE TRIGGER trg_agent_identity_mapping_history_append_only
5008
- BEFORE DELETE ON agent_identity_source_mappings
5009
- BEGIN
5010
- SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
5011
- END;
5014
+ ${AGENT_IDENTITY_HISTORY_TRIGGER_DDL}
5012
5015
  `);
5013
5016
  db.run("INSERT OR IGNORE INTO _migrations (id) VALUES (?)", [IDENTITY_MIGRATION_ID]);
5014
5017
  });
@@ -5048,7 +5051,20 @@ function storeAgentAlias(agentId, label, aliasKind, status, db) {
5048
5051
  function recordAgentAlias(agentId, label, db) {
5049
5052
  return storeAgentAlias(agentId, label, "historical", "active", db);
5050
5053
  }
5051
- var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65;
5054
+ var IDENTITY_PROJECTION_CONTRACT, IDENTITY_MIGRATION_ID = 65, AGENT_IDENTITY_HISTORY_TRIGGER_DDL = `
5055
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_immutable
5056
+ BEFORE UPDATE OF local_agent_id, observed_label, evidence, mapping_basis, status, revision, created_at
5057
+ ON agent_identity_source_mappings
5058
+ BEGIN
5059
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
5060
+ END;
5061
+
5062
+ CREATE TRIGGER IF NOT EXISTS trg_agent_identity_mapping_history_append_only
5063
+ BEFORE DELETE ON agent_identity_source_mappings
5064
+ BEGIN
5065
+ SELECT RAISE(ABORT, 'IDENTITY_MAPPING_HISTORY_IMMUTABLE');
5066
+ END;
5067
+ `;
5052
5068
  var init_identity_mapping = __esm(() => {
5053
5069
  init_types();
5054
5070
  init_types();
@@ -5365,15 +5381,18 @@ __export(exports_database, {
5365
5381
  resolvePartialId: () => resolvePartialId,
5366
5382
  resolveAssignedToAliases: () => resolveAssignedToAliases,
5367
5383
  resetDatabase: () => resetDatabase,
5384
+ refuseLocalStore: () => refuseLocalStore,
5368
5385
  now: () => now,
5369
5386
  lowerInClause: () => lowerInClause,
5370
5387
  lockExpiryCutoff: () => lockExpiryCutoff,
5371
5388
  isLockExpired: () => isLockExpired,
5389
+ isLocalStoreRefused: () => isLocalStoreRefused,
5372
5390
  getDatabasePath: () => getDatabasePath,
5373
5391
  getDatabase: () => getDatabase,
5374
5392
  closeDatabase: () => closeDatabase,
5375
5393
  clearExpiredLocks: () => clearExpiredLocks,
5376
5394
  assignedToAliasSet: () => assignedToAliasSet,
5395
+ allowLocalStore: () => allowLocalStore,
5377
5396
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
5378
5397
  });
5379
5398
  import { Database } from "bun:sqlite";
@@ -5490,7 +5509,18 @@ function maybeInstallShadowCapture(db) {
5490
5509
  console.error(`[todos] shadow capture install failed: ${error instanceof Error ? error.message : String(error)}`);
5491
5510
  }
5492
5511
  }
5512
+ function refuseLocalStore(message) {
5513
+ localStoreRefusal = message;
5514
+ }
5515
+ function allowLocalStore() {
5516
+ localStoreRefusal = null;
5517
+ }
5518
+ function isLocalStoreRefused() {
5519
+ return localStoreRefusal !== null;
5520
+ }
5493
5521
  function getDatabase(dbPath) {
5522
+ if (localStoreRefusal !== null)
5523
+ throw new Error(localStoreRefusal);
5494
5524
  if (dbPath === undefined && !selectsTodosLocalStore()) {
5495
5525
  throw new Error("API_DATABASE_FALLBACK_FORBIDDEN: this operation must use the shared Todos API; implicit SQLite access is unavailable");
5496
5526
  }
@@ -5631,7 +5661,7 @@ function lowerInClause(column, values, params) {
5631
5661
  params.push(...values.map((v) => v.toLowerCase()));
5632
5662
  return `LOWER(${column}) IN (${values.map(() => "?").join(",")})`;
5633
5663
  }
5634
- var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
5664
+ var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, localStoreRefusal = null, ALLOWED_TABLES;
5635
5665
  var init_database = __esm(() => {
5636
5666
  init_local_opt_in();
5637
5667
  init_schema();
@@ -10679,7 +10709,9 @@ function listTasks2(filter = {}, db) {
10679
10709
  params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
10680
10710
  } catch {}
10681
10711
  }
10682
- if (!filter.include_archived) {
10712
+ if (filter.archived_only) {
10713
+ conditions.push("archived_at IS NOT NULL");
10714
+ } else if (!filter.include_archived) {
10683
10715
  conditions.push("archived_at IS NULL");
10684
10716
  }
10685
10717
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -10692,7 +10724,8 @@ function listTasks2(filter = {}, db) {
10692
10724
  params.push(filter.offset);
10693
10725
  }
10694
10726
  }
10695
- const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
10727
+ const orderBy = filter.archived_only ? "archived_at DESC, id DESC" : `${PRIORITY_RANK}, created_at DESC, id ASC`;
10728
+ const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${orderBy}${limitClause}`).all(...params);
10696
10729
  return rows.map(rowToTask2);
10697
10730
  }
10698
10731
  function getTaskByFingerprint(fingerprint, db) {
@@ -10835,7 +10868,9 @@ function countTasks(filter = {}, db) {
10835
10868
  }
10836
10869
  }
10837
10870
  addMetadataConditions(filter.metadata, conditions, params);
10838
- if (!filter.include_archived) {
10871
+ if (filter.archived_only) {
10872
+ conditions.push("archived_at IS NOT NULL");
10873
+ } else if (!filter.include_archived) {
10839
10874
  conditions.push("archived_at IS NULL");
10840
10875
  }
10841
10876
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
@@ -10972,6 +11007,10 @@ function updateTaskStored(id, input, db) {
10972
11007
  sets.push("completed_at = ?");
10973
11008
  params.push(input.completed_at);
10974
11009
  }
11010
+ if (input.archived_at !== undefined) {
11011
+ sets.push("archived_at = ?");
11012
+ params.push(input.archived_at);
11013
+ }
10975
11014
  if (input.confidence !== undefined) {
10976
11015
  sets.push("confidence = ?");
10977
11016
  params.push(input.confidence);
@@ -11035,6 +11074,8 @@ function updateTaskStored(id, input, db) {
11035
11074
  logTaskChange2(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
11036
11075
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
11037
11076
  logTaskChange2(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
11077
+ if (input.archived_at !== undefined && input.archived_at !== task.archived_at)
11078
+ logTaskChange2(id, "update", "archived_at", task.archived_at, input.archived_at, agentId, d);
11038
11079
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
11039
11080
  logTaskChange2(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
11040
11081
  if (input.approved_by !== undefined)
@@ -13735,7 +13776,7 @@ import { createHash as createHash8 } from "crypto";
13735
13776
  // package.json
13736
13777
  var package_default = {
13737
13778
  name: "@hasna/todos",
13738
- version: "0.16.0",
13779
+ version: "0.17.0",
13739
13780
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
13740
13781
  type: "module",
13741
13782
  main: "dist/index.js",
@@ -13848,7 +13889,7 @@ var package_default = {
13848
13889
  url: "https://github.com/hasna/apps.git",
13849
13890
  directory: "apps/todos"
13850
13891
  },
13851
- homepage: "https://github.com/hasna/apps",
13892
+ homepage: "https://github.com/hasna/apps/tree/main/apps/todos#readme",
13852
13893
  bugs: {
13853
13894
  url: "https://github.com/hasna/apps/issues"
13854
13895
  },
@@ -16128,6 +16169,8 @@ function createPostgresTodosStorageAdapter(options) {
16128
16169
  count: (filter = {}) => store.countTasks(filter),
16129
16170
  update: (id, input, context) => updateTask(id, input, store, context),
16130
16171
  delete: (id, context) => store.deleteTaskHierarchy(id, context),
16172
+ bulkCreateAtomic: (inputs, context) => bulkCreateTasksAtomic(inputs, store, context),
16173
+ bulkDeleteAtomic: (ids, force, context) => bulkDeleteTasksAtomic(ids, force, store, context),
16131
16174
  start: (id, agentId) => startTask(id, agentId, store),
16132
16175
  complete: (id, agentId, options2) => completeTask(id, agentId, options2, store),
16133
16176
  fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, store),
@@ -16144,6 +16187,7 @@ function createPostgresTodosStorageAdapter(options) {
16144
16187
  add: (taskId, dependsOn, context) => addDependency(taskId, dependsOn, store, context),
16145
16188
  remove: (taskId, dependsOn) => removeDependency(taskId, dependsOn, store),
16146
16189
  list: (taskId) => listDependencies(taskId, store),
16190
+ listPage: (page) => store.listDependencyPage(page),
16147
16191
  listAll: () => store.list("dependencies")
16148
16192
  },
16149
16193
  verifications: {
@@ -16209,6 +16253,7 @@ function createPostgresTodosStorageAdapter(options) {
16209
16253
  get: (id) => store.get("agents", id),
16210
16254
  getByName: async (name) => matchAgentByName(await store.list("agents"), name),
16211
16255
  list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
16256
+ listPage: (options2) => store.listAgentPage(options2),
16212
16257
  update: (id, input) => updateAgent(id, input, store),
16213
16258
  heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
16214
16259
  release: (idOrName, sessionId, context) => releaseAgent(idOrName, sessionId, store, context)
@@ -16266,8 +16311,9 @@ function createPostgresTodosStorageAdapter(options) {
16266
16311
  getCommentsPage: async (taskId, options2) => {
16267
16312
  return (await store.listComments(taskId, options2)).map(redactComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
16268
16313
  },
16269
- getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
16270
- getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
16314
+ getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id)),
16315
+ getTaskHistoryPage: (taskId, options2) => store.listTaskHistoryPage(taskId, options2),
16316
+ getRecentActivity: (limit = 20) => store.listRecentActivity(limit)
16271
16317
  },
16272
16318
  machines: createPostgresMachineRegistry(options.client, options.service ?? "todos", options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, () => store.ensureSchema()),
16273
16319
  atomicProjectMigration: createAtomicProjectMigration({ client: options.client, table: options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, service: options.service ?? "todos", ensureSchema: () => store.ensureSchema() }),
@@ -16304,6 +16350,8 @@ class PostgresJsonRecordStore {
16304
16350
  }
16305
16351
  async withDependencyGraphTransaction(fn) {
16306
16352
  await this.ensureSchema();
16353
+ if (this.projectIntegrityLocked)
16354
+ return fn(this);
16307
16355
  return this.withTaskParentIntegrityTransaction(async (client) => {
16308
16356
  const scoped = new PostgresJsonRecordStore({ ...this.options, client });
16309
16357
  scoped.schemaReady = Promise.resolve();
@@ -16450,6 +16498,128 @@ class PostgresJsonRecordStore {
16450
16498
  async list(type) {
16451
16499
  return (await this.listRecords(type)).map((record) => record.payload);
16452
16500
  }
16501
+ async listAgentPage(options) {
16502
+ const { limit, offset } = options;
16503
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
16504
+ throw new Error("Postgres agent page limit must be an integer from 1 to 500");
16505
+ }
16506
+ if (!Number.isSafeInteger(offset) || offset < 0) {
16507
+ throw new Error("Postgres agent page offset must be a non-negative integer");
16508
+ }
16509
+ await this.ensureSchema();
16510
+ const result = await this.options.client.query(`/* todos:list-agent-page */ WITH filtered AS (
16511
+ SELECT object_id, payload
16512
+ FROM ${this.tableName}
16513
+ WHERE service = $1 AND object_type = 'agents' AND deleted_at IS NULL
16514
+ AND ($2::boolean OR COALESCE(payload->>'status', 'active') <> 'archived')
16515
+ ), page AS (
16516
+ SELECT object_id, payload FROM filtered
16517
+ ORDER BY LOWER(payload->>'name'), object_id
16518
+ LIMIT $3 OFFSET $4
16519
+ )
16520
+ SELECT (SELECT count(*) FROM filtered) AS total,
16521
+ COALESCE((SELECT jsonb_agg(payload ORDER BY LOWER(payload->>'name'), object_id) FROM page), '[]'::jsonb) AS agents`, [this.service, options.include_archived === true, limit, offset]);
16522
+ const row = result.rows[0];
16523
+ const total = Number(row?.total);
16524
+ if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.agents)) {
16525
+ throw new Error("Postgres agent page returned invalid count or agents");
16526
+ }
16527
+ if (row.agents.length > limit || row.agents.length > 0 && offset + row.agents.length > total) {
16528
+ throw new Error("Postgres agent page exceeded its requested bounds");
16529
+ }
16530
+ return { agents: row.agents.map((value) => payloadRecord2(value)), total };
16531
+ }
16532
+ async listRecentActivity(limit) {
16533
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e4) {
16534
+ throw new Error("Postgres recent activity limit must be an integer from 1 to 10000");
16535
+ }
16536
+ await this.ensureSchema();
16537
+ const result = await this.options.client.query(`/* todos:list-recent-activity */ SELECT payload
16538
+ FROM ${this.tableName}
16539
+ WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
16540
+ ORDER BY payload->>'created_at' DESC, object_id DESC
16541
+ LIMIT $2`, [this.service, limit]);
16542
+ return result.rows.map((row) => payloadRecord2(row.payload));
16543
+ }
16544
+ async listTaskHistoryPage(taskId, options) {
16545
+ const { limit, offset } = options;
16546
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
16547
+ throw new Error("Postgres task history limit must be an integer from 1 to 500");
16548
+ }
16549
+ if (!Number.isSafeInteger(offset) || offset < 0) {
16550
+ throw new Error("Postgres task history offset must be a non-negative integer");
16551
+ }
16552
+ await this.ensureSchema();
16553
+ const direction = options.order === "asc" ? "ASC" : "DESC";
16554
+ const result = await this.options.client.query(`/* todos:list-task-history-page */ WITH filtered AS (
16555
+ SELECT object_id, payload, payload->>'created_at' AS created_at
16556
+ FROM ${this.tableName}
16557
+ WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
16558
+ AND payload->>'task_id' = $2
16559
+ AND ($3::text IS NULL OR payload->>'created_at' >= $3)
16560
+ AND ($4::text IS NULL OR payload->>'created_at' <= $4)
16561
+ ), page AS (
16562
+ SELECT object_id, payload, created_at FROM filtered
16563
+ ORDER BY created_at ${direction}, object_id ${direction}
16564
+ LIMIT $5 OFFSET $6
16565
+ )
16566
+ SELECT (SELECT count(*) FROM filtered) AS total,
16567
+ COALESCE((SELECT jsonb_agg(payload ORDER BY created_at ${direction}, object_id ${direction}) FROM page), '[]'::jsonb) AS history`, [this.service, taskId, options.since ?? null, options.until ?? null, limit, offset]);
16568
+ const row = result.rows[0];
16569
+ const total = Number(row?.total);
16570
+ if (!Number.isSafeInteger(total) || total < 0 || !Array.isArray(row?.history)) {
16571
+ throw new Error("Postgres task history page returned invalid count or history");
16572
+ }
16573
+ if (row.history.length > limit || row.history.length > 0 && offset + row.history.length > total) {
16574
+ throw new Error("Postgres task history page exceeded its requested bounds");
16575
+ }
16576
+ return { history: row.history.map((value) => payloadRecord2(value)), total };
16577
+ }
16578
+ async listDependencyPage(options) {
16579
+ const { limit, offset } = options;
16580
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
16581
+ throw new Error("Postgres dependency page limit must be an integer from 1 to 500");
16582
+ }
16583
+ if (!Number.isSafeInteger(offset) || offset < 0) {
16584
+ throw new Error("Postgres dependency page offset must be a non-negative integer");
16585
+ }
16586
+ await this.ensureSchema();
16587
+ const result = await this.options.client.query(`/* todos:list-dependencies-page */ WITH page AS (
16588
+ SELECT payload, updated_at, object_id
16589
+ FROM ${this.tableName}
16590
+ WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL
16591
+ ORDER BY updated_at ASC, object_id ASC
16592
+ LIMIT $2 OFFSET $3
16593
+ )
16594
+ SELECT
16595
+ (SELECT COUNT(*)::text FROM ${this.tableName}
16596
+ WHERE service = $1 AND object_type = 'dependencies' AND deleted_at IS NULL) AS total,
16597
+ COALESCE(
16598
+ jsonb_agg(page.payload ORDER BY page.updated_at ASC, page.object_id ASC)
16599
+ FILTER (WHERE page.payload IS NOT NULL),
16600
+ '[]'::jsonb
16601
+ ) AS dependencies
16602
+ FROM page`, [this.service, limit, offset]);
16603
+ const row = result.rows[0];
16604
+ const totalText = typeof row?.total === "string" ? row.total : typeof row?.total === "number" && Number.isSafeInteger(row.total) ? String(row.total) : null;
16605
+ if (totalText === null || !/^\d+$/.test(totalText)) {
16606
+ throw new Error("Postgres dependency page returned an invalid total");
16607
+ }
16608
+ const total = Number(totalText);
16609
+ if (!Number.isSafeInteger(total)) {
16610
+ throw new Error("Postgres dependency page total exceeds the safe integer range");
16611
+ }
16612
+ if (!Array.isArray(row?.dependencies)) {
16613
+ throw new Error("Postgres dependency page returned an invalid dependencies array");
16614
+ }
16615
+ if (row.dependencies.length > limit || offset + row.dependencies.length > total) {
16616
+ throw new Error("Postgres dependency page returned rows outside the requested bound");
16617
+ }
16618
+ return {
16619
+ dependencies: row.dependencies.map((value) => payloadRecord2(value)),
16620
+ total
16621
+ };
16622
+ }
16453
16623
  async listComments(taskId, options = {}) {
16454
16624
  await this.ensureSchema();
16455
16625
  const limit = options.limit ?? 100;
@@ -16525,7 +16695,9 @@ class PostgresJsonRecordStore {
16525
16695
  return "1=0";
16526
16696
  return `${column} IN (${values.map((v) => p(v)).join(", ")})`;
16527
16697
  };
16528
- if (filter.include_archived === false)
16698
+ if (filter.archived_only === true)
16699
+ conds.push(`(payload->>'archived_at' IS NOT NULL)`);
16700
+ else if (filter.include_archived === false)
16529
16701
  conds.push(`(payload->>'archived_at' IS NULL)`);
16530
16702
  if (filter.ids)
16531
16703
  conds.push(inClause("payload->>'id'", filter.ids));
@@ -16582,7 +16754,7 @@ class PostgresJsonRecordStore {
16582
16754
  async listTasks(filter) {
16583
16755
  await this.ensureSchema();
16584
16756
  const { where, params, queryRef } = await this.buildTaskFilterSql(filter);
16585
- const orderBy = queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
16757
+ const orderBy = filter.archived_only ? `ORDER BY payload->>'archived_at' DESC, object_id DESC` : queryRef ? `ORDER BY ts_rank_cd(task_search_tsv, websearch_to_tsquery('simple', todos_immutable_unaccent(${queryRef}))) DESC, ${TASK_ORDER_TIEBREAK}` : TASK_ORDER_BY;
16586
16758
  let sql = `/* todos:list-tasks */ SELECT payload FROM ${this.tableName} WHERE ${where} ${orderBy}`;
16587
16759
  if (filter.limit !== undefined) {
16588
16760
  params.push(filter.limit);
@@ -16802,6 +16974,8 @@ class PostgresJsonRecordStore {
16802
16974
  if (planIds.length === 0 && !parentGuard)
16803
16975
  return this.upsert("tasks", value, context);
16804
16976
  await this.ensureSchema();
16977
+ if (!queryClient && this.projectIntegrityLocked)
16978
+ queryClient = this.options.client;
16805
16979
  if (!queryClient) {
16806
16980
  return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
16807
16981
  }
@@ -17339,9 +17513,11 @@ class PostgresJsonRecordStore {
17339
17513
  }
17340
17514
  async deleteTaskHierarchy(id, context = {}) {
17341
17515
  await this.ensureSchema();
17342
- return this.withTaskParentIntegrityTransaction(async (client) => {
17343
- const timestamp = new Date().toISOString();
17344
- const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
17516
+ if (!this.projectIntegrityLocked) {
17517
+ return this.withDependencyGraphTransaction((scoped) => scoped.deleteTaskHierarchy(id, context));
17518
+ }
17519
+ const timestamp = new Date().toISOString();
17520
+ const result = await this.options.client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
17345
17521
  task_tree(object_id, path, cycle) AS (
17346
17522
  SELECT task.object_id, ARRAY[task.object_id], false
17347
17523
  FROM ${this.tableName} AS task
@@ -17403,13 +17579,12 @@ class PostgresJsonRecordStore {
17403
17579
  SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
17404
17580
  (SELECT count(*) FROM tombstoned) AS deleted_count,
17405
17581
  (SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
17406
- this.service,
17407
- id,
17408
- timestamp,
17409
- context.requestId ?? this.sourceMachineId ?? null
17410
- ]);
17411
- return Boolean(result.rows[0]?.found);
17412
- });
17582
+ this.service,
17583
+ id,
17584
+ timestamp,
17585
+ context.requestId ?? this.sourceMachineId ?? null
17586
+ ]);
17587
+ return Boolean(result.rows[0]?.found);
17413
17588
  }
17414
17589
  async getPlanProjectLinkReceipt(receiptId) {
17415
17590
  const value = await this.get("plan_project_link_receipts", receiptId);
@@ -17865,6 +18040,102 @@ class PostgresJsonRecordStore {
17865
18040
  updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
17866
18041
  }
17867
18042
  }
18043
+ async function bulkCreateTasksAtomic(inputs, store, context) {
18044
+ return store.withDependencyGraphTransaction(async (scoped) => {
18045
+ const tempIds = new Map;
18046
+ const created = [];
18047
+ const dependencies = [];
18048
+ for (const input of inputs) {
18049
+ const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
18050
+ const task = await createTask(taskInput, scoped, context);
18051
+ if (temp_id)
18052
+ tempIds.set(temp_id, task.id);
18053
+ created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
18054
+ }
18055
+ for (let index = 0;index < inputs.length; index++) {
18056
+ const input = inputs[index];
18057
+ const taskId = created[index].id;
18058
+ const seenDependencies = new Set;
18059
+ for (const reference of input.depends_on ?? []) {
18060
+ const exact = tempIds.get(reference) ?? (await scoped.get("tasks", reference))?.id;
18061
+ const dependencyId = exact ?? (await scoped.resolveTaskRef(reference))?.id;
18062
+ if (!dependencyId)
18063
+ throw new TaskNotFoundError(reference);
18064
+ if (seenDependencies.has(dependencyId)) {
18065
+ throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId}`);
18066
+ }
18067
+ seenDependencies.add(dependencyId);
18068
+ dependencies.push(await addDependency(taskId, dependencyId, scoped, context));
18069
+ }
18070
+ }
18071
+ return { schema_version: 1, atomic: true, created, dependencies };
18072
+ });
18073
+ }
18074
+ async function bulkDeleteTasksAtomic(ids, force, store, context) {
18075
+ return store.withDependencyGraphTransaction(async (scoped) => {
18076
+ const resolved = [];
18077
+ for (const reference of ids) {
18078
+ resolved.push({
18079
+ reference,
18080
+ task: await scoped.get("tasks", reference) ?? await scoped.resolveTaskRef(reference)
18081
+ });
18082
+ }
18083
+ const seen = new Set;
18084
+ for (const item of resolved) {
18085
+ if (!item.task)
18086
+ continue;
18087
+ if (seen.has(item.task.id)) {
18088
+ throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
18089
+ }
18090
+ seen.add(item.task.id);
18091
+ }
18092
+ const childState = new Map;
18093
+ for (const item of resolved) {
18094
+ if (!item.task)
18095
+ continue;
18096
+ const children = await scoped.listTasks({ parent_id: item.task.id, include_subtasks: true, limit: 1 });
18097
+ childState.set(item.task.id, children.length > 0);
18098
+ }
18099
+ const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
18100
+ const plannedIds = new Set(planned.map((item) => item.task.id));
18101
+ const roots = [];
18102
+ for (const item of planned) {
18103
+ if (!force) {
18104
+ roots.push(item);
18105
+ continue;
18106
+ }
18107
+ let parentId = item.task.parent_id;
18108
+ let covered = false;
18109
+ while (parentId) {
18110
+ if (plannedIds.has(parentId)) {
18111
+ covered = true;
18112
+ break;
18113
+ }
18114
+ parentId = (await scoped.get("tasks", parentId))?.parent_id ?? null;
18115
+ }
18116
+ if (!covered)
18117
+ roots.push(item);
18118
+ }
18119
+ for (const item of roots) {
18120
+ if (!await scoped.deleteTaskHierarchy(item.task.id, context)) {
18121
+ throw new Error(`Atomic bulk delete lost task ${item.task.id}`);
18122
+ }
18123
+ }
18124
+ return {
18125
+ schema_version: 1,
18126
+ atomic: true,
18127
+ force,
18128
+ results: resolved.map((item) => {
18129
+ if (!item.task)
18130
+ return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
18131
+ if (!force && childState.get(item.task.id)) {
18132
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
18133
+ }
18134
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
18135
+ })
18136
+ };
18137
+ });
18138
+ }
17868
18139
  async function createTask(input, store, context) {
17869
18140
  const timestamp = new Date().toISOString();
17870
18141
  const taskId = randomUUID2();
@@ -17955,12 +18226,16 @@ async function updateTask(id, input, store, context) {
17955
18226
  }
17956
18227
  }
17957
18228
  }
18229
+ const patchTouchesPlacement = input.project_id !== undefined || input.task_list_id !== undefined;
18230
+ const effectiveProjectIdForShortId = input.project_id !== undefined ? input.project_id : existing.project_id;
18231
+ const backfilledShortId = existing.short_id === null && patchTouchesPlacement && effectiveProjectIdForShortId ? await nextTaskShortId(effectiveProjectIdForShortId, store, context) : null;
17958
18232
  const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
17959
18233
  const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
17960
18234
  const task = {
17961
18235
  ...existing,
17962
18236
  ...definedPatch(input),
17963
18237
  ...terminalNow ? { locked_by: null, locked_at: null } : {},
18238
+ short_id: backfilledShortId ?? existing.short_id,
17964
18239
  version: existing.version + 1,
17965
18240
  updated_at: new Date().toISOString(),
17966
18241
  tags: input.tags ?? existing.tags,
@@ -19652,6 +19927,7 @@ function searchTasks(options, projectId, taskListId, db) {
19652
19927
 
19653
19928
  // src/storage/local-sqlite.ts
19654
19929
  init_tasks();
19930
+ init_task_graph();
19655
19931
  init_projects();
19656
19932
  init_plans();
19657
19933
 
@@ -20210,6 +20486,19 @@ function listAgents(opts, db) {
20210
20486
  }
20211
20487
  return d.query("SELECT * FROM agents WHERE status = 'active' ORDER BY name").all().map(rowToAgent);
20212
20488
  }
20489
+ function listAgentsPage(options, db) {
20490
+ if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 500) {
20491
+ throw new Error("Agent page limit must be an integer from 1 to 500");
20492
+ }
20493
+ if (!Number.isSafeInteger(options.offset) || options.offset < 0) {
20494
+ throw new Error("Agent page offset must be a non-negative integer");
20495
+ }
20496
+ const d = db || getDatabase();
20497
+ const where = options.include_archived ? "" : "WHERE status = 'active'";
20498
+ const total = d.query(`SELECT COUNT(*) AS total FROM agents ${where}`).get().total;
20499
+ const agents = d.query(`SELECT * FROM agents ${where} ORDER BY LOWER(name), id LIMIT ? OFFSET ?`).all(options.limit, options.offset).map(rowToAgent);
20500
+ return { agents, total };
20501
+ }
20213
20502
  function updateAgentActivity(id, db) {
20214
20503
  const d = db || getDatabase();
20215
20504
  d.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", [now(), id]);
@@ -21048,6 +21337,98 @@ function exportSqliteMachines(db) {
21048
21337
  }
21049
21338
 
21050
21339
  // src/storage/local-sqlite.ts
21340
+ function bulkCreateAtomicSqlite(db, inputs) {
21341
+ return db.transaction(() => {
21342
+ const tempIds = new Map;
21343
+ const created = [];
21344
+ const dependencies = [];
21345
+ for (const input of inputs) {
21346
+ const { temp_id, depends_on: _dependsOn, ...taskInput } = input;
21347
+ const task = createTask2(taskInput, db);
21348
+ if (temp_id)
21349
+ tempIds.set(temp_id, task.id);
21350
+ created.push({ temp_id: temp_id ?? null, id: task.id, short_id: task.short_id, title: task.title });
21351
+ }
21352
+ for (let index = 0;index < inputs.length; index++) {
21353
+ const input = inputs[index];
21354
+ const taskId = created[index].id;
21355
+ const seenDependencies = new Set;
21356
+ for (const reference of input.depends_on ?? []) {
21357
+ const dependencyId2 = tempIds.get(reference) ?? resolveTaskRefLocal(db, reference)?.id;
21358
+ if (!dependencyId2)
21359
+ throw new TaskNotFoundError(reference);
21360
+ if (seenDependencies.has(dependencyId2)) {
21361
+ throw new ResourceConflictError("BULK_CREATE_DUPLICATE_DEPENDENCY", `Multiple references resolve to dependency ${dependencyId2}`);
21362
+ }
21363
+ seenDependencies.add(dependencyId2);
21364
+ addDependency2(taskId, dependencyId2, db);
21365
+ dependencies.push({ task_id: taskId, depends_on: dependencyId2 });
21366
+ }
21367
+ }
21368
+ return { schema_version: 1, atomic: true, created, dependencies };
21369
+ })();
21370
+ }
21371
+ function deleteTaskHierarchySqlite(db, rootId) {
21372
+ const rows = db.query(`WITH RECURSIVE tree(id, depth) AS (
21373
+ SELECT id, 0 FROM tasks WHERE id = ?
21374
+ UNION ALL
21375
+ SELECT child.id, tree.depth + 1 FROM tasks child JOIN tree ON child.parent_id = tree.id
21376
+ ) SELECT id FROM tree ORDER BY depth DESC, id`).all(rootId);
21377
+ if (rows.length === 0)
21378
+ throw new TaskNotFoundError(rootId);
21379
+ for (const row of rows) {
21380
+ if (!deleteTask(row.id, db))
21381
+ throw new Error(`Atomic hierarchy delete lost task ${row.id}`);
21382
+ }
21383
+ }
21384
+ function bulkDeleteAtomicSqlite(db, ids, force) {
21385
+ return db.transaction(() => {
21386
+ const resolved = ids.map((reference) => ({ reference, task: resolveTaskRefLocal(db, reference) }));
21387
+ const seen = new Set;
21388
+ for (const item of resolved) {
21389
+ if (!item.task)
21390
+ continue;
21391
+ if (seen.has(item.task.id)) {
21392
+ throw new ResourceConflictError("BULK_DELETE_DUPLICATE_TASK", `Multiple references resolve to task ${item.task.id}`);
21393
+ }
21394
+ seen.add(item.task.id);
21395
+ }
21396
+ const childState = new Map;
21397
+ for (const item of resolved) {
21398
+ if (!item.task)
21399
+ continue;
21400
+ childState.set(item.task.id, Boolean(db.query("SELECT id FROM tasks WHERE parent_id = ? LIMIT 1").get(item.task.id)));
21401
+ }
21402
+ const planned = resolved.filter((item) => item.task && (force || !childState.get(item.task.id)));
21403
+ const plannedIds = new Set(planned.map((item) => item.task.id));
21404
+ const roots = planned.filter((item) => {
21405
+ if (!force)
21406
+ return true;
21407
+ let parentId = item.task.parent_id;
21408
+ while (parentId) {
21409
+ if (plannedIds.has(parentId))
21410
+ return false;
21411
+ parentId = getTask(parentId, db)?.parent_id ?? null;
21412
+ }
21413
+ return true;
21414
+ });
21415
+ for (const item of roots)
21416
+ deleteTaskHierarchySqlite(db, item.task.id);
21417
+ return {
21418
+ schema_version: 1,
21419
+ atomic: true,
21420
+ force,
21421
+ results: resolved.map((item) => {
21422
+ if (!item.task)
21423
+ return { requested_id: item.reference, task_id: null, outcome: "missing", reason: "not_found" };
21424
+ if (!force && childState.get(item.task.id)) {
21425
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "skipped", reason: "has_children" };
21426
+ }
21427
+ return { requested_id: item.reference, task_id: item.task.id, outcome: "deleted", reason: null };
21428
+ })
21429
+ };
21430
+ })();
21431
+ }
21051
21432
  var TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
21052
21433
  function resolveTaskRefLocal(db, ref) {
21053
21434
  const raw = ref.trim().toLowerCase();
@@ -21144,6 +21525,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
21144
21525
  },
21145
21526
  handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
21146
21527
  delete: (id) => deleteTask(id, database()),
21528
+ bulkCreateAtomic: (inputs, _context) => bulkCreateAtomicSqlite(database(), inputs),
21529
+ bulkDeleteAtomic: (ids, force, _context) => bulkDeleteAtomicSqlite(database(), ids, force),
21147
21530
  start: (id, agentId) => startTask2(id, agentId, database()),
21148
21531
  complete: (id, agentId, options2) => completeTask2(id, agentId, database(), options2),
21149
21532
  fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, database()),
@@ -21152,6 +21535,30 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
21152
21535
  getActiveWork: (filters) => getActiveWork2(filters, database()),
21153
21536
  getChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
21154
21537
  },
21538
+ dependencies: {
21539
+ add: (taskId, dependsOn) => {
21540
+ addDependency2(taskId, dependsOn, database());
21541
+ return { task_id: taskId, depends_on: dependsOn };
21542
+ },
21543
+ remove: (taskId, dependsOn) => removeDependency2(taskId, dependsOn, database()),
21544
+ list: (taskId) => {
21545
+ const dependencies = getTaskDependencies(taskId, database());
21546
+ const blocks = getTaskDependents(taskId, database());
21547
+ return { dependencies, blocks, blocked_by: blocks };
21548
+ },
21549
+ listPage: ({ limit, offset }) => {
21550
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
21551
+ throw new Error("SQLite dependency page limit must be an integer from 1 to 500");
21552
+ }
21553
+ if (!Number.isSafeInteger(offset) || offset < 0) {
21554
+ throw new Error("SQLite dependency page offset must be a non-negative integer");
21555
+ }
21556
+ const totalRow = database().query("SELECT COUNT(*) AS total FROM task_dependencies").get();
21557
+ const dependencies = database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on LIMIT ? OFFSET ?").all(limit, offset);
21558
+ return { dependencies, total: totalRow.total };
21559
+ },
21560
+ listAll: () => database().query("SELECT task_id, depends_on FROM task_dependencies ORDER BY task_id, depends_on").all()
21561
+ },
21155
21562
  projects: {
21156
21563
  create: (input) => createProject2(input, database()),
21157
21564
  get: (id) => getProject(id, database()),
@@ -21195,6 +21602,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
21195
21602
  get: (id) => getAgent(id, database()),
21196
21603
  getByName: (name) => getAgentByName(name, database()),
21197
21604
  list: (options2) => listAgents(options2, database()),
21605
+ listPage: (options2) => listAgentsPage(options2, database()),
21198
21606
  update: (id, input) => updateAgent2(id, input, database())
21199
21607
  },
21200
21608
  taskLists: {
@@ -21233,6 +21641,29 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
21233
21641
  return comments;
21234
21642
  },
21235
21643
  getTaskHistory: (taskId) => getTaskHistory(taskId, database()),
21644
+ getTaskHistoryPage: (taskId, options2) => {
21645
+ if (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 500) {
21646
+ throw new Error("Task history limit must be an integer from 1 to 500");
21647
+ }
21648
+ if (!Number.isSafeInteger(options2.offset) || options2.offset < 0) {
21649
+ throw new Error("Task history offset must be a non-negative integer");
21650
+ }
21651
+ const conditions = ["task_id = ?"];
21652
+ const values = [taskId];
21653
+ if (options2.since) {
21654
+ conditions.push("created_at >= ?");
21655
+ values.push(options2.since);
21656
+ }
21657
+ if (options2.until) {
21658
+ conditions.push("created_at <= ?");
21659
+ values.push(options2.until);
21660
+ }
21661
+ const where = conditions.join(" AND ");
21662
+ const total = database().query(`SELECT COUNT(*) AS total FROM task_history WHERE ${where}`).get(...values).total;
21663
+ const direction = options2.order === "asc" ? "ASC" : "DESC";
21664
+ const history = database().query(`SELECT * FROM task_history WHERE ${where} ORDER BY created_at ${direction}, id ${direction} LIMIT ? OFFSET ?`).all(...values, options2.limit, options2.offset);
21665
+ return { history, total };
21666
+ },
21236
21667
  getRecentActivity: (limit) => getRecentActivity(limit, database())
21237
21668
  },
21238
21669
  sync: {