@hasna/todos 0.15.14 → 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.
Files changed (45) hide show
  1. package/dist/cli/cloud-router.d.ts.map +1 -1
  2. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  3. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  4. package/dist/cli/index.js +12470 -10747
  5. package/dist/contracts.js +1 -1
  6. package/dist/index.js +358 -47
  7. package/dist/lib/agent-adapter-docs.d.ts +10 -0
  8. package/dist/lib/agent-adapter-docs.d.ts.map +1 -1
  9. package/dist/mcp/index.js +2290 -803
  10. package/dist/mcp.js +1 -1
  11. package/dist/project-registration.js +1 -1
  12. package/dist/registry.js +1 -1
  13. package/dist/release-provenance.json +5 -5
  14. package/dist/sdk/index.d.ts +1 -1
  15. package/dist/sdk/index.d.ts.map +1 -1
  16. package/dist/sdk/index.js +7 -0
  17. package/dist/sdk/v1.generated.d.ts +23 -0
  18. package/dist/sdk/v1.generated.d.ts.map +1 -1
  19. package/dist/server/cloud.d.ts +3 -0
  20. package/dist/server/cloud.d.ts.map +1 -1
  21. package/dist/server/index.js +19806 -18319
  22. package/dist/server/openapi.d.ts +134 -0
  23. package/dist/server/openapi.d.ts.map +1 -1
  24. package/dist/server/v1.d.ts +2 -1
  25. package/dist/server/v1.d.ts.map +1 -1
  26. package/dist/task-manifest/authority.d.ts +3 -1
  27. package/dist/task-manifest/authority.d.ts.map +1 -1
  28. package/dist/task-manifest/backend.d.ts +18 -1
  29. package/dist/task-manifest/backend.d.ts.map +1 -1
  30. package/dist/task-manifest/http.d.ts +2 -1
  31. package/dist/task-manifest/http.d.ts.map +1 -1
  32. package/dist/task-manifest/index.d.ts +2 -2
  33. package/dist/task-manifest/index.d.ts.map +1 -1
  34. package/dist/task-manifest/postgres.d.ts +3 -1
  35. package/dist/task-manifest/postgres.d.ts.map +1 -1
  36. package/dist/task-manifest/schema-sql.d.ts +3 -1
  37. package/dist/task-manifest/schema-sql.d.ts.map +1 -1
  38. package/dist/task-manifest/schema.d.ts +2 -1
  39. package/dist/task-manifest/schema.d.ts.map +1 -1
  40. package/dist/task-manifest/sqlite.d.ts +4 -2
  41. package/dist/task-manifest/sqlite.d.ts.map +1 -1
  42. package/dist/task-manifest/types.d.ts +24 -1
  43. package/dist/task-manifest/types.d.ts.map +1 -1
  44. package/dist/task-manifest.js +357 -46
  45. package/package.json +1 -1
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.14",
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;
@@ -39177,6 +39180,14 @@ var compensationSchema = exports_external.object({
39177
39180
  idempotency_key: identifier,
39178
39181
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
39179
39182
  }).strict();
39183
+ var bindingLookupSchema = exports_external.object({
39184
+ authority: exports_external.string().min(1).max(64),
39185
+ route: exports_external.string().min(1).max(128),
39186
+ schema_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
39187
+ tenant_id: identifier,
39188
+ plan_id: uuid2,
39189
+ max_items: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
39190
+ }).strict();
39180
39191
  function parseTodosTaskManifest(input) {
39181
39192
  const parsed = schema.safeParse(input);
39182
39193
  if (!parsed.success) {
@@ -39242,6 +39253,36 @@ function parseTodosTaskManifestCompensation(input) {
39242
39253
  }
39243
39254
  return parsed.data;
39244
39255
  }
39256
+ function parseTodosTaskManifestBindingLookup(input) {
39257
+ const parsed = bindingLookupSchema.safeParse(input);
39258
+ if (!parsed.success) {
39259
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", `Invalid task-manifest binding lookup: ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, { issues: parsed.error.issues });
39260
+ }
39261
+ return parsed.data;
39262
+ }
39263
+
39264
+ // src/task-manifest/backend.ts
39265
+ var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
39266
+ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
39267
+ if (rows.length === 0) {
39268
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BINDING_NOT_FOUND", `Managed task-manifest binding not found for plan: ${planId}`, { plan_id: planId });
39269
+ }
39270
+ if (rows.length !== 1) {
39271
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest plan lookup matched more than one binding", { plan_id: planId, matched_items: rows.length, max_items: 1 });
39272
+ }
39273
+ const row = rows[0];
39274
+ const bindingVersion = Number(row.binding_version);
39275
+ const state = row.state;
39276
+ if (row.binding_tenant_id !== tenantId || row.receipt_tenant_id !== tenantId || row.binding_plan_id !== planId || row.receipt_plan_id !== planId || row.receipt_authority !== "todos" || row.receipt_route !== "todos.task-manifest.v1" || Number(row.receipt_schema_version) !== 1 || row.receipt_kind !== "apply" || row.binding_operation_id !== row.receipt_operation_id || typeof row.apply_receipt_id !== "string" || !UUID_PATTERN2.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
39277
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
39278
+ }
39279
+ return {
39280
+ plan_id: planId,
39281
+ apply_receipt_id: row.apply_receipt_id,
39282
+ binding_version: bindingVersion,
39283
+ state
39284
+ };
39285
+ }
39245
39286
 
