@hasna/todos 0.15.15 → 0.15.16

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.
package/dist/index.js CHANGED
@@ -12492,7 +12492,7 @@ var init_dispatches = __esm(() => {
12492
12492
  // package.json
12493
12493
  var package_default = {
12494
12494
  name: "@hasna/todos",
12495
- version: "0.15.15",
12495
+ version: "0.15.16",
12496
12496
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12497
12497
  type: "module",
12498
12498
  main: "dist/index.js",
@@ -39090,6 +39090,9 @@ init_types();
39090
39090
  // src/task-manifest/types.ts
39091
39091
  var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1";
39092
39092
  var TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1;
39093
+ function supportsIdempotentOutboxDelivery(capability2) {
39094
+ return capability2 !== null && typeof capability2 === "object" && capability2["idempotent_outbox_delivery"] === true;
39095
+ }
39093
39096
 
39094
39097
  class TodosTaskManifestError extends Error {
39095
39098
  code;
@@ -39735,10 +39738,27 @@ class SqliteTodosTaskManifestBackend {
39735
39738
  SELECT 1 FROM todos_task_manifest_receipts r
39736
39739
  WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
39737
39740
  AND r.tenant_id = ?
39741
+ AND r.authority = 'todos'
39742
+ AND r.route = 'todos.task-manifest.v1'
39743
+ AND r.schema_version = 1
39744
+ AND r.kind = 'apply'
39738
39745
  )`).run(deliveredAt, outboxId, this.tenantId);
39739
- if (result.changes !== 1) {
39740
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
39741
- }
39746
+ if (result.changes === 1)
39747
+ return;
39748
+ const existing = this.db.query(`SELECT o.status
39749
+ FROM todos_task_manifest_outbox o
39750
+ JOIN todos_task_manifest_receipts r
39751
+ ON r.receipt_id = o.apply_receipt_id
39752
+ WHERE r.tenant_id = ?
39753
+ AND r.authority = 'todos'
39754
+ AND r.route = 'todos.task-manifest.v1'
39755
+ AND r.schema_version = 1
39756
+ AND r.kind = 'apply'
39757
+ AND o.id = ?
39758
+ LIMIT 1`).get(this.tenantId, outboxId);
39759
+ if (existing?.status === "delivered")
39760
+ return;
39761
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
39742
39762
  });
39743
39763
  }
39744
39764
  async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
@@ -40270,6 +40290,22 @@ class PostgresTodosTaskManifestBackend {
40270
40290
  async markOutboxDelivered(outboxId, deliveredAt) {
40271
40291
  await this.ensureSchema();
40272
40292
  await this.client.transaction(async (tx) => {
40293
+ const owned = await tx.query(`SELECT r.operation_id
40294
+ FROM todos_task_manifest_outbox o
40295
+ JOIN todos_task_manifest_receipts r
40296
+ ON r.receipt_id = o.apply_receipt_id
40297
+ WHERE r.tenant_id = $1
40298
+ AND r.authority = 'todos'
40299
+ AND r.route = 'todos.task-manifest.v1'
40300
+ AND r.schema_version = 1
40301
+ AND r.kind = 'apply'
40302
+ AND o.id = $2
40303
+ LIMIT 1`, [this.tenantId, outboxId]);
40304
+ const operationId = owned.rows[0]?.operation_id;
40305
+ if (operationId == null) {
40306
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
40307
+ }
40308
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${String(operationId)}`]);
40273
40309
  const result = await tx.query(`UPDATE todos_task_manifest_outbox
40274
40310
  SET status = 'delivered', delivered_at = $1, attempts = attempts + 1
40275
40311
  WHERE id = $2 AND status = 'pending'
@@ -40277,10 +40313,28 @@ class PostgresTodosTaskManifestBackend {
40277
40313
  SELECT 1 FROM todos_task_manifest_receipts r
40278
40314
  WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
40279
40315
  AND r.tenant_id = $3
40316
+ AND r.authority = 'todos'
40317
+ AND r.route = 'todos.task-manifest.v1'
40318
+ AND r.schema_version = 1
40319
+ AND r.kind = 'apply'
40280
40320
  )
40281
40321
  RETURNING id`, [deliveredAt, outboxId, this.tenantId]);
40282
- if (!result.rows[0])
40283
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
40322
+ if (result.rows[0])
40323
+ return;
40324
+ const existing = await tx.query(`SELECT o.status
40325
+ FROM todos_task_manifest_outbox o
40326
+ JOIN todos_task_manifest_receipts r
40327
+ ON r.receipt_id = o.apply_receipt_id
40328
+ WHERE r.tenant_id = $1
40329
+ AND r.authority = 'todos'
40330
+ AND r.route = 'todos.task-manifest.v1'
40331
+ AND r.schema_version = 1
40332
+ AND r.kind = 'apply'
40333
+ AND o.id = $2
40334
+ LIMIT 1`, [this.tenantId, outboxId]);
40335
+ if (existing.rows[0]?.status === "delivered")
40336
+ return;
40337
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
40284
40338
  });
40285
40339
  }
40286
40340
  async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
@@ -40312,7 +40366,13 @@ class PostgresTodosTaskManifestBackend {
40312
40366
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
40313
40367
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
40314
40368
  JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
40315
- WHERE r.tenant_id = $1 AND o.apply_receipt_id = $2 AND o.status = 'delivered'
40369
+ WHERE r.tenant_id = $1
40370
+ AND r.authority = 'todos'
40371
+ AND r.route = 'todos.task-manifest.v1'
40372
+ AND r.schema_version = 1
40373
+ AND r.kind = 'apply'
40374
+ AND o.apply_receipt_id = $2
40375
+ AND o.status = 'delivered'
40316
40376
  LIMIT 1`, [this.tenantId, input.receipt_id]);
40317
40377
  if (delivered.rows[0])
40318
40378
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
@@ -40325,15 +40385,20 @@ class PostgresTodosTaskManifestBackend {
40325
40385
  },
40326
40386
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
40327
40387
  ];
40328
- const outboxRows = await tx.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
40329
- FROM todos_task_manifest_outbox
40330
- WHERE apply_receipt_id = $1
40388
+ const outboxRows = await tx.query(`SELECT o.id, o.topic, o.payload, o.payload_digest, o.status, o.attempts, o.delivered_at
40389
+ FROM todos_task_manifest_outbox o
40390
+ WHERE o.apply_receipt_id = $1
40331
40391
  AND EXISTS (
40332
40392
  SELECT 1 FROM todos_task_manifest_receipts r
40333
- WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
40393
+ WHERE r.receipt_id = o.apply_receipt_id
40334
40394
  AND r.tenant_id = $2
40395
+ AND r.authority = 'todos'
40396
+ AND r.route = 'todos.task-manifest.v1'
40397
+ AND r.schema_version = 1
40398
+ AND r.kind = 'apply'
40335
40399
  )
40336
- ORDER BY id`, [input.receipt_id, this.tenantId]);
40400
+ ORDER BY o.id
40401
+ FOR UPDATE OF o`, [input.receipt_id, this.tenantId]);
40337
40402
  if (outboxRows.rows.length !== expectedEffects.length) {
40338
40403
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
40339
40404
  }
@@ -40433,14 +40498,23 @@ class PostgresTodosTaskManifestBackend {
40433
40498
  }
40434
40499
  if (stored.rows.length !== managedIds.length)
40435
40500
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: managed graph is incomplete");
40436
- await tx.query(`UPDATE todos_task_manifest_outbox
40501
+ const cancelled = await tx.query(`UPDATE todos_task_manifest_outbox
40437
40502
  SET status = 'cancelled'
40438
40503
  WHERE apply_receipt_id = $1 AND status = 'pending'
40439
40504
  AND EXISTS (
40440
40505
  SELECT 1 FROM todos_task_manifest_receipts r
40441
40506
  WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
40442
40507
  AND r.tenant_id = $2
40443
- )`, [input.receipt_id, this.tenantId]);
40508
+ AND r.authority = 'todos'
40509
+ AND r.route = 'todos.task-manifest.v1'
40510
+ AND r.schema_version = 1
40511
+ AND r.kind = 'apply'
40512
+ )
40513
+ RETURNING id`, [input.receipt_id, this.tenantId]);
40514
+ const cancelledIds = new Set(cancelled.rows.map((row) => String(row.id)));
40515
+ if (cancelledIds.size !== applyResult.outbox_ids.length || applyResult.outbox_ids.some((id) => !cancelledIds.has(id))) {
40516
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: failed to cancel every expected outbox row");
40517
+ }
40444
40518
  const typedIds = [
40445
40519
  ["dependencies", applyResult.graph.dependency_ids],
40446
40520
  ["comments", applyResult.graph.comment_ids],
@@ -40627,6 +40701,7 @@ class PackageOwnedTodosTaskManifestAuthority {
40627
40701
  deterministic_ids: true,
40628
40702
  immutable_receipts: true,
40629
40703
  transactional_outbox: true,
40704
+ idempotent_outbox_delivery: true,
40630
40705
  exact_bounded_readback: true,
40631
40706
  conditional_compensation: true,
40632
40707
  transcript_safe: false,
@@ -62008,6 +62083,7 @@ export {
62008
62083
  syncWithAgents,
62009
62084
  syncWithAgent,
62010
62085
  syncKgEdges,
62086
+ supportsIdempotentOutboxDelivery,
62011
62087
  supersedeDecisionRecord,
62012
62088
  summarizeTaskRun,
62013
62089
  summarizeRoadmap,
@@ -6,6 +6,16 @@ import { GOAL_COMMAND_RECIPES } from "./goal-workflow.js";
6
6
  export declare const ADAPTER_DOCS_SCHEMA_VERSION = "todos.agent_adapter_docs.v1";
7
7
  export declare const AGENT_ADAPTER_HOSTS: readonly ["codex", "claude-code", "takumi"];
8
8
  export type AgentAdapterHost = (typeof AGENT_ADAPTER_HOSTS)[number];
9
+ /**
10
+ * Agents accepted by `todos mcp --register` / `--unregister`. Kept beside the
11
+ * adapter docs so the docs can never advertise a host the CLI rejects — the
12
+ * reader of a recovery field is an agent, and a recovery command that exits 1
13
+ * strands it. `todos mcp --register takumi` shipped in doc metadata while the
14
+ * CLI rejected it; the consistency test in agent-adapter-docs.test.ts pins
15
+ * every advertised `--register <agent>` to this list.
16
+ */
17
+ export declare const MCP_REGISTRABLE_CLI_AGENTS: readonly ["claude", "codex", "gemini", "cursor", "takumi"];
18
+ export type McpRegistrableCliAgent = (typeof MCP_REGISTRABLE_CLI_AGENTS)[number];
9
19
  export interface AdapterWorkflowStep {
10
20
  step: number;
11
21
  title: string;
@@ -1 +1 @@
1
- {"version":3,"file":"agent-adapter-docs.d.ts","sourceRoot":"","sources":["../../src/lib/agent-adapter-docs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,eAAO,MAAM,2BAA2B,gCAAgC,CAAC;AAEzE,eAAO,MAAM,mBAAmB,6CAA8C,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,OAAO,2BAA2B,CAAC;IACnD,IAAI,EAAE,gBAAgB,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE;QACP,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,GAAG,EAAE;QACH,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,GAAG,EAAE,MAAM,EAAE,CAAC;KACf,CAAC;IACF,aAAa,EAAE,OAAO,oBAAoB,CAAC;IAC3C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,aAAa,EAAE;QACb,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;KACpC,CAAC;IACF,YAAY,EAAE;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,QAAQ,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAgCD,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,gBAAgB,EAAE,eAAe,CA0IxE,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAM3E;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAGvE;AAED,wBAAgB,oBAAoB,IAAI,eAAe,EAAE,CAExD;AAED,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CA6B9C;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6EpE;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAIrD;AAED,gFAAgF;AAChF,wBAAgB,yBAAyB,IAAI,MAAM,CAoBlD"}
1
+ {"version":3,"file":"agent-adapter-docs.d.ts","sourceRoot":"","sources":["../../src/lib/agent-adapter-docs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,eAAO,MAAM,2BAA2B,gCAAgC,CAAC;AAEzE,eAAO,MAAM,mBAAmB,6CAA8C,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,4DAA6D,CAAC;AACrG,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,OAAO,2BAA2B,CAAC;IACnD,IAAI,EAAE,gBAAgB,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE;QACP,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,GAAG,EAAE;QACH,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,GAAG,EAAE,MAAM,EAAE,CAAC;KACf,CAAC;IACF,aAAa,EAAE,OAAO,oBAAoB,CAAC;IAC3C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,aAAa,EAAE;QACb,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;KACpC,CAAC;IACF,YAAY,EAAE;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,QAAQ,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAgCD,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,gBAAgB,EAAE,eAAe,CA0IxE,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAM3E;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAGvE;AAED,wBAAgB,oBAAoB,IAAI,eAAe,EAAE,CAExD;AAED,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CA6B9C;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6EpE;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAIrD;AAED,gFAAgF;AAChF,wBAAgB,yBAAyB,IAAI,MAAM,CAoBlD"}
package/dist/mcp/index.js CHANGED
@@ -21041,10 +21041,72 @@ async function requireTagsFilterCapability(client) {
21041
21041
  throw new Error(`REMOTE_TAGS_FILTER_UNSUPPORTED: configured Todos authority ${authority} does not advertise the tags ` + "query param on GET /v1/tasks; deploy the current @hasna/todos /v1 server to filter by tag; " + "no unfiltered task read was issued");
21042
21042
  }
21043
21043
  }
21044
- async function requestCloudTaskPage(client, filter) {
21044
+ function parseCloudTaskTotal(raw) {
21045
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
21046
+ return;
21047
+ const total = raw.total;
21048
+ return typeof total === "number" && Number.isSafeInteger(total) && total >= 0 ? total : undefined;
21049
+ }
21050
+ async function requestRawCloudTaskPage(client, filter) {
21045
21051
  const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
21046
21052
  const envelope = res.raw;
21047
- return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
21053
+ return {
21054
+ tasks: Array.isArray(envelope?.tasks) ? envelope.tasks : res.items,
21055
+ total: parseCloudTaskTotal(res.raw)
21056
+ };
21057
+ }
21058
+ function cloudTaskListFilterError(code, taskListId, detail) {
21059
+ return new Error(`${code}: hosted authority returned rows outside requested task_list_id ${taskListId}; ${detail}; refusing an incomplete exact-list result`);
21060
+ }
21061
+ async function requestCloudTaskPage(client, filter) {
21062
+ const firstPage = await requestRawCloudTaskPage(client, filter);
21063
+ const tasks = firstPage.tasks;
21064
+ if (filter.task_list_id === undefined)
21065
+ return tasks;
21066
+ const taskListId = filter.task_list_id;
21067
+ if (tasks.every((task) => task.task_list_id === taskListId))
21068
+ return tasks;
21069
+ const scanLimit = filter.limit;
21070
+ const startOffset = filter.offset ?? 0;
21071
+ if (startOffset !== 0 || scanLimit === undefined || !Number.isSafeInteger(scanLimit) || scanLimit <= 0 || firstPage.total === undefined) {
21072
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, "the response does not provide complete bounded total/offset pagination evidence");
21073
+ }
21074
+ const total = firstPage.total;
21075
+ if (total > scanLimit) {
21076
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_INCOMPLETE", taskListId, `reported total ${total} exceeds bounded scan limit ${scanLimit}`);
21077
+ }
21078
+ if (tasks.length > total) {
21079
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, `the first page contains ${tasks.length} rows but reports total ${total}`);
21080
+ }
21081
+ const seenTaskIds = new Set;
21082
+ for (const task of tasks) {
21083
+ if (seenTaskIds.has(task.id)) {
21084
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, `the first page repeats task id ${task.id}`);
21085
+ }
21086
+ seenTaskIds.add(task.id);
21087
+ }
21088
+ while (tasks.length < total) {
21089
+ const remaining = total - tasks.length;
21090
+ const page = await requestRawCloudTaskPage(client, {
21091
+ ...filter,
21092
+ offset: tasks.length,
21093
+ limit: Math.min(scanLimit - tasks.length, remaining)
21094
+ });
21095
+ if (page.total !== total) {
21096
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, `pagination total changed from ${total} to ${String(page.total)}`);
21097
+ }
21098
+ if (page.tasks.length === 0 || tasks.length + page.tasks.length > total) {
21099
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, "pagination did not make bounded progress toward the reported total");
21100
+ }
21101
+ for (const task of page.tasks) {
21102
+ if (seenTaskIds.has(task.id)) {
21103
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, `pagination repeats task id ${task.id}`);
21104
+ }
21105
+ seenTaskIds.add(task.id);
21106
+ }
21107
+ tasks.push(...page.tasks);
21108
+ }
21109
+ return tasks.filter((task) => task.task_list_id === taskListId);
21048
21110
  }
21049
21111
  async function cloudListTasks(client, filter = {}) {
21050
21112
  if (filter.tags?.length)
@@ -21083,12 +21145,10 @@ async function cloudGetTask(client, id) {
21083
21145
  }
21084
21146
  async function cloudCreateTask(client, input) {
21085
21147
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
21086
- const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => expectedParentId === null ? client.create("tasks", input) : client.create("tasks", input, { retry: false }), ["PARENT_TASK_NOT_FOUND"]));
21148
+ const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.create("tasks", input, { retry: false }), ["PARENT_TASK_NOT_FOUND"]));
21087
21149
  if (!created || typeof created.id !== "string" || !created.id.trim()) {
21088
21150
  throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
21089
21151
  }
21090
- if (expectedParentId === null)
21091
- return created;
21092
21152
  const persisted = await cloudGetTask(client, created.id);
21093
21153
  if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId) {
21094
21154
  throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + "stored task id and parent_id; no success row or local SQLite fallback is permitted");
@@ -35415,7 +35475,7 @@ var package_default;
35415
35475
  var init_package = __esm(() => {
35416
35476
  package_default = {
35417
35477
  name: "@hasna/todos",
35418
- version: "0.15.15",
35478
+ version: "0.15.16",
35419
35479
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
35420
35480
  type: "module",
35421
35481
  main: "dist/index.js",
@@ -51606,6 +51666,22 @@ class PostgresTodosTaskManifestBackend {
51606
51666
  async markOutboxDelivered(outboxId, deliveredAt) {
51607
51667
  await this.ensureSchema();
51608
51668
  await this.client.transaction(async (tx) => {
51669
+ const owned = await tx.query(`SELECT r.operation_id
51670
+ FROM todos_task_manifest_outbox o
51671
+ JOIN todos_task_manifest_receipts r
51672
+ ON r.receipt_id = o.apply_receipt_id
51673
+ WHERE r.tenant_id = $1
51674
+ AND r.authority = 'todos'
51675
+ AND r.route = 'todos.task-manifest.v1'
51676
+ AND r.schema_version = 1
51677
+ AND r.kind = 'apply'
51678
+ AND o.id = $2
51679
+ LIMIT 1`, [this.tenantId, outboxId]);
51680
+ const operationId = owned.rows[0]?.operation_id;
51681
+ if (operationId == null) {
51682
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
51683
+ }
51684
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${String(operationId)}`]);
51609
51685
  const result = await tx.query(`UPDATE todos_task_manifest_outbox
51610
51686
  SET status = 'delivered', delivered_at = $1, attempts = attempts + 1
51611
51687
  WHERE id = $2 AND status = 'pending'
@@ -51613,10 +51689,28 @@ class PostgresTodosTaskManifestBackend {
51613
51689
  SELECT 1 FROM todos_task_manifest_receipts r
51614
51690
  WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
51615
51691
  AND r.tenant_id = $3
51692
+ AND r.authority = 'todos'
51693
+ AND r.route = 'todos.task-manifest.v1'
51694
+ AND r.schema_version = 1
51695
+ AND r.kind = 'apply'
51616
51696
  )
51617
51697
  RETURNING id`, [deliveredAt, outboxId, this.tenantId]);
51618
- if (!result.rows[0])
51619
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
51698
+ if (result.rows[0])
51699
+ return;
51700
+ const existing = await tx.query(`SELECT o.status
51701
+ FROM todos_task_manifest_outbox o
51702
+ JOIN todos_task_manifest_receipts r
51703
+ ON r.receipt_id = o.apply_receipt_id
51704
+ WHERE r.tenant_id = $1
51705
+ AND r.authority = 'todos'
51706
+ AND r.route = 'todos.task-manifest.v1'
51707
+ AND r.schema_version = 1
51708
+ AND r.kind = 'apply'
51709
+ AND o.id = $2
51710
+ LIMIT 1`, [this.tenantId, outboxId]);
51711
+ if (existing.rows[0]?.status === "delivered")
51712
+ return;
51713
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
51620
51714
  });
51621
51715
  }
51622
51716
  async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
@@ -51648,7 +51742,13 @@ class PostgresTodosTaskManifestBackend {
51648
51742
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
51649
51743
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
51650
51744
  JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
51651
- WHERE r.tenant_id = $1 AND o.apply_receipt_id = $2 AND o.status = 'delivered'
51745
+ WHERE r.tenant_id = $1
51746
+ AND r.authority = 'todos'
51747
+ AND r.route = 'todos.task-manifest.v1'
51748
+ AND r.schema_version = 1
51749
+ AND r.kind = 'apply'
51750
+ AND o.apply_receipt_id = $2
51751
+ AND o.status = 'delivered'
51652
51752
  LIMIT 1`, [this.tenantId, input.receipt_id]);
51653
51753
  if (delivered.rows[0])
51654
51754
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
@@ -51661,15 +51761,20 @@ class PostgresTodosTaskManifestBackend {
51661
51761
  },
51662
51762
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
51663
51763
  ];
51664
- const outboxRows = await tx.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
51665
- FROM todos_task_manifest_outbox
51666
- WHERE apply_receipt_id = $1
51764
+ const outboxRows = await tx.query(`SELECT o.id, o.topic, o.payload, o.payload_digest, o.status, o.attempts, o.delivered_at
51765
+ FROM todos_task_manifest_outbox o
51766
+ WHERE o.apply_receipt_id = $1
51667
51767
  AND EXISTS (
51668
51768
  SELECT 1 FROM todos_task_manifest_receipts r
51669
- WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
51769
+ WHERE r.receipt_id = o.apply_receipt_id
51670
51770
  AND r.tenant_id = $2
51771
+ AND r.authority = 'todos'
51772
+ AND r.route = 'todos.task-manifest.v1'
51773
+ AND r.schema_version = 1
51774
+ AND r.kind = 'apply'
51671
51775
  )
51672
- ORDER BY id`, [input.receipt_id, this.tenantId]);
51776
+ ORDER BY o.id
51777
+ FOR UPDATE OF o`, [input.receipt_id, this.tenantId]);
51673
51778
  if (outboxRows.rows.length !== expectedEffects.length) {
51674
51779
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
51675
51780
  }
@@ -51769,14 +51874,23 @@ class PostgresTodosTaskManifestBackend {
51769
51874
  }
51770
51875
  if (stored.rows.length !== managedIds.length)
51771
51876
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: managed graph is incomplete");
51772
- await tx.query(`UPDATE todos_task_manifest_outbox
51877
+ const cancelled = await tx.query(`UPDATE todos_task_manifest_outbox
51773
51878
  SET status = 'cancelled'
51774
51879
  WHERE apply_receipt_id = $1 AND status = 'pending'
51775
51880
  AND EXISTS (
51776
51881
  SELECT 1 FROM todos_task_manifest_receipts r
51777
51882
  WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
51778
51883
  AND r.tenant_id = $2
51779
- )`, [input.receipt_id, this.tenantId]);
51884
+ AND r.authority = 'todos'
51885
+ AND r.route = 'todos.task-manifest.v1'
51886
+ AND r.schema_version = 1
51887
+ AND r.kind = 'apply'
51888
+ )
51889
+ RETURNING id`, [input.receipt_id, this.tenantId]);
51890
+ const cancelledIds = new Set(cancelled.rows.map((row) => String(row.id)));
51891
+ if (cancelledIds.size !== applyResult.outbox_ids.length || applyResult.outbox_ids.some((id) => !cancelledIds.has(id))) {
51892
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: failed to cancel every expected outbox row");
51893
+ }
51780
51894
  const typedIds = [
51781
51895
  ["dependencies", applyResult.graph.dependency_ids],
51782
51896
  ["comments", applyResult.graph.comment_ids],
@@ -51959,6 +52073,7 @@ class PackageOwnedTodosTaskManifestAuthority {
51959
52073
  deterministic_ids: true,
51960
52074
  immutable_receipts: true,
51961
52075
  transactional_outbox: true,
52076
+ idempotent_outbox_delivery: true,
51962
52077
  exact_bounded_readback: true,
51963
52078
  conditional_compensation: true,
51964
52079
  transcript_safe: false,
package/dist/mcp.js CHANGED
@@ -41,7 +41,7 @@ var __require = import.meta.require;
41
41
  // package.json
42
42
  var package_default = {
43
43
  name: "@hasna/todos",
44
- version: "0.15.15",
44
+ version: "0.15.16",
45
45
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
46
46
  type: "module",
47
47
  main: "dist/index.js",
@@ -12382,7 +12382,7 @@ import { createHash as createHash5 } from "crypto";
12382
12382
  // package.json
12383
12383
  var package_default = {
12384
12384
  name: "@hasna/todos",
12385
- version: "0.15.15",
12385
+ version: "0.15.16",
12386
12386
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12387
12387
  type: "module",
12388
12388
  main: "dist/index.js",
package/dist/registry.js CHANGED
@@ -12379,7 +12379,7 @@ var init_tasks = __esm(() => {
12379
12379
  // package.json
12380
12380
  var package_default = {
12381
12381
  name: "@hasna/todos",
12382
- version: "0.15.15",
12382
+ version: "0.15.16",
12383
12383
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12384
12384
  type: "module",
12385
12385
  main: "dist/index.js",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.15.15",
3
+ "packageVersion": "0.15.16",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "3c9e40955ed30592a313258561b6cc76b928080e",
6
- "gitTree": "372cf75a8993b31e328a995694c2f7a4c49fc63c",
7
- "sourceTreeSha256": "95cf4fe5ee9dff0b651b37d4a499b714112a0b1a2f13a7eb326d5b79d1a3d77b",
8
- "generatedAt": "2026-08-08T18:01:52.000Z"
5
+ "gitCommit": "084685de11895e9c0bbed1ed49111034be5f7abf",
6
+ "gitTree": "abbe0059ad26a5e3d912791a29be4ddf41a5806a",
7
+ "sourceTreeSha256": "b78b8353a1695771896c48af0113c731345a9a15545b984f2778b69cec96eda9",
8
+ "generatedAt": "2026-08-09T02:43:36.000Z"
9
9
  }