@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
@@ -4835,6 +4835,9 @@ init_types();
4835
4835
  // src/task-manifest/types.ts
4836
4836
  var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1";
4837
4837
  var TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1;
4838
+ function supportsIdempotentOutboxDelivery(capability) {
4839
+ return capability !== null && typeof capability === "object" && capability["idempotent_outbox_delivery"] === true;
4840
+ }
4838
4841
 
4839
4842
  class TodosTaskManifestError extends Error {
4840
4843
  code;
@@ -4922,6 +4925,14 @@ var compensationSchema = exports_external.object({
4922
4925
  idempotency_key: identifier,
4923
4926
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
4924
4927
  }).strict();
4928
+ var bindingLookupSchema = exports_external.object({
4929
+ authority: exports_external.string().min(1).max(64),
4930
+ route: exports_external.string().min(1).max(128),
4931
+ schema_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
4932
+ tenant_id: identifier,
4933
+ plan_id: uuid,
4934
+ max_items: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
4935
+ }).strict();
4925
4936
  function parseTodosTaskManifest(input) {
4926
4937
  const parsed = schema.safeParse(input);
4927
4938
  if (!parsed.success) {
@@ -4987,6 +4998,36 @@ function parseTodosTaskManifestCompensation(input) {
4987
4998
  }
4988
4999
  return parsed.data;
4989
5000
  }
5001
+ function parseTodosTaskManifestBindingLookup(input) {
5002
+ const parsed = bindingLookupSchema.safeParse(input);
5003
+ if (!parsed.success) {
5004
+ 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 });
5005
+ }
5006
+ return parsed.data;
5007
+ }
5008
+
5009
+ // src/task-manifest/backend.ts
5010
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
5011
+ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
5012
+ if (rows.length === 0) {
5013
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BINDING_NOT_FOUND", `Managed task-manifest binding not found for plan: ${planId}`, { plan_id: planId });
5014
+ }
5015
+ if (rows.length !== 1) {
5016
+ 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 });
5017
+ }
5018
+ const row = rows[0];
5019
+ const bindingVersion = Number(row.binding_version);
5020
+ const state = row.state;
5021
+ 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_PATTERN.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
5022
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
5023
+ }
5024
+ return {
5025
+ plan_id: planId,
5026
+ apply_receipt_id: row.apply_receipt_id,
5027
+ binding_version: bindingVersion,
5028
+ state
5029
+ };
5030
+ }
4990
5031
 
4991
5032
  // src/task-manifest/reference-guard.ts