39246
39287
  // src/task-manifest/reference-guard.ts
39247
39288
  function quoteSqliteIdentifier2(value) {
@@ -39331,10 +39372,14 @@ function postgresTaskManifestForeignReferenceSql(tableName) {
39331
39372
  }
39332
39373
 
39333
39374
  // src/task-manifest/schema-sql.ts
39375
+ function sqlString(value) {
39376
+ return `'${value.replaceAll("'", "''")}'`;
39377
+ }
39334
39378
  function sqliteTodosTaskManifestSchemaSql() {
39335
39379
  return `
39336
39380
  CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
39337
39381
  receipt_id TEXT PRIMARY KEY,
39382
+ tenant_id TEXT NOT NULL,
39338
39383
  authority TEXT NOT NULL CHECK(authority = 'todos'),
39339
39384
  route TEXT NOT NULL,
39340
39385
  schema_version INTEGER NOT NULL CHECK(schema_version = 1),
@@ -39352,6 +39397,7 @@ function sqliteTodosTaskManifestSchemaSql() {
39352
39397
  );
39353
39398
  CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
39354
39399
  operation_id TEXT PRIMARY KEY,
39400
+ tenant_id TEXT NOT NULL,
39355
39401
  idempotency_key TEXT NOT NULL UNIQUE,
39356
39402
  request_digest TEXT NOT NULL,
39357
39403
  result_digest TEXT NOT NULL,
@@ -39387,10 +39433,37 @@ function sqliteTodosTaskManifestSchemaSql() {
39387
39433
  END;
39388
39434
  `;
39389
39435
  }
39390
- function postgresTodosTaskManifestSchemaSql() {
39436
+ function sqliteTableHasColumn(db, tableName, columnName) {
39437
+ const columns = db.query(`PRAGMA table_info("${tableName}")`).all();
39438
+ return columns.some((column) => column.name === columnName);
39439
+ }
39440
+ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
39441
+ db.exec(sqliteTodosTaskManifestSchemaSql());
39442
+ const tenantDefault = sqlString(tenantId);
39443
+ for (const tableName of [
39444
+ "todos_task_manifest_receipts",
39445
+ "todos_task_manifest_bindings"
39446
+ ]) {
39447
+ if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
39448
+ db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
39449
+ }
39450
+ }
39451
+ db.exec(`
39452
+ CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
39453
+ ON todos_task_manifest_receipts(tenant_id, receipt_id, kind);
39454
+ CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_bindings_tenant_plan
39455
+ ON todos_task_manifest_bindings(
39456
+ tenant_id,
39457
+ json_extract(result_json, '$.graph.plan_id')
39458
+ );
39459
+ `);
39460
+ }
39461
+ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
39462
+ const tenantDefault = sqlString(tenantId);
39391
39463
  return [
39392
39464
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
39393
39465
  receipt_id text PRIMARY KEY,
39466
+ tenant_id text NOT NULL,
39394
39467
  authority text NOT NULL CHECK(authority = 'todos'),
39395
39468
  route text NOT NULL,
39396
39469
  schema_version integer NOT NULL CHECK(schema_version = 1),
@@ -39406,8 +39479,13 @@ function postgresTodosTaskManifestSchemaSql() {
39406
39479
  created_at timestamptz NOT NULL,
39407
39480
  UNIQUE(kind, idempotency_key)
39408
39481
  )`,
39482
+ `ALTER TABLE todos_task_manifest_receipts
39483
+ ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
39484
+ `ALTER TABLE todos_task_manifest_receipts
39485
+ ALTER COLUMN tenant_id DROP DEFAULT`,
39409
39486
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
39410
39487
  operation_id text PRIMARY KEY,
39488
+ tenant_id text NOT NULL,
39411
39489
  idempotency_key text NOT NULL UNIQUE,
39412
39490
  request_digest text NOT NULL,
39413
39491
  result_digest text NOT NULL,
@@ -39420,6 +39498,10 @@ function postgresTodosTaskManifestSchemaSql() {
39420
39498
  created_at timestamptz NOT NULL,
39421
39499
  updated_at timestamptz NOT NULL
39422
39500
  )`,
39501
+ `ALTER TABLE todos_task_manifest_bindings
39502
+ ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
39503
+ `ALTER TABLE todos_task_manifest_bindings
39504
+ ALTER COLUMN tenant_id DROP DEFAULT`,
39423
39505
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
39424
39506
  id text PRIMARY KEY,
39425
39507
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -39433,6 +39515,13 @@ function postgresTodosTaskManifestSchemaSql() {
39433
39515
  )`,
39434
39516
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
39435
39517
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
39518
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
39519
+ ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
39520
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
39521
+ ON todos_task_manifest_bindings(
39522
+ tenant_id,
39523
+ ((result_json #>> '{graph,plan_id}'))
39524
+ )`,
39436
39525
  `CREATE OR REPLACE FUNCTION todos_task_manifest_receipts_immutable()
39437
39526
  RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
39438
39527
  RAISE EXCEPTION 'todos task manifest receipts are immutable';
@@ -39456,10 +39545,12 @@ function parseApplyResult(value, duplicate) {
39456
39545
 
39457
39546
  class SqliteTodosTaskManifestBackend {
39458
39547
  db;
39548
+ tenantId;
39459
39549
  kind = "sqlite";
39460
- constructor(db) {
39550
+ constructor(db, tenantId = "default") {
39461
39551
  this.db = db;
39462
- db.exec(sqliteTodosTaskManifestSchemaSql());
39552
+ this.tenantId = tenantId;
39553
+ ensureSqliteTodosTaskManifestSchema(db, tenantId);
39463
39554
  }
39464
39555
  async serialized(run) {
39465
39556
  const previous = sqliteTails.get(this.db) ?? Promise.resolve();
@@ -39489,7 +39580,7 @@ class SqliteTodosTaskManifestBackend {
39489
39580
  async apply(input, faults) {
39490
39581
  return this.serialized(() => {
39491
39582
  const { manifest } = input;
39492
- const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = ? LIMIT 1").get(manifest.operation_id);
39583
+ const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = ? AND operation_id = ? LIMIT 1").get(this.tenantId, manifest.operation_id);
39493
39584
  if (binding) {
39494
39585
  if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
39495
39586
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
@@ -39499,7 +39590,7 @@ class SqliteTodosTaskManifestBackend {
39499
39590
  }
39500
39591
  return parseApplyResult(String(binding["result_json"]), true);
39501
39592
  }
39502
- const idempotency = this.db.query("SELECT operation_id, request_digest FROM todos_task_manifest_bindings WHERE idempotency_key = ? LIMIT 1").get(manifest.idempotency_key);
39593
+ const idempotency = this.db.query("SELECT operation_id, request_digest FROM todos_task_manifest_bindings WHERE tenant_id = ? AND idempotency_key = ? LIMIT 1").get(this.tenantId, manifest.idempotency_key);
39503
39594
  if (idempotency)
39504
39595
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used by another operation");
39505
39596
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
@@ -39589,9 +39680,9 @@ class SqliteTodosTaskManifestBackend {
39589
39680
  const resultJson = canonicalJson(result);
39590
39681
  const manifestJson = canonicalJson(manifest);
39591
39682
  this.db.query(`INSERT INTO todos_task_manifest_receipts (
39592
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
39683
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
39593
39684
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
39594
- ) VALUES (?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, 1, NULL, ?, ?, ?)`).run(input.receipt_id, manifest.operation_id, manifest.idempotency_key, input.request_digest, input.result_digest, manifestJson, resultJson, input.now);
39685
+ ) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, 1, NULL, ?, ?, ?)`).run(input.receipt_id, this.tenantId, manifest.operation_id, manifest.idempotency_key, input.request_digest, input.result_digest, manifestJson, resultJson, input.now);
39595
39686
  for (const entry2 of input.outbox) {
39596
39687
  this.db.query(`INSERT INTO todos_task_manifest_outbox (
39597
39688
  id, apply_receipt_id, topic, payload, payload_digest, status, created_at
@@ -39599,49 +39690,102 @@ class SqliteTodosTaskManifestBackend {
39599
39690
  }
39600
39691
  fault(faults, "after_outbox_write");
39601
39692
  this.db.query(`INSERT INTO todos_task_manifest_bindings (
39602
- operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
39693
+ operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
39603
39694
  manifest_json, result_json, state, version, created_at, updated_at
39604
- ) VALUES (?, ?, ?, ?, ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, manifest.idempotency_key, input.request_digest, input.result_digest, input.receipt_id, manifestJson, resultJson, input.now, input.now);
39695
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, this.tenantId, manifest.idempotency_key, input.request_digest, input.result_digest, input.receipt_id, manifestJson, resultJson, input.now, input.now);
39605
39696
  fault(faults, "after_receipt_write");
39606
39697
  return result;
39607
39698
  });
39608
39699
  }
39609
39700
  async readExact(receiptId2) {
39610
- const row = this.db.query("SELECT result_json FROM todos_task_manifest_receipts WHERE receipt_id = ? AND kind = 'apply' LIMIT 1").get(receiptId2);
39701
+ const row = this.db.query("SELECT result_json FROM todos_task_manifest_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, receiptId2);
39611
39702
  if (!row)
39612
39703
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
39613
39704
  return parseApplyResult(row.result_json, false);
39614
39705
  }
39706
+ async lookupBindingByPlanId(planId) {
39707
+ const rows = this.db.query(`
39708
+ SELECT
39709
+ b.apply_receipt_id AS apply_receipt_id,
39710
+ b.state AS state,
39711
+ b.version AS binding_version,
39712
+ b.tenant_id AS binding_tenant_id,
39713
+ b.operation_id AS binding_operation_id,
39714
+ json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
39715
+ r.tenant_id AS receipt_tenant_id,
39716
+ r.authority AS receipt_authority,
39717
+ r.route AS receipt_route,
39718
+ r.schema_version AS receipt_schema_version,
39719
+ r.kind AS receipt_kind,
39720
+ r.operation_id AS receipt_operation_id,
39721
+ json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
39722
+ FROM todos_task_manifest_bindings b
39723
+ LEFT JOIN todos_task_manifest_receipts r
39724
+ ON r.receipt_id = b.apply_receipt_id
39725
+ AND r.tenant_id = b.tenant_id
39726
+ WHERE b.tenant_id = ?
39727
+ AND json_extract(b.result_json, '$.graph.plan_id') = ?
39728
+ LIMIT 2
39729
+ `).all(this.tenantId, planId);
39730
+ return validateTaskManifestBindingLookupRows(rows, this.tenantId, planId);
39731
+ }
39615
39732
  async markOutboxDelivered(outboxId, deliveredAt) {
39616
39733
  await this.serialized(() => {
39617
39734
  const result = this.db.query(`UPDATE todos_task_manifest_outbox
39618
39735
  SET status = 'delivered', delivered_at = ?, attempts = attempts + 1
39619
- WHERE id = ? AND status = 'pending'`).run(deliveredAt, outboxId);
39620
- if (result.changes !== 1) {
39621
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
39622
- }
39736
+ WHERE id = ? AND status = 'pending'
39737
+ AND EXISTS (
39738
+ SELECT 1 FROM todos_task_manifest_receipts r
39739
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
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'
39745
+ )`).run(deliveredAt, outboxId, this.tenantId);
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}`);
39623
39762
  });
39624
39763
  }
39625
39764
  async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
39626
39765
  return this.serialized(() => {
39627
39766
  const existing = this.db.query(`SELECT apply_receipt_id, request_digest, result_json
39628
- FROM todos_task_manifest_receipts WHERE kind = 'compensate' AND idempotency_key = ? LIMIT 1`).get(input.idempotency_key);
39767
+ FROM todos_task_manifest_receipts
39768
+ WHERE tenant_id = ? AND kind = 'compensate' AND idempotency_key = ?
39769
+ LIMIT 1`).get(this.tenantId, input.idempotency_key);
39629
39770
  if (existing) {
39630
39771
  if (existing.apply_receipt_id !== input.receipt_id || existing.request_digest !== requestDigest) {
39631
39772
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation idempotency key is already used");
39632
39773
  }
39633
39774
  return { ...JSON.parse(existing.result_json), duplicate: true };
39634
39775
  }
39635
- const row = this.db.query("SELECT * FROM todos_task_manifest_receipts WHERE receipt_id = ? AND kind = 'apply' LIMIT 1").get(input.receipt_id);
39776
+ const row = this.db.query("SELECT * FROM todos_task_manifest_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, input.receipt_id);
39636
39777
  if (!row)
39637
39778
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
39638
- const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = ? LIMIT 1").get(String(row["operation_id"]));
39779
+ const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = ? AND operation_id = ? LIMIT 1").get(this.tenantId, String(row["operation_id"]));
39639
39780
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
39640
39781
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
39641
39782
  }
39642
39783
  if (binding["state"] !== "applied")
39643
39784
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
39644
- const delivered = this.db.query("SELECT id FROM todos_task_manifest_outbox WHERE apply_receipt_id = ? AND status = 'delivered' LIMIT 1").get(input.receipt_id);
39785
+ const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
39786
+ JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
39787
+ WHERE r.tenant_id = ? AND o.apply_receipt_id = ? AND o.status = 'delivered'
39788
+ LIMIT 1`).get(this.tenantId, input.receipt_id);
39645
39789
  if (delivered)
39646
39790
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
39647
39791
  const applyResult = parseApplyResult(String(row["result_json"]), false);
@@ -39654,7 +39798,14 @@ class SqliteTodosTaskManifestBackend {
39654
39798
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
39655
39799
  ];
39656
39800
  const storedOutbox = this.db.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
39657
- FROM todos_task_manifest_outbox WHERE apply_receipt_id = ? ORDER BY id`).all(input.receipt_id);
39801
+ FROM todos_task_manifest_outbox
39802
+ WHERE apply_receipt_id = ?
39803
+ AND EXISTS (
39804
+ SELECT 1 FROM todos_task_manifest_receipts r
39805
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
39806
+ AND r.tenant_id = ?
39807
+ )
39808
+ ORDER BY id`).all(input.receipt_id, this.tenantId);
39658
39809
  if (storedOutbox.length !== expectedEffects.length) {
39659
39810
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
39660
39811
  }
@@ -39742,7 +39893,14 @@ class SqliteTodosTaskManifestBackend {
39742
39893
  }
39743
39894
  expectedVerificationIndex += 1;
39744
39895
  }
39745
- this.db.query("UPDATE todos_task_manifest_outbox SET status = 'cancelled' WHERE apply_receipt_id = ? AND status = 'pending'").run(input.receipt_id);
39896
+ this.db.query(`UPDATE todos_task_manifest_outbox
39897
+ SET status = 'cancelled'
39898
+ WHERE apply_receipt_id = ? AND status = 'pending'
39899
+ AND EXISTS (
39900
+ SELECT 1 FROM todos_task_manifest_receipts r
39901
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
39902
+ AND r.tenant_id = ?
39903
+ )`).run(input.receipt_id, this.tenantId);
39746
39904
  for (const table of ["task_tags", "task_dependencies", "task_comments", "task_verifications"]) {
39747
39905
  const column = table === "task_dependencies" ? "task_id" : "task_id";
39748
39906
  this.db.query(`DELETE FROM ${table} WHERE ${column} IN (${placeholders2})`).run(...taskIds);
@@ -39753,11 +39911,11 @@ class SqliteTodosTaskManifestBackend {
39753
39911
  const result = { duplicate: false, receipt, absent: true, readback };
39754
39912
  const resultJson = canonicalJson(result);
39755
39913
  this.db.query(`INSERT INTO todos_task_manifest_receipts (
39756
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
39914
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
39757
39915
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
39758
- ) VALUES (?, 'todos', 'todos.task-manifest.v1', 1, 'compensate', ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(compensationReceiptId, receipt.operation_id, input.idempotency_key, requestDigest, receipt.result_digest, receipt.binding_version, input.receipt_id, resultJson, now4);
39916
+ ) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'compensate', ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(compensationReceiptId, this.tenantId, receipt.operation_id, input.idempotency_key, requestDigest, receipt.result_digest, receipt.binding_version, input.receipt_id, resultJson, now4);
39759
39917
  const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
39760
- WHERE operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now4, receipt.operation_id, input.if_binding_version);
39918
+ WHERE tenant_id = ? AND operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now4, this.tenantId, receipt.operation_id, input.if_binding_version);
39761
39919
  if (updated.changes !== 1) {
39762
39920
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding changed during compensation");
39763
39921
  }
@@ -39896,17 +40054,19 @@ class PostgresTodosTaskManifestBackend {
39896
40054
  kind = "postgresql";
39897
40055
  service;
39898
40056
  tableName;
40057
+ tenantId;
39899
40058
  schemaReady = null;
39900
40059
  constructor(client, options = {}) {
39901
40060
  this.client = client;
39902
40061
  this.service = options.service ?? "todos";
39903
40062
  this.tableName = safeIdentifier2(options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, "tableName");
40063
+ this.tenantId = options.tenantId ?? "default";
39904
40064
  }
39905
40065
  async ensureSchema() {
39906
40066
  this.schemaReady ??= (async () => {
39907
40067
  for (const sql of postgresTodosSyncSchemaSql(this.tableName))
39908
40068
  await this.client.query(sql);
39909
- for (const sql of postgresTodosTaskManifestSchemaSql())
40069
+ for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
39910
40070
  await this.client.query(sql);
39911
40071
  })();
39912
40072
  await this.schemaReady;
@@ -39928,7 +40088,7 @@ class PostgresTodosTaskManifestBackend {
39928
40088
  const { manifest } = input;
39929
40089
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
39930
40090
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
39931
- const existing = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = $1 LIMIT 1 FOR UPDATE", [manifest.operation_id]);
40091
+ const existing = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND operation_id = $2 LIMIT 1 FOR UPDATE", [this.tenantId, manifest.operation_id]);
39932
40092
  if (existing.rows[0]) {
39933
40093
  const binding = existing.rows[0];
39934
40094
  if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
@@ -39939,7 +40099,7 @@ class PostgresTodosTaskManifestBackend {
39939
40099
  }
39940
40100
  return { ...parseJson2(binding["result_json"]), duplicate: true };
39941
40101
  }
39942
- const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE idempotency_key = $1 LIMIT 1", [manifest.idempotency_key]);
40102
+ const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND idempotency_key = $2 LIMIT 1", [this.tenantId, manifest.idempotency_key]);
39943
40103
  if (reused.rows[0])
39944
40104
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
39945
40105
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
@@ -40049,10 +40209,11 @@ class PostgresTodosTaskManifestBackend {
40049
40209
  const manifestJson = canonicalJson(manifest);
40050
40210
  const resultJson = canonicalJson(result);
40051
40211
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
40052
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
40212
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
40053
40213
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
40054
- ) VALUES ($1, 'todos', 'todos.task-manifest.v1', 1, 'apply', $2, $3, $4, $5, 1, NULL, $6::jsonb, $7::jsonb, $8)`, [
40214
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, 1, NULL, $7::jsonb, $8::jsonb, $9)`, [
40055
40215
  input.receipt_id,
40216
+ this.tenantId,
40056
40217
  manifest.operation_id,
40057
40218
  manifest.idempotency_key,
40058
40219
  input.request_digest,
@@ -40075,10 +40236,11 @@ class PostgresTodosTaskManifestBackend {
40075
40236
  }
40076
40237
  fault2(faults, "after_outbox_write");
40077
40238
  await tx.query(`INSERT INTO todos_task_manifest_bindings (
40078
- operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
40239
+ operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
40079
40240
  manifest_json, result_json, state, version, created_at, updated_at
40080
- ) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, 'applied', 1, $8, $8)`, [
40241
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
40081
40242
  manifest.operation_id,
40243
+ this.tenantId,
40082
40244
  manifest.idempotency_key,
40083
40245
  input.request_digest,
40084
40246
  input.result_digest,
@@ -40093,19 +40255,86 @@ class PostgresTodosTaskManifestBackend {
40093
40255
  }
40094
40256
  async readExact(receiptId2) {
40095
40257
  await this.ensureSchema();
40096
- const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE receipt_id = $1 AND kind = 'apply' LIMIT 1", [receiptId2]);
40258
+ const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, receiptId2]);
40097
40259
  if (!result.rows[0])
40098
40260
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
40099
40261
  return { ...parseJson2(result.rows[0]["result_json"]), duplicate: false };
40100
40262
  }
40263
+ async lookupBindingByPlanId(planId) {
40264
+ await this.ensureSchema();
40265
+ const result = await this.client.query(`
40266
+ SELECT
40267
+ b.apply_receipt_id AS apply_receipt_id,
40268
+ b.state AS state,
40269
+ b.version AS binding_version,
40270
+ b.tenant_id AS binding_tenant_id,
40271
+ b.operation_id AS binding_operation_id,
40272
+ b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
40273
+ r.tenant_id AS receipt_tenant_id,
40274
+ r.authority AS receipt_authority,
40275
+ r.route AS receipt_route,
40276
+ r.schema_version AS receipt_schema_version,
40277
+ r.kind AS receipt_kind,
40278
+ r.operation_id AS receipt_operation_id,
40279
+ r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
40280
+ FROM todos_task_manifest_bindings b
40281
+ LEFT JOIN todos_task_manifest_receipts r
40282
+ ON r.receipt_id = b.apply_receipt_id
40283
+ AND r.tenant_id = b.tenant_id
40284
+ WHERE b.tenant_id = $1
40285
+ AND b.result_json #>> '{graph,plan_id}' = $2
40286
+ LIMIT 2
40287
+ `, [this.tenantId, planId]);
40288
+ return validateTaskManifestBindingLookupRows(result.rows, this.tenantId, planId);
40289
+ }
40101
40290
  async markOutboxDelivered(outboxId, deliveredAt) {
40102
40291
  await this.ensureSchema();
40103
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)}`]);
40104
40309
  const result = await tx.query(`UPDATE todos_task_manifest_outbox
40105
40310
  SET status = 'delivered', delivered_at = $1, attempts = attempts + 1
40106
- WHERE id = $2 AND status = 'pending' RETURNING id`, [deliveredAt, outboxId]);
40107
- if (!result.rows[0])
40108
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
40311
+ WHERE id = $2 AND status = 'pending'
40312
+ AND EXISTS (
40313
+ SELECT 1 FROM todos_task_manifest_receipts r
40314
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
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'
40320
+ )
40321
+ RETURNING id`, [deliveredAt, outboxId, this.tenantId]);
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}`);
40109
40338
  });
40110
40339
  }
40111
40340
  async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
@@ -40113,7 +40342,10 @@ class PostgresTodosTaskManifestBackend {
40113
40342
  return this.client.transaction(async (tx) => {
40114
40343
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${receipt.operation_id}`]);
40115
40344
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fcompensation-idempotency\x1F${input.idempotency_key}`]);
40116
- const previous = await tx.query("SELECT apply_receipt_id, request_digest, result_json FROM todos_task_manifest_receipts WHERE kind = 'compensate' AND idempotency_key = $1 LIMIT 1", [input.idempotency_key]);
40345
+ const previous = await tx.query(`SELECT apply_receipt_id, request_digest, result_json
40346
+ FROM todos_task_manifest_receipts
40347
+ WHERE tenant_id = $1 AND kind = 'compensate' AND idempotency_key = $2
40348
+ LIMIT 1`, [this.tenantId, input.idempotency_key]);
40117
40349
  if (previous.rows[0]) {
40118
40350
  const row = previous.rows[0];
40119
40351
  if (row["apply_receipt_id"] !== input.receipt_id || row["request_digest"] !== requestDigest) {
@@ -40121,18 +40353,27 @@ class PostgresTodosTaskManifestBackend {
40121
40353
  }
40122
40354
  return { ...parseJson2(row["result_json"]), duplicate: true };
40123
40355
  }
40124
- const applyRows = await tx.query("SELECT * FROM todos_task_manifest_receipts WHERE receipt_id = $1 AND kind = 'apply' LIMIT 1", [input.receipt_id]);
40356
+ const applyRows = await tx.query("SELECT * FROM todos_task_manifest_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, input.receipt_id]);
40125
40357
  const applyRow = applyRows.rows[0];
40126
40358
  if (!applyRow)
40127
40359
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
40128
- const bindingRows = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = $1 LIMIT 1 FOR UPDATE", [receipt.operation_id]);
40360
+ const bindingRows = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND operation_id = $2 LIMIT 1 FOR UPDATE", [this.tenantId, receipt.operation_id]);
40129
40361
  const binding = bindingRows.rows[0];
40130
40362
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
40131
40363
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
40132
40364
  }
40133
40365
  if (binding["state"] !== "applied")
40134
40366
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
40135
- const delivered = await tx.query("SELECT id FROM todos_task_manifest_outbox WHERE apply_receipt_id = $1 AND status = 'delivered' LIMIT 1", [input.receipt_id]);
40367
+ const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
40368
+ JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
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'
40376
+ LIMIT 1`, [this.tenantId, input.receipt_id]);
40136
40377
  if (delivered.rows[0])
40137
40378
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
40138
40379
  const applyResult = parseJson2(applyRow["result_json"]);
@@ -40144,8 +40385,20 @@ class PostgresTodosTaskManifestBackend {
40144
40385
  },
40145
40386
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
40146
40387
  ];
40147
- const outboxRows = await tx.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
40148
- FROM todos_task_manifest_outbox WHERE apply_receipt_id = $1 ORDER BY id`, [input.receipt_id]);
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
40391
+ AND EXISTS (
40392
+ SELECT 1 FROM todos_task_manifest_receipts r
40393
+ WHERE r.receipt_id = o.apply_receipt_id
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'
40399
+ )
40400
+ ORDER BY o.id
40401
+ FOR UPDATE OF o`, [input.receipt_id, this.tenantId]);
40149
40402
  if (outboxRows.rows.length !== expectedEffects.length) {
40150
40403
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
40151
40404
  }
@@ -40245,7 +40498,23 @@ class PostgresTodosTaskManifestBackend {
40245
40498
  }
40246
40499
  if (stored.rows.length !== managedIds.length)
40247
40500
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: managed graph is incomplete");
40248
- await tx.query("UPDATE todos_task_manifest_outbox SET status = 'cancelled' WHERE apply_receipt_id = $1 AND status = 'pending'", [input.receipt_id]);
40501
+ const cancelled = await tx.query(`UPDATE todos_task_manifest_outbox
40502
+ SET status = 'cancelled'
40503
+ WHERE apply_receipt_id = $1 AND status = 'pending'
40504
+ AND EXISTS (
40505
+ SELECT 1 FROM todos_task_manifest_receipts r
40506
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
40507
+ AND r.tenant_id = $2
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
+ }
40249
40518
  const typedIds = [
40250
40519
  ["dependencies", applyResult.graph.dependency_ids],
40251
40520
  ["comments", applyResult.graph.comment_ids],
@@ -40262,10 +40531,11 @@ class PostgresTodosTaskManifestBackend {
40262
40531
  const readback = await this.readback(tx, applyResult.graph);
40263
40532
  const result = { duplicate: false, receipt, absent: true, readback };
40264
40533
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
40265
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
40534
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
40266
40535
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
40267
- ) VALUES ($1, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $2, $3, $4, $5, $6, $7, NULL, $8::jsonb, $9)`, [
40536
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10)`, [
40268
40537
  compensationReceiptId,
40538
+ this.tenantId,
40269
40539
  receipt.operation_id,
40270
40540
  input.idempotency_key,
40271
40541
  requestDigest,
@@ -40277,10 +40547,11 @@ class PostgresTodosTaskManifestBackend {
40277
40547
  ]);
40278
40548
  const updated = await tx.query(`UPDATE todos_task_manifest_bindings
40279
40549
  SET state = 'compensated', version = $1, compensation_receipt_id = $2, updated_at = $3
40280
- WHERE operation_id = $4 AND state = 'applied' AND version = $5 RETURNING operation_id`, [
40550
+ WHERE tenant_id = $4 AND operation_id = $5 AND state = 'applied' AND version = $6 RETURNING operation_id`, [
40281
40551
  receipt.binding_version,
40282
40552
  compensationReceiptId,
40283
40553
  now4,
40554
+ this.tenantId,
40284
40555
  receipt.operation_id,
40285
40556
  input.if_binding_version
40286
40557
  ]);
@@ -40320,6 +40591,13 @@ var FAULT_POINTS = [
40320
40591
  "after_outbox_write",
40321
40592
  "after_receipt_write"
40322
40593
  ];
40594
+ function resolveTenantId(value) {
40595
+ const tenantId = value ?? "default";
40596
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenantId)) {
40597
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
40598
+ }
40599
+ return tenantId;
40600
+ }
40323
40601
  function normalize(input, now4) {
40324
40602
  const parsed = parseTodosTaskManifest(input);
40325
40603
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
@@ -40407,19 +40685,23 @@ function sanitizeManifest(manifest) {
40407
40685
  class PackageOwnedTodosTaskManifestAuthority {
40408
40686
  backend;
40409
40687
  options;
40688
+ tenantId;
40410
40689
  constructor(backend, options = {}) {
40411
40690
  this.backend = backend;
40412
40691
  this.options = options;
40692
+ this.tenantId = resolveTenantId(options.tenantId);
40413
40693
  }
40414
40694
  async capability() {
40415
40695
  return {
40416
40696
  authority: "todos",
40417
40697
  route: TODOS_TASK_MANIFEST_ROUTE,
40418
40698
  schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
40699
+ tenant_id: this.tenantId,
40419
40700
  backend: this.backend.kind,
40420
40701
  deterministic_ids: true,
40421
40702
  immutable_receipts: true,
40422
40703
  transactional_outbox: true,
40704
+ idempotent_outbox_delivery: true,
40423
40705
  exact_bounded_readback: true,
40424
40706
  conditional_compensation: true,
40425
40707
  transcript_safe: false,
@@ -40454,6 +40736,22 @@ class PackageOwnedTodosTaskManifestAuthority {
40454
40736
  }
40455
40737
  return this.backend.readExact(receiptId2).then((result) => this.bounded(result));
40456
40738
  }
40739
+ async lookupBinding(input) {
40740
+ const request = parseTodosTaskManifestBindingLookup(input);
40741
+ if (request.max_items !== 1) {
40742
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BOUNDS_EXCEEDED", "max_items must be exactly 1 for task-manifest binding lookup", { max_items: request.max_items, max_items_limit: 1 });
40743
+ }
40744
+ if (request.authority !== "todos" || request.route !== TODOS_TASK_MANIFEST_ROUTE || request.schema_version !== TODOS_TASK_MANIFEST_SCHEMA_VERSION || request.tenant_id !== this.tenantId) {
40745
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAPABILITY_MISMATCH", "Task-manifest binding lookup does not match this authority identity");
40746
+ }
40747
+ return this.bounded({
40748
+ authority: "todos",
40749
+ route: TODOS_TASK_MANIFEST_ROUTE,
40750
+ schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
40751
+ tenant_id: this.tenantId,
40752
+ ...await this.backend.lookupBindingByPlanId(request.plan_id)
40753
+ });
40754
+ }
40457
40755
  markOutboxDelivered(outboxId) {
40458
40756
  if (!outboxId || outboxId.length > 200) {
40459
40757
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "outboxId must be a bounded exact identifier");
@@ -40493,13 +40791,15 @@ function createSqliteTodosTaskManifestAuthority(options) {
40493
40791
  if (!options?.database) {
40494
40792
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An explicit SQLite Database is required");
40495
40793
  }
40496
- return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database), options);
40794
+ const tenantId = resolveTenantId(options.tenantId);
40795
+ return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database, tenantId), { ...options, tenantId });
40497
40796
  }
40498
40797
  function createPostgresTodosTaskManifestAuthority(client, options = {}) {
40499
40798
  if (!client || typeof client.transaction !== "function") {
40500
40799
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An authoritative PostgreSQL transaction(callback) client is required");
40501
40800
  }
40502
- return new PackageOwnedTodosTaskManifestAuthority(new PostgresTodosTaskManifestBackend(client, options), options);
40801
+ const tenantId = resolveTenantId(options.tenantId);
40802
+ return new PackageOwnedTodosTaskManifestAuthority(new PostgresTodosTaskManifestBackend(client, { ...options, tenantId }), { ...options, tenantId });
40503
40803
  }
40504
40804
  // src/task-manifest/http.ts
40505
40805
  var JSON_HEADERS2 = { "Content-Type": "application/json" };
@@ -40513,6 +40813,7 @@ function status(error) {
40513
40813
  case "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE":
40514
40814
  return 400;
40515
40815
  case "TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND":
40816
+ case "TODOS_TASK_MANIFEST_BINDING_NOT_FOUND":
40516
40817
  return 404;
40517
40818
  case "TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE":
40518
40819
  return 503;
@@ -40575,6 +40876,11 @@ async function handleTodosTaskManifestHttpRequest(request, url, authority, baseP
40575
40876
  }
40576
40877
  return json2({ result: await authority.readExact(input.receipt_id) });
40577
40878
  }
40879
+ if (action === "bindings/lookup") {
40880
+ return json2({
40881
+ result: await authority.lookupBinding(await body(request))
40882
+ });
40883
+ }
40578
40884
  if (action === "compensate") {
40579
40885
  return json2({ result: await authority.compensate(await body(request)) }, 201);
40580
40886
  }
@@ -40634,6 +40940,9 @@ class TodosTaskManifestHttpClient {
40634
40940
  async readExact(receiptId2) {
40635
40941
  return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId2 }) })).result;
40636
40942
  }
40943
+ async lookupBinding(input) {
40944
+ return (await this.request("/bindings/lookup", { method: "POST", body: JSON.stringify(input) })).result;
40945
+ }
40637
40946
  async markOutboxDelivered(outboxId) {
40638
40947
  await this.request("/outbox/delivered", { method: "POST", body: JSON.stringify({ outbox_id: outboxId }) });
40639
40948
  }
@@ -61774,6 +62083,7 @@ export {
61774
62083
  syncWithAgents,
61775
62084
  syncWithAgent,
61776
62085
  syncKgEdges,
62086
+ supportsIdempotentOutboxDelivery,
61777
62087
  supersedeDecisionRecord,
61778
62088
  summarizeTaskRun,
61779
62089
  summarizeRoadmap,
@@ -61987,6 +62297,7 @@ export {
61987
62297
  pauseFocusSession,
61988
62298
  patrolTasks,
61989
62299
  parseTodosTaskManifestCompensation,
62300
+ parseTodosTaskManifestBindingLookup,
61990
62301
  parseTodosTaskManifest,
61991
62302
  parseTmuxTarget,
61992
62303
  parseStorageMode,