4992
5033
  function quoteSqliteIdentifier(value) {
@@ -5076,10 +5117,14 @@ function postgresTaskManifestForeignReferenceSql(tableName) {
5076
5117
  }
5077
5118
 
5078
5119
  // src/task-manifest/schema-sql.ts
5120
+ function sqlString(value) {
5121
+ return `'${value.replaceAll("'", "''")}'`;
5122
+ }
5079
5123
  function sqliteTodosTaskManifestSchemaSql() {
5080
5124
  return `
5081
5125
  CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
5082
5126
  receipt_id TEXT PRIMARY KEY,
5127
+ tenant_id TEXT NOT NULL,
5083
5128
  authority TEXT NOT NULL CHECK(authority = 'todos'),
5084
5129
  route TEXT NOT NULL,
5085
5130
  schema_version INTEGER NOT NULL CHECK(schema_version = 1),
@@ -5097,6 +5142,7 @@ function sqliteTodosTaskManifestSchemaSql() {
5097
5142
  );
5098
5143
  CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
5099
5144
  operation_id TEXT PRIMARY KEY,
5145
+ tenant_id TEXT NOT NULL,
5100
5146
  idempotency_key TEXT NOT NULL UNIQUE,
5101
5147
  request_digest TEXT NOT NULL,
5102
5148
  result_digest TEXT NOT NULL,
@@ -5132,10 +5178,37 @@ function sqliteTodosTaskManifestSchemaSql() {
5132
5178
  END;
5133
5179
  `;
5134
5180
  }
5135
- function postgresTodosTaskManifestSchemaSql() {
5181
+ function sqliteTableHasColumn(db, tableName, columnName) {
5182
+ const columns = db.query(`PRAGMA table_info("${tableName}")`).all();
5183
+ return columns.some((column) => column.name === columnName);
5184
+ }
5185
+ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
5186
+ db.exec(sqliteTodosTaskManifestSchemaSql());
5187
+ const tenantDefault = sqlString(tenantId);
5188
+ for (const tableName of [
5189
+ "todos_task_manifest_receipts",
5190
+ "todos_task_manifest_bindings"
5191
+ ]) {
5192
+ if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
5193
+ db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
5194
+ }
5195
+ }
5196
+ db.exec(`
5197
+ CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
5198
+ ON todos_task_manifest_receipts(tenant_id, receipt_id, kind);
5199
+ CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_bindings_tenant_plan
5200
+ ON todos_task_manifest_bindings(
5201
+ tenant_id,
5202
+ json_extract(result_json, '$.graph.plan_id')
5203
+ );
5204
+ `);
5205
+ }
5206
+ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
5207
+ const tenantDefault = sqlString(tenantId);
5136
5208
  return [
5137
5209
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
5138
5210
  receipt_id text PRIMARY KEY,
5211
+ tenant_id text NOT NULL,
5139
5212
  authority text NOT NULL CHECK(authority = 'todos'),
5140
5213
  route text NOT NULL,
5141
5214
  schema_version integer NOT NULL CHECK(schema_version = 1),
@@ -5151,8 +5224,13 @@ function postgresTodosTaskManifestSchemaSql() {
5151
5224
  created_at timestamptz NOT NULL,
5152
5225
  UNIQUE(kind, idempotency_key)
5153
5226
  )`,
5227
+ `ALTER TABLE todos_task_manifest_receipts
5228
+ ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
5229
+ `ALTER TABLE todos_task_manifest_receipts
5230
+ ALTER COLUMN tenant_id DROP DEFAULT`,
5154
5231
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
5155
5232
  operation_id text PRIMARY KEY,
5233
+ tenant_id text NOT NULL,
5156
5234
  idempotency_key text NOT NULL UNIQUE,
5157
5235
  request_digest text NOT NULL,
5158
5236
  result_digest text NOT NULL,
@@ -5165,6 +5243,10 @@ function postgresTodosTaskManifestSchemaSql() {
5165
5243
  created_at timestamptz NOT NULL,
5166
5244
  updated_at timestamptz NOT NULL
5167
5245
  )`,
5246
+ `ALTER TABLE todos_task_manifest_bindings
5247
+ ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
5248
+ `ALTER TABLE todos_task_manifest_bindings
5249
+ ALTER COLUMN tenant_id DROP DEFAULT`,
5168
5250
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
5169
5251
  id text PRIMARY KEY,
5170
5252
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -5178,6 +5260,13 @@ function postgresTodosTaskManifestSchemaSql() {
5178
5260
  )`,
5179
5261
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
5180
5262
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
5263
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
5264
+ ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
5265
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
5266
+ ON todos_task_manifest_bindings(
5267
+ tenant_id,
5268
+ ((result_json #>> '{graph,plan_id}'))
5269
+ )`,
5181
5270
  `CREATE OR REPLACE FUNCTION todos_task_manifest_receipts_immutable()
5182
5271
  RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
5183
5272
  RAISE EXCEPTION 'todos task manifest receipts are immutable';
@@ -5201,10 +5290,12 @@ function parseApplyResult(value, duplicate) {
5201
5290
 
5202
5291
  class SqliteTodosTaskManifestBackend {
5203
5292
  db;
5293
+ tenantId;
5204
5294
  kind = "sqlite";
5205
- constructor(db) {
5295
+ constructor(db, tenantId = "default") {
5206
5296
  this.db = db;
5207
- db.exec(sqliteTodosTaskManifestSchemaSql());
5297
+ this.tenantId = tenantId;
5298
+ ensureSqliteTodosTaskManifestSchema(db, tenantId);
5208
5299
  }
5209
5300
  async serialized(run) {
5210
5301
  const previous = sqliteTails.get(this.db) ?? Promise.resolve();
@@ -5234,7 +5325,7 @@ class SqliteTodosTaskManifestBackend {
5234
5325
  async apply(input, faults) {
5235
5326
  return this.serialized(() => {
5236
5327
  const { manifest } = input;
5237
- const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = ? LIMIT 1").get(manifest.operation_id);
5328
+ 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);
5238
5329
  if (binding) {
5239
5330
  if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
5240
5331
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
@@ -5244,7 +5335,7 @@ class SqliteTodosTaskManifestBackend {
5244
5335
  }
5245
5336
  return parseApplyResult(String(binding["result_json"]), true);
5246
5337
  }
5247
- const idempotency = this.db.query("SELECT operation_id, request_digest FROM todos_task_manifest_bindings WHERE idempotency_key = ? LIMIT 1").get(manifest.idempotency_key);
5338
+ 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);
5248
5339
  if (idempotency)
5249
5340
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used by another operation");
5250
5341
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
@@ -5334,9 +5425,9 @@ class SqliteTodosTaskManifestBackend {
5334
5425
  const resultJson = canonicalJson(result);
5335
5426
  const manifestJson = canonicalJson(manifest);
5336
5427
  this.db.query(`INSERT INTO todos_task_manifest_receipts (
5337
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
5428
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
5338
5429
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
5339
- ) 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);
5430
+ ) 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);
5340
5431
  for (const entry of input.outbox) {
5341
5432
  this.db.query(`INSERT INTO todos_task_manifest_outbox (
5342
5433
  id, apply_receipt_id, topic, payload, payload_digest, status, created_at
@@ -5344,49 +5435,102 @@ class SqliteTodosTaskManifestBackend {
5344
5435
  }
5345
5436
  fault(faults, "after_outbox_write");
5346
5437
  this.db.query(`INSERT INTO todos_task_manifest_bindings (
5347
- operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
5438
+ operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
5348
5439
  manifest_json, result_json, state, version, created_at, updated_at
5349
- ) 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);
5440
+ ) 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);
5350
5441
  fault(faults, "after_receipt_write");
5351
5442
  return result;
5352
5443
  });
5353
5444
  }
5354
5445
  async readExact(receiptId) {
5355
- const row = this.db.query("SELECT result_json FROM todos_task_manifest_receipts WHERE receipt_id = ? AND kind = 'apply' LIMIT 1").get(receiptId);
5446
+ 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, receiptId);
5356
5447
  if (!row)
5357
5448
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId}`);
5358
5449
  return parseApplyResult(row.result_json, false);
5359
5450
  }
5451
+ async lookupBindingByPlanId(planId) {
5452
+ const rows = this.db.query(`
5453
+ SELECT
5454
+ b.apply_receipt_id AS apply_receipt_id,
5455
+ b.state AS state,
5456
+ b.version AS binding_version,
5457
+ b.tenant_id AS binding_tenant_id,
5458
+ b.operation_id AS binding_operation_id,
5459
+ json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
5460
+ r.tenant_id AS receipt_tenant_id,
5461
+ r.authority AS receipt_authority,
5462
+ r.route AS receipt_route,
5463
+ r.schema_version AS receipt_schema_version,
5464
+ r.kind AS receipt_kind,
5465
+ r.operation_id AS receipt_operation_id,
5466
+ json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
5467
+ FROM todos_task_manifest_bindings b
5468
+ LEFT JOIN todos_task_manifest_receipts r
5469
+ ON r.receipt_id = b.apply_receipt_id
5470
+ AND r.tenant_id = b.tenant_id
5471
+ WHERE b.tenant_id = ?
5472
+ AND json_extract(b.result_json, '$.graph.plan_id') = ?
5473
+ LIMIT 2
5474
+ `).all(this.tenantId, planId);
5475
+ return validateTaskManifestBindingLookupRows(rows, this.tenantId, planId);
5476
+ }
5360
5477
  async markOutboxDelivered(outboxId, deliveredAt) {
5361
5478
  await this.serialized(() => {
5362
5479
  const result = this.db.query(`UPDATE todos_task_manifest_outbox
5363
5480
  SET status = 'delivered', delivered_at = ?, attempts = attempts + 1
5364
- WHERE id = ? AND status = 'pending'`).run(deliveredAt, outboxId);
5365
- if (result.changes !== 1) {
5366
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
5367
- }
5481
+ WHERE id = ? AND status = 'pending'
5482
+ AND EXISTS (
5483
+ SELECT 1 FROM todos_task_manifest_receipts r
5484
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
5485
+ AND r.tenant_id = ?
5486
+ AND r.authority = 'todos'
5487
+ AND r.route = 'todos.task-manifest.v1'
5488
+ AND r.schema_version = 1
5489
+ AND r.kind = 'apply'
5490
+ )`).run(deliveredAt, outboxId, this.tenantId);
5491
+ if (result.changes === 1)
5492
+ return;
5493
+ const existing = this.db.query(`SELECT o.status
5494
+ FROM todos_task_manifest_outbox o
5495
+ JOIN todos_task_manifest_receipts r
5496
+ ON r.receipt_id = o.apply_receipt_id
5497
+ WHERE r.tenant_id = ?
5498
+ AND r.authority = 'todos'
5499
+ AND r.route = 'todos.task-manifest.v1'
5500
+ AND r.schema_version = 1
5501
+ AND r.kind = 'apply'
5502
+ AND o.id = ?
5503
+ LIMIT 1`).get(this.tenantId, outboxId);
5504
+ if (existing?.status === "delivered")
5505
+ return;
5506
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
5368
5507
  });
5369
5508
  }
5370
5509
  async compensate(input, receipt, compensationReceiptId, requestDigest, now) {
5371
5510
  return this.serialized(() => {
5372
5511
  const existing = this.db.query(`SELECT apply_receipt_id, request_digest, result_json
5373
- FROM todos_task_manifest_receipts WHERE kind = 'compensate' AND idempotency_key = ? LIMIT 1`).get(input.idempotency_key);
5512
+ FROM todos_task_manifest_receipts
5513
+ WHERE tenant_id = ? AND kind = 'compensate' AND idempotency_key = ?
5514
+ LIMIT 1`).get(this.tenantId, input.idempotency_key);
5374
5515
  if (existing) {
5375
5516
  if (existing.apply_receipt_id !== input.receipt_id || existing.request_digest !== requestDigest) {
5376
5517
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation idempotency key is already used");
5377
5518
  }
5378
5519
  return { ...JSON.parse(existing.result_json), duplicate: true };
5379
5520
  }
5380
- const row = this.db.query("SELECT * FROM todos_task_manifest_receipts WHERE receipt_id = ? AND kind = 'apply' LIMIT 1").get(input.receipt_id);
5521
+ 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);
5381
5522
  if (!row)
5382
5523
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
5383
- const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = ? LIMIT 1").get(String(row["operation_id"]));
5524
+ 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"]));
5384
5525
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
5385
5526
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
5386
5527
  }
5387
5528
  if (binding["state"] !== "applied")
5388
5529
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
5389
- 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);
5530
+ const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
5531
+ JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
5532
+ WHERE r.tenant_id = ? AND o.apply_receipt_id = ? AND o.status = 'delivered'
5533
+ LIMIT 1`).get(this.tenantId, input.receipt_id);
5390
5534
  if (delivered)
5391
5535
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
5392
5536
  const applyResult = parseApplyResult(String(row["result_json"]), false);
@@ -5399,7 +5543,14 @@ class SqliteTodosTaskManifestBackend {
5399
5543
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
5400
5544
  ];
5401
5545
  const storedOutbox = this.db.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
5402
- FROM todos_task_manifest_outbox WHERE apply_receipt_id = ? ORDER BY id`).all(input.receipt_id);
5546
+ FROM todos_task_manifest_outbox
5547
+ WHERE apply_receipt_id = ?
5548
+ AND EXISTS (
5549
+ SELECT 1 FROM todos_task_manifest_receipts r
5550
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
5551
+ AND r.tenant_id = ?
5552
+ )
5553
+ ORDER BY id`).all(input.receipt_id, this.tenantId);
5403
5554
  if (storedOutbox.length !== expectedEffects.length) {
5404
5555
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
5405
5556
  }
@@ -5487,7 +5638,14 @@ class SqliteTodosTaskManifestBackend {
5487
5638
  }
5488
5639
  expectedVerificationIndex += 1;
5489
5640
  }
5490
- this.db.query("UPDATE todos_task_manifest_outbox SET status = 'cancelled' WHERE apply_receipt_id = ? AND status = 'pending'").run(input.receipt_id);
5641
+ this.db.query(`UPDATE todos_task_manifest_outbox
5642
+ SET status = 'cancelled'
5643
+ WHERE apply_receipt_id = ? AND status = 'pending'
5644
+ AND EXISTS (
5645
+ SELECT 1 FROM todos_task_manifest_receipts r
5646
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
5647
+ AND r.tenant_id = ?
5648
+ )`).run(input.receipt_id, this.tenantId);
5491
5649
  for (const table of ["task_tags", "task_dependencies", "task_comments", "task_verifications"]) {
5492
5650
  const column = table === "task_dependencies" ? "task_id" : "task_id";
5493
5651
  this.db.query(`DELETE FROM ${table} WHERE ${column} IN (${placeholders2})`).run(...taskIds);
@@ -5498,11 +5656,11 @@ class SqliteTodosTaskManifestBackend {
5498
5656
  const result = { duplicate: false, receipt, absent: true, readback };
5499
5657
  const resultJson = canonicalJson(result);
5500
5658
  this.db.query(`INSERT INTO todos_task_manifest_receipts (
5501
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
5659
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
5502
5660
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
5503
- ) 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, now);
5661
+ ) 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, now);
5504
5662
  const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
5505
- WHERE operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now, receipt.operation_id, input.if_binding_version);
5663
+ WHERE tenant_id = ? AND operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now, this.tenantId, receipt.operation_id, input.if_binding_version);
5506
5664
  if (updated.changes !== 1) {
5507
5665
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding changed during compensation");
5508
5666
  }
@@ -6045,17 +6203,19 @@ class PostgresTodosTaskManifestBackend {
6045
6203
  kind = "postgresql";
6046
6204
  service;
6047
6205
  tableName;
6206
+ tenantId;
6048
6207
  schemaReady = null;
6049
6208
  constructor(client, options = {}) {
6050
6209
  this.client = client;
6051
6210
  this.service = options.service ?? "todos";
6052
6211
  this.tableName = safeIdentifier(options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, "tableName");
6212
+ this.tenantId = options.tenantId ?? "default";
6053
6213
  }
6054
6214
  async ensureSchema() {
6055
6215
  this.schemaReady ??= (async () => {
6056
6216
  for (const sql of postgresTodosSyncSchemaSql(this.tableName))
6057
6217
  await this.client.query(sql);
6058
- for (const sql of postgresTodosTaskManifestSchemaSql())
6218
+ for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
6059
6219
  await this.client.query(sql);
6060
6220
  })();
6061
6221
  await this.schemaReady;
@@ -6077,7 +6237,7 @@ class PostgresTodosTaskManifestBackend {
6077
6237
  const { manifest } = input;
6078
6238
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
6079
6239
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
6080
- const existing = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = $1 LIMIT 1 FOR UPDATE", [manifest.operation_id]);
6240
+ 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]);
6081
6241
  if (existing.rows[0]) {
6082
6242
  const binding = existing.rows[0];
6083
6243
  if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
@@ -6088,7 +6248,7 @@ class PostgresTodosTaskManifestBackend {
6088
6248
  }
6089
6249
  return { ...parseJson(binding["result_json"]), duplicate: true };
6090
6250
  }
6091
- const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE idempotency_key = $1 LIMIT 1", [manifest.idempotency_key]);
6251
+ 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]);
6092
6252
  if (reused.rows[0])
6093
6253
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
6094
6254
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
@@ -6198,10 +6358,11 @@ class PostgresTodosTaskManifestBackend {
6198
6358
  const manifestJson = canonicalJson(manifest);
6199
6359
  const resultJson = canonicalJson(result);
6200
6360
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
6201
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
6361
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
6202
6362
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
6203
- ) VALUES ($1, 'todos', 'todos.task-manifest.v1', 1, 'apply', $2, $3, $4, $5, 1, NULL, $6::jsonb, $7::jsonb, $8)`, [
6363
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, 1, NULL, $7::jsonb, $8::jsonb, $9)`, [
6204
6364
  input.receipt_id,
6365
+ this.tenantId,
6205
6366
  manifest.operation_id,
6206
6367
  manifest.idempotency_key,
6207
6368
  input.request_digest,
@@ -6224,10 +6385,11 @@ class PostgresTodosTaskManifestBackend {
6224
6385
  }
6225
6386
  fault2(faults, "after_outbox_write");
6226
6387
  await tx.query(`INSERT INTO todos_task_manifest_bindings (
6227
- operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
6388
+ operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
6228
6389
  manifest_json, result_json, state, version, created_at, updated_at
6229
- ) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, 'applied', 1, $8, $8)`, [
6390
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
6230
6391
  manifest.operation_id,
6392
+ this.tenantId,
6231
6393
  manifest.idempotency_key,
6232
6394
  input.request_digest,
6233
6395
  input.result_digest,
@@ -6242,19 +6404,86 @@ class PostgresTodosTaskManifestBackend {
6242
6404
  }
6243
6405
  async readExact(receiptId) {
6244
6406
  await this.ensureSchema();
6245
- const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE receipt_id = $1 AND kind = 'apply' LIMIT 1", [receiptId]);
6407
+ 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, receiptId]);
6246
6408
  if (!result.rows[0])
6247
6409
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId}`);
6248
6410
  return { ...parseJson(result.rows[0]["result_json"]), duplicate: false };
6249
6411
  }
6412
+ async lookupBindingByPlanId(planId) {
6413
+ await this.ensureSchema();
6414
+ const result = await this.client.query(`
6415
+ SELECT
6416
+ b.apply_receipt_id AS apply_receipt_id,
6417
+ b.state AS state,
6418
+ b.version AS binding_version,
6419
+ b.tenant_id AS binding_tenant_id,
6420
+ b.operation_id AS binding_operation_id,
6421
+ b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
6422
+ r.tenant_id AS receipt_tenant_id,
6423
+ r.authority AS receipt_authority,
6424
+ r.route AS receipt_route,
6425
+ r.schema_version AS receipt_schema_version,
6426
+ r.kind AS receipt_kind,
6427
+ r.operation_id AS receipt_operation_id,
6428
+ r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
6429
+ FROM todos_task_manifest_bindings b
6430
+ LEFT JOIN todos_task_manifest_receipts r
6431
+ ON r.receipt_id = b.apply_receipt_id
6432
+ AND r.tenant_id = b.tenant_id
6433
+ WHERE b.tenant_id = $1
6434
+ AND b.result_json #>> '{graph,plan_id}' = $2
6435
+ LIMIT 2
6436
+ `, [this.tenantId, planId]);
6437
+ return validateTaskManifestBindingLookupRows(result.rows, this.tenantId, planId);
6438
+ }
6250
6439
  async markOutboxDelivered(outboxId, deliveredAt) {
6251
6440
  await this.ensureSchema();
6252
6441
  await this.client.transaction(async (tx) => {
6442
+ const owned = await tx.query(`SELECT r.operation_id
6443
+ FROM todos_task_manifest_outbox o
6444
+ JOIN todos_task_manifest_receipts r
6445
+ ON r.receipt_id = o.apply_receipt_id
6446
+ WHERE r.tenant_id = $1
6447
+ AND r.authority = 'todos'
6448
+ AND r.route = 'todos.task-manifest.v1'
6449
+ AND r.schema_version = 1
6450
+ AND r.kind = 'apply'
6451
+ AND o.id = $2
6452
+ LIMIT 1`, [this.tenantId, outboxId]);
6453
+ const operationId = owned.rows[0]?.operation_id;
6454
+ if (operationId == null) {
6455
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
6456
+ }
6457
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${String(operationId)}`]);
6253
6458
  const result = await tx.query(`UPDATE todos_task_manifest_outbox
6254
6459
  SET status = 'delivered', delivered_at = $1, attempts = attempts + 1
6255
- WHERE id = $2 AND status = 'pending' RETURNING id`, [deliveredAt, outboxId]);
6256
- if (!result.rows[0])
6257
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
6460
+ WHERE id = $2 AND status = 'pending'
6461
+ AND EXISTS (
6462
+ SELECT 1 FROM todos_task_manifest_receipts r
6463
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
6464
+ AND r.tenant_id = $3
6465
+ AND r.authority = 'todos'
6466
+ AND r.route = 'todos.task-manifest.v1'
6467
+ AND r.schema_version = 1
6468
+ AND r.kind = 'apply'
6469
+ )
6470
+ RETURNING id`, [deliveredAt, outboxId, this.tenantId]);
6471
+ if (result.rows[0])
6472
+ return;
6473
+ const existing = await tx.query(`SELECT o.status
6474
+ FROM todos_task_manifest_outbox o
6475
+ JOIN todos_task_manifest_receipts r
6476
+ ON r.receipt_id = o.apply_receipt_id
6477
+ WHERE r.tenant_id = $1
6478
+ AND r.authority = 'todos'
6479
+ AND r.route = 'todos.task-manifest.v1'
6480
+ AND r.schema_version = 1
6481
+ AND r.kind = 'apply'
6482
+ AND o.id = $2
6483
+ LIMIT 1`, [this.tenantId, outboxId]);
6484
+ if (existing.rows[0]?.status === "delivered")
6485
+ return;
6486
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
6258
6487
  });
6259
6488
  }
6260
6489
  async compensate(input, receipt, compensationReceiptId, requestDigest, now) {
@@ -6262,7 +6491,10 @@ class PostgresTodosTaskManifestBackend {
6262
6491
  return this.client.transaction(async (tx) => {
6263
6492
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${receipt.operation_id}`]);
6264
6493
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fcompensation-idempotency\x1F${input.idempotency_key}`]);
6265
- 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]);
6494
+ const previous = await tx.query(`SELECT apply_receipt_id, request_digest, result_json
6495
+ FROM todos_task_manifest_receipts
6496
+ WHERE tenant_id = $1 AND kind = 'compensate' AND idempotency_key = $2
6497
+ LIMIT 1`, [this.tenantId, input.idempotency_key]);
6266
6498
  if (previous.rows[0]) {
6267
6499
  const row = previous.rows[0];
6268
6500
  if (row["apply_receipt_id"] !== input.receipt_id || row["request_digest"] !== requestDigest) {
@@ -6270,18 +6502,27 @@ class PostgresTodosTaskManifestBackend {
6270
6502
  }
6271
6503
  return { ...parseJson(row["result_json"]), duplicate: true };
6272
6504
  }
6273
- const applyRows = await tx.query("SELECT * FROM todos_task_manifest_receipts WHERE receipt_id = $1 AND kind = 'apply' LIMIT 1", [input.receipt_id]);
6505
+ 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]);
6274
6506
  const applyRow = applyRows.rows[0];
6275
6507
  if (!applyRow)
6276
6508
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
6277
- const bindingRows = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = $1 LIMIT 1 FOR UPDATE", [receipt.operation_id]);
6509
+ 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]);
6278
6510
  const binding = bindingRows.rows[0];
6279
6511
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
6280
6512
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
6281
6513
  }
6282
6514
  if (binding["state"] !== "applied")
6283
6515
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
6284
- 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]);
6516
+ const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
6517
+ JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
6518
+ WHERE r.tenant_id = $1
6519
+ AND r.authority = 'todos'
6520
+ AND r.route = 'todos.task-manifest.v1'
6521
+ AND r.schema_version = 1
6522
+ AND r.kind = 'apply'
6523
+ AND o.apply_receipt_id = $2
6524
+ AND o.status = 'delivered'
6525
+ LIMIT 1`, [this.tenantId, input.receipt_id]);
6285
6526
  if (delivered.rows[0])
6286
6527
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
6287
6528
  const applyResult = parseJson(applyRow["result_json"]);
@@ -6293,8 +6534,20 @@ class PostgresTodosTaskManifestBackend {
6293
6534
  },
6294
6535
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
6295
6536
  ];
6296
- const outboxRows = await tx.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
6297
- FROM todos_task_manifest_outbox WHERE apply_receipt_id = $1 ORDER BY id`, [input.receipt_id]);
6537
+ const outboxRows = await tx.query(`SELECT o.id, o.topic, o.payload, o.payload_digest, o.status, o.attempts, o.delivered_at
6538
+ FROM todos_task_manifest_outbox o
6539
+ WHERE o.apply_receipt_id = $1
6540
+ AND EXISTS (
6541
+ SELECT 1 FROM todos_task_manifest_receipts r
6542
+ WHERE r.receipt_id = o.apply_receipt_id
6543
+ AND r.tenant_id = $2
6544
+ AND r.authority = 'todos'
6545
+ AND r.route = 'todos.task-manifest.v1'
6546
+ AND r.schema_version = 1
6547
+ AND r.kind = 'apply'
6548
+ )
6549
+ ORDER BY o.id
6550
+ FOR UPDATE OF o`, [input.receipt_id, this.tenantId]);
6298
6551
  if (outboxRows.rows.length !== expectedEffects.length) {
6299
6552
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
6300
6553
  }
@@ -6394,7 +6647,23 @@ class PostgresTodosTaskManifestBackend {
6394
6647
  }
6395
6648
  if (stored.rows.length !== managedIds.length)
6396
6649
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: managed graph is incomplete");
6397
- await tx.query("UPDATE todos_task_manifest_outbox SET status = 'cancelled' WHERE apply_receipt_id = $1 AND status = 'pending'", [input.receipt_id]);
6650
+ const cancelled = await tx.query(`UPDATE todos_task_manifest_outbox
6651
+ SET status = 'cancelled'
6652
+ WHERE apply_receipt_id = $1 AND status = 'pending'
6653
+ AND EXISTS (
6654
+ SELECT 1 FROM todos_task_manifest_receipts r
6655
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
6656
+ AND r.tenant_id = $2
6657
+ AND r.authority = 'todos'
6658
+ AND r.route = 'todos.task-manifest.v1'
6659
+ AND r.schema_version = 1
6660
+ AND r.kind = 'apply'
6661
+ )
6662
+ RETURNING id`, [input.receipt_id, this.tenantId]);
6663
+ const cancelledIds = new Set(cancelled.rows.map((row) => String(row.id)));
6664
+ if (cancelledIds.size !== applyResult.outbox_ids.length || applyResult.outbox_ids.some((id) => !cancelledIds.has(id))) {
6665
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: failed to cancel every expected outbox row");
6666
+ }
6398
6667
  const typedIds = [
6399
6668
  ["dependencies", applyResult.graph.dependency_ids],
6400
6669
  ["comments", applyResult.graph.comment_ids],
@@ -6411,10 +6680,11 @@ class PostgresTodosTaskManifestBackend {
6411
6680
  const readback = await this.readback(tx, applyResult.graph);
6412
6681
  const result = { duplicate: false, receipt, absent: true, readback };
6413
6682
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
6414
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
6683
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
6415
6684
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
6416
- ) VALUES ($1, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $2, $3, $4, $5, $6, $7, NULL, $8::jsonb, $9)`, [
6685
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10)`, [
6417
6686
  compensationReceiptId,
6687
+ this.tenantId,
6418
6688
  receipt.operation_id,
6419
6689
  input.idempotency_key,
6420
6690
  requestDigest,
@@ -6426,10 +6696,11 @@ class PostgresTodosTaskManifestBackend {
6426
6696
  ]);
6427
6697
  const updated = await tx.query(`UPDATE todos_task_manifest_bindings
6428
6698
  SET state = 'compensated', version = $1, compensation_receipt_id = $2, updated_at = $3
6429
- WHERE operation_id = $4 AND state = 'applied' AND version = $5 RETURNING operation_id`, [
6699
+ WHERE tenant_id = $4 AND operation_id = $5 AND state = 'applied' AND version = $6 RETURNING operation_id`, [
6430
6700
  receipt.binding_version,
6431
6701
  compensationReceiptId,
6432
6702
  now,
6703
+ this.tenantId,
6433
6704
  receipt.operation_id,
6434
6705
  input.if_binding_version
6435
6706
  ]);
@@ -6469,6 +6740,13 @@ var FAULT_POINTS = [
6469
6740
  "after_outbox_write",
6470
6741
  "after_receipt_write"
6471
6742
  ];
6743
+ function resolveTenantId(value) {
6744
+ const tenantId = value ?? "default";
6745
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenantId)) {
6746
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
6747
+ }
6748
+ return tenantId;
6749
+ }
6472
6750
  function normalize(input, now) {
6473
6751
  const parsed = parseTodosTaskManifest(input);
6474
6752
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
@@ -6556,19 +6834,23 @@ function sanitizeManifest(manifest) {
6556
6834
  class PackageOwnedTodosTaskManifestAuthority {
6557
6835
  backend;
6558
6836
  options;
6837
+ tenantId;
6559
6838
  constructor(backend, options = {}) {
6560
6839
  this.backend = backend;
6561
6840
  this.options = options;
6841
+ this.tenantId = resolveTenantId(options.tenantId);
6562
6842
  }
6563
6843
  async capability() {
6564
6844
  return {
6565
6845
  authority: "todos",
6566
6846
  route: TODOS_TASK_MANIFEST_ROUTE,
6567
6847
  schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
6848
+ tenant_id: this.tenantId,
6568
6849
  backend: this.backend.kind,
6569
6850
  deterministic_ids: true,
6570
6851
  immutable_receipts: true,
6571
6852
  transactional_outbox: true,
6853
+ idempotent_outbox_delivery: true,
6572
6854
  exact_bounded_readback: true,
6573
6855
  conditional_compensation: true,
6574
6856
  transcript_safe: false,
@@ -6603,6 +6885,22 @@ class PackageOwnedTodosTaskManifestAuthority {
6603
6885
  }
6604
6886
  return this.backend.readExact(receiptId).then((result) => this.bounded(result));
6605
6887
  }
6888
+ async lookupBinding(input) {
6889
+ const request = parseTodosTaskManifestBindingLookup(input);
6890
+ if (request.max_items !== 1) {
6891
+ 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 });
6892
+ }
6893
+ if (request.authority !== "todos" || request.route !== TODOS_TASK_MANIFEST_ROUTE || request.schema_version !== TODOS_TASK_MANIFEST_SCHEMA_VERSION || request.tenant_id !== this.tenantId) {
6894
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAPABILITY_MISMATCH", "Task-manifest binding lookup does not match this authority identity");
6895
+ }
6896
+ return this.bounded({
6897
+ authority: "todos",
6898
+ route: TODOS_TASK_MANIFEST_ROUTE,
6899
+ schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
6900
+ tenant_id: this.tenantId,
6901
+ ...await this.backend.lookupBindingByPlanId(request.plan_id)
6902
+ });
6903
+ }
6606
6904
  markOutboxDelivered(outboxId) {
6607
6905
  if (!outboxId || outboxId.length > 200) {
6608
6906
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "outboxId must be a bounded exact identifier");
@@ -6642,13 +6940,15 @@ function createSqliteTodosTaskManifestAuthority(options) {
6642
6940
  if (!options?.database) {
6643
6941
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An explicit SQLite Database is required");
6644
6942
  }
6645
- return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database), options);
6943
+ const tenantId = resolveTenantId(options.tenantId);
6944
+ return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database, tenantId), { ...options, tenantId });
6646
6945
  }
6647
6946
  function createPostgresTodosTaskManifestAuthority(client, options = {}) {
6648
6947
  if (!client || typeof client.transaction !== "function") {
6649
6948
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An authoritative PostgreSQL transaction(callback) client is required");
6650
6949
  }
6651
- return new PackageOwnedTodosTaskManifestAuthority(new PostgresTodosTaskManifestBackend(client, options), options);
6950
+ const tenantId = resolveTenantId(options.tenantId);
6951
+ return new PackageOwnedTodosTaskManifestAuthority(new PostgresTodosTaskManifestBackend(client, { ...options, tenantId }), { ...options, tenantId });
6652
6952
  }
6653
6953
  // src/task-manifest/http.ts
6654
6954
  var JSON_HEADERS = { "Content-Type": "application/json" };
@@ -6662,6 +6962,7 @@ function status(error) {
6662
6962
  case "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE":
6663
6963
  return 400;
6664
6964
  case "TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND":
6965
+ case "TODOS_TASK_MANIFEST_BINDING_NOT_FOUND":
6665
6966
  return 404;
6666
6967
  case "TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE":
6667
6968
  return 503;
@@ -6724,6 +7025,11 @@ async function handleTodosTaskManifestHttpRequest(request, url, authority, baseP
6724
7025
  }
6725
7026
  return json({ result: await authority.readExact(input.receipt_id) });
6726
7027
  }
7028
+ if (action === "bindings/lookup") {
7029
+ return json({
7030
+ result: await authority.lookupBinding(await body(request))
7031
+ });
7032
+ }
6727
7033
  if (action === "compensate") {
6728
7034
  return json({ result: await authority.compensate(await body(request)) }, 201);
6729
7035
  }
@@ -6783,6 +7089,9 @@ class TodosTaskManifestHttpClient {
6783
7089
  async readExact(receiptId) {
6784
7090
  return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId }) })).result;
6785
7091
  }
7092
+ async lookupBinding(input) {
7093
+ return (await this.request("/bindings/lookup", { method: "POST", body: JSON.stringify(input) })).result;
7094
+ }
6786
7095
  async markOutboxDelivered(outboxId) {
6787
7096
  await this.request("/outbox/delivered", { method: "POST", body: JSON.stringify({ outbox_id: outboxId }) });
6788
7097
  }
@@ -6794,9 +7103,11 @@ function createTodosTaskManifestHttpClient(options) {
6794
7103
  return new TodosTaskManifestHttpClient(options);
6795
7104
  }
6796
7105
  export {
7106
+ supportsIdempotentOutboxDelivery,
6797
7107
  sqliteTodosTaskManifestSchemaSql,
6798
7108
  postgresTodosTaskManifestSchemaSql,
6799
7109
  parseTodosTaskManifestCompensation,
7110
+ parseTodosTaskManifestBindingLookup,
6800
7111
  parseTodosTaskManifest,
6801
7112
  handleTodosTaskManifestHttpRequest,
6802
7113
  canonicalDigest as digestTodosTaskManifest,