@hasna/todos 0.15.13 → 0.15.15

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 (42) hide show
  1. package/dist/cli/cloud-router.d.ts.map +1 -1
  2. package/dist/cli/index.js +12117 -10745
  3. package/dist/contracts.js +1 -1
  4. package/dist/index.js +287 -53
  5. package/dist/mcp/index.js +2181 -810
  6. package/dist/mcp.js +1 -1
  7. package/dist/project-registration.js +12 -13
  8. package/dist/registry.js +1 -1
  9. package/dist/release-provenance.json +5 -5
  10. package/dist/sdk/index.d.ts +1 -1
  11. package/dist/sdk/index.d.ts.map +1 -1
  12. package/dist/sdk/index.js +7 -0
  13. package/dist/sdk/v1.generated.d.ts +23 -0
  14. package/dist/sdk/v1.generated.d.ts.map +1 -1
  15. package/dist/server/cloud.d.ts +3 -0
  16. package/dist/server/cloud.d.ts.map +1 -1
  17. package/dist/server/index.js +19661 -18290
  18. package/dist/server/openapi.d.ts +134 -0
  19. package/dist/server/openapi.d.ts.map +1 -1
  20. package/dist/server/v1.d.ts +2 -1
  21. package/dist/server/v1.d.ts.map +1 -1
  22. package/dist/storage.js +11 -12
  23. package/dist/task-manifest/authority.d.ts +3 -1
  24. package/dist/task-manifest/authority.d.ts.map +1 -1
  25. package/dist/task-manifest/backend.d.ts +18 -1
  26. package/dist/task-manifest/backend.d.ts.map +1 -1
  27. package/dist/task-manifest/http.d.ts +2 -1
  28. package/dist/task-manifest/http.d.ts.map +1 -1
  29. package/dist/task-manifest/index.d.ts +1 -1
  30. package/dist/task-manifest/index.d.ts.map +1 -1
  31. package/dist/task-manifest/postgres.d.ts +3 -1
  32. package/dist/task-manifest/postgres.d.ts.map +1 -1
  33. package/dist/task-manifest/schema-sql.d.ts +3 -1
  34. package/dist/task-manifest/schema-sql.d.ts.map +1 -1
  35. package/dist/task-manifest/schema.d.ts +2 -1
  36. package/dist/task-manifest/schema.d.ts.map +1 -1
  37. package/dist/task-manifest/sqlite.d.ts +4 -2
  38. package/dist/task-manifest/sqlite.d.ts.map +1 -1
  39. package/dist/task-manifest/types.d.ts +22 -1
  40. package/dist/task-manifest/types.d.ts.map +1 -1
  41. package/dist/task-manifest.js +275 -40
  42. 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.13",
12495
+ version: "0.15.15",
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",
@@ -27580,21 +27580,21 @@ class PostgresJsonRecordStore {
27580
27580
  ), existing AS (
27581
27581
  SELECT payload FROM ${this.tableName}
27582
27582
  WHERE service = $1 AND object_type = 'plan_project_link_rollback_receipts'
27583
- AND object_id = $5 AND deleted_at IS NULL
27583
+ AND object_id = $4 AND deleted_at IS NULL
27584
27584
  FOR UPDATE
27585
27585
  ), checks AS (
27586
27586
  SELECT
27587
27587
  EXISTS (SELECT 1 FROM target_plan) AS plan_found,
27588
27588
  COALESCE((SELECT payload->>'updated_at' = $3 FROM target_plan), false) AS plan_revision_ok,
27589
- COALESCE((SELECT jsonb_object_agg(object_id, COALESCE(payload->'project_id', 'null'::jsonb) ORDER BY object_id) FROM member_tasks), '{}'::jsonb) = $7::jsonb
27590
- AND COALESCE((SELECT jsonb_agg(object_id ORDER BY object_id) FROM member_tasks), '[]'::jsonb) = $8::jsonb AS membership_ok,
27589
+ COALESCE((SELECT jsonb_object_agg(object_id, COALESCE(payload->'project_id', 'null'::jsonb) ORDER BY object_id) FROM member_tasks), '{}'::jsonb) = $6::jsonb
27590
+ AND COALESCE((SELECT jsonb_agg(object_id ORDER BY object_id) FROM member_tasks), '[]'::jsonb) = $7::jsonb AS membership_ok,
27591
27591
  EXISTS (SELECT 1 FROM existing) AS has_existing
27592
27592
  ), updated_plan AS (
27593
27593
  UPDATE ${this.tableName} r SET
27594
- payload = r.payload || jsonb_build_object('project_id', $9::jsonb, 'updated_at', $10::text),
27595
- updated_at = $10::timestamptz,
27594
+ payload = r.payload || jsonb_build_object('project_id', $8::jsonb, 'updated_at', $9::text),
27595
+ updated_at = $9::timestamptz,
27596
27596
  version = COALESCE(r.version, 0) + 1,
27597
- source_machine_id = COALESCE($11, r.source_machine_id)
27597
+ source_machine_id = COALESCE($10, r.source_machine_id)
27598
27598
  FROM checks
27599
27599
  WHERE r.service = $1 AND r.object_type = 'plans' AND r.object_id = $2 AND r.deleted_at IS NULL
27600
27600
  AND checks.plan_found AND checks.plan_revision_ok AND checks.membership_ok AND NOT checks.has_existing
@@ -27602,20 +27602,20 @@ class PostgresJsonRecordStore {
27602
27602
  ), updated_tasks AS (
27603
27603
  UPDATE ${this.tableName} r SET
27604
27604
  payload = r.payload || jsonb_build_object(
27605
- 'project_id', COALESCE($12::jsonb -> r.object_id, 'null'::jsonb),
27606
- 'updated_at', $10::text,
27605
+ 'project_id', COALESCE($11::jsonb -> r.object_id, 'null'::jsonb),
27606
+ 'updated_at', $9::text,
27607
27607
  'version', COALESCE((r.payload->>'version')::int, 0) + 1
27608
27608
  ),
27609
- updated_at = $10::timestamptz,
27609
+ updated_at = $9::timestamptz,
27610
27610
  version = COALESCE(r.version, 0) + 1,
27611
- source_machine_id = COALESCE($11, r.source_machine_id)
27611
+ source_machine_id = COALESCE($10, r.source_machine_id)
27612
27612
  WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
27613
27613
  AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM updated_plan)
27614
27614
  RETURNING 1
27615
27615
  ), task_gate AS (SELECT count(*) AS count FROM updated_tasks), inserted AS (
27616
27616
  INSERT INTO ${this.tableName}
27617
27617
  (service, object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version)
27618
- SELECT $1, 'plan_project_link_rollback_receipts', $5, $6::jsonb, $10::timestamptz, NULL, $11, 1
27618
+ SELECT $1, 'plan_project_link_rollback_receipts', $4, $5::jsonb, $9::timestamptz, NULL, $10, 1
27619
27619
  FROM checks, task_gate
27620
27620
  WHERE NOT checks.has_existing AND EXISTS (SELECT 1 FROM updated_plan)
27621
27621
  RETURNING payload
@@ -27629,7 +27629,6 @@ class PostgresJsonRecordStore {
27629
27629
  this.service,
27630
27630
  input.plan_id,
27631
27631
  input.expected_plan_revision,
27632
- input.receipt_id,
27633
27632
  input.rollback_receipt_id,
27634
27633
  jsonbParam(rollback),
27635
27634
  jsonbParam(currentTaskProjects),
@@ -39178,6 +39177,14 @@ var compensationSchema = exports_external.object({
39178
39177
  idempotency_key: identifier,
39179
39178
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
39180
39179
  }).strict();
39180
+ var bindingLookupSchema = exports_external.object({
39181
+ authority: exports_external.string().min(1).max(64),
39182
+ route: exports_external.string().min(1).max(128),
39183
+ schema_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
39184
+ tenant_id: identifier,
39185
+ plan_id: uuid2,
39186
+ max_items: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
39187
+ }).strict();
39181
39188
  function parseTodosTaskManifest(input) {
39182
39189
  const parsed = schema.safeParse(input);
39183
39190
  if (!parsed.success) {
@@ -39243,6 +39250,36 @@ function parseTodosTaskManifestCompensation(input) {
39243
39250
  }
39244
39251
  return parsed.data;
39245
39252
  }
39253
+ function parseTodosTaskManifestBindingLookup(input) {
39254
+ const parsed = bindingLookupSchema.safeParse(input);
39255
+ if (!parsed.success) {
39256
+ 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 });
39257
+ }
39258
+ return parsed.data;
39259
+ }
39260
+
39261
+ // src/task-manifest/backend.ts
39262
+ 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;
39263
+ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
39264
+ if (rows.length === 0) {
39265
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BINDING_NOT_FOUND", `Managed task-manifest binding not found for plan: ${planId}`, { plan_id: planId });
39266
+ }
39267
+ if (rows.length !== 1) {
39268
+ 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 });
39269
+ }
39270
+ const row = rows[0];
39271
+ const bindingVersion = Number(row.binding_version);
39272
+ const state = row.state;
39273
+ 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") {
39274
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
39275
+ }
39276
+ return {
39277
+ plan_id: planId,
39278
+ apply_receipt_id: row.apply_receipt_id,
39279
+ binding_version: bindingVersion,
39280
+ state
39281
+ };
39282
+ }
39246
39283
 
39247
39284
  // src/task-manifest/reference-guard.ts
39248
39285
  function quoteSqliteIdentifier2(value) {
@@ -39332,10 +39369,14 @@ function postgresTaskManifestForeignReferenceSql(tableName) {
39332
39369
  }
39333
39370
 
39334
39371
  // src/task-manifest/schema-sql.ts
39372
+ function sqlString(value) {
39373
+ return `'${value.replaceAll("'", "''")}'`;
39374
+ }
39335
39375
  function sqliteTodosTaskManifestSchemaSql() {
39336
39376
  return `
39337
39377
  CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
39338
39378
  receipt_id TEXT PRIMARY KEY,
39379
+ tenant_id TEXT NOT NULL,
39339
39380
  authority TEXT NOT NULL CHECK(authority = 'todos'),
39340
39381
  route TEXT NOT NULL,
39341
39382
  schema_version INTEGER NOT NULL CHECK(schema_version = 1),
@@ -39353,6 +39394,7 @@ function sqliteTodosTaskManifestSchemaSql() {
39353
39394
  );
39354
39395
  CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
39355
39396
  operation_id TEXT PRIMARY KEY,
39397
+ tenant_id TEXT NOT NULL,
39356
39398
  idempotency_key TEXT NOT NULL UNIQUE,
39357
39399
  request_digest TEXT NOT NULL,
39358
39400
  result_digest TEXT NOT NULL,
@@ -39388,10 +39430,37 @@ function sqliteTodosTaskManifestSchemaSql() {
39388
39430
  END;
39389
39431
  `;
39390
39432
  }
39391
- function postgresTodosTaskManifestSchemaSql() {
39433
+ function sqliteTableHasColumn(db, tableName, columnName) {
39434
+ const columns = db.query(`PRAGMA table_info("${tableName}")`).all();
39435
+ return columns.some((column) => column.name === columnName);
39436
+ }
39437
+ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
39438
+ db.exec(sqliteTodosTaskManifestSchemaSql());
39439
+ const tenantDefault = sqlString(tenantId);
39440
+ for (const tableName of [
39441
+ "todos_task_manifest_receipts",
39442
+ "todos_task_manifest_bindings"
39443
+ ]) {
39444
+ if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
39445
+ db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
39446
+ }
39447
+ }
39448
+ db.exec(`
39449
+ CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
39450
+ ON todos_task_manifest_receipts(tenant_id, receipt_id, kind);
39451
+ CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_bindings_tenant_plan
39452
+ ON todos_task_manifest_bindings(
39453
+ tenant_id,
39454
+ json_extract(result_json, '$.graph.plan_id')
39455
+ );
39456
+ `);
39457
+ }
39458
+ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
39459
+ const tenantDefault = sqlString(tenantId);
39392
39460
  return [
39393
39461
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_receipts (
39394
39462
  receipt_id text PRIMARY KEY,
39463
+ tenant_id text NOT NULL,
39395
39464
  authority text NOT NULL CHECK(authority = 'todos'),
39396
39465
  route text NOT NULL,
39397
39466
  schema_version integer NOT NULL CHECK(schema_version = 1),
@@ -39407,8 +39476,13 @@ function postgresTodosTaskManifestSchemaSql() {
39407
39476
  created_at timestamptz NOT NULL,
39408
39477
  UNIQUE(kind, idempotency_key)
39409
39478
  )`,
39479
+ `ALTER TABLE todos_task_manifest_receipts
39480
+ ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
39481
+ `ALTER TABLE todos_task_manifest_receipts
39482
+ ALTER COLUMN tenant_id DROP DEFAULT`,
39410
39483
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
39411
39484
  operation_id text PRIMARY KEY,
39485
+ tenant_id text NOT NULL,
39412
39486
  idempotency_key text NOT NULL UNIQUE,
39413
39487
  request_digest text NOT NULL,
39414
39488
  result_digest text NOT NULL,
@@ -39421,6 +39495,10 @@ function postgresTodosTaskManifestSchemaSql() {
39421
39495
  created_at timestamptz NOT NULL,
39422
39496
  updated_at timestamptz NOT NULL
39423
39497
  )`,
39498
+ `ALTER TABLE todos_task_manifest_bindings
39499
+ ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
39500
+ `ALTER TABLE todos_task_manifest_bindings
39501
+ ALTER COLUMN tenant_id DROP DEFAULT`,
39424
39502
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
39425
39503
  id text PRIMARY KEY,
39426
39504
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -39434,6 +39512,13 @@ function postgresTodosTaskManifestSchemaSql() {
39434
39512
  )`,
39435
39513
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
39436
39514
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
39515
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
39516
+ ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
39517
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
39518
+ ON todos_task_manifest_bindings(
39519
+ tenant_id,
39520
+ ((result_json #>> '{graph,plan_id}'))
39521
+ )`,
39437
39522
  `CREATE OR REPLACE FUNCTION todos_task_manifest_receipts_immutable()
39438
39523
  RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN
39439
39524
  RAISE EXCEPTION 'todos task manifest receipts are immutable';
@@ -39457,10 +39542,12 @@ function parseApplyResult(value, duplicate) {
39457
39542
 
39458
39543
  class SqliteTodosTaskManifestBackend {
39459
39544
  db;
39545
+ tenantId;
39460
39546
  kind = "sqlite";
39461
- constructor(db) {
39547
+ constructor(db, tenantId = "default") {
39462
39548
  this.db = db;
39463
- db.exec(sqliteTodosTaskManifestSchemaSql());
39549
+ this.tenantId = tenantId;
39550
+ ensureSqliteTodosTaskManifestSchema(db, tenantId);
39464
39551
  }
39465
39552
  async serialized(run) {
39466
39553
  const previous = sqliteTails.get(this.db) ?? Promise.resolve();
@@ -39490,7 +39577,7 @@ class SqliteTodosTaskManifestBackend {
39490
39577
  async apply(input, faults) {
39491
39578
  return this.serialized(() => {
39492
39579
  const { manifest } = input;
39493
- const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = ? LIMIT 1").get(manifest.operation_id);
39580
+ 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);
39494
39581
  if (binding) {
39495
39582
  if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
39496
39583
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
@@ -39500,7 +39587,7 @@ class SqliteTodosTaskManifestBackend {
39500
39587
  }
39501
39588
  return parseApplyResult(String(binding["result_json"]), true);
39502
39589
  }
39503
- const idempotency = this.db.query("SELECT operation_id, request_digest FROM todos_task_manifest_bindings WHERE idempotency_key = ? LIMIT 1").get(manifest.idempotency_key);
39590
+ 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);
39504
39591
  if (idempotency)
39505
39592
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used by another operation");
39506
39593
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
@@ -39590,9 +39677,9 @@ class SqliteTodosTaskManifestBackend {
39590
39677
  const resultJson = canonicalJson(result);
39591
39678
  const manifestJson = canonicalJson(manifest);
39592
39679
  this.db.query(`INSERT INTO todos_task_manifest_receipts (
39593
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
39680
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
39594
39681
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
39595
- ) 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);
39682
+ ) 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);
39596
39683
  for (const entry2 of input.outbox) {
39597
39684
  this.db.query(`INSERT INTO todos_task_manifest_outbox (
39598
39685
  id, apply_receipt_id, topic, payload, payload_digest, status, created_at
@@ -39600,24 +39687,55 @@ class SqliteTodosTaskManifestBackend {
39600
39687
  }
39601
39688
  fault(faults, "after_outbox_write");
39602
39689
  this.db.query(`INSERT INTO todos_task_manifest_bindings (
39603
- operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
39690
+ operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
39604
39691
  manifest_json, result_json, state, version, created_at, updated_at
39605
- ) 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);
39692
+ ) 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);
39606
39693
  fault(faults, "after_receipt_write");
39607
39694
  return result;
39608
39695
  });
39609
39696
  }
39610
39697
  async readExact(receiptId2) {
39611
- const row = this.db.query("SELECT result_json FROM todos_task_manifest_receipts WHERE receipt_id = ? AND kind = 'apply' LIMIT 1").get(receiptId2);
39698
+ 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);
39612
39699
  if (!row)
39613
39700
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
39614
39701
  return parseApplyResult(row.result_json, false);
39615
39702
  }
39703
+ async lookupBindingByPlanId(planId) {
39704
+ const rows = this.db.query(`
39705
+ SELECT
39706
+ b.apply_receipt_id AS apply_receipt_id,
39707
+ b.state AS state,
39708
+ b.version AS binding_version,
39709
+ b.tenant_id AS binding_tenant_id,
39710
+ b.operation_id AS binding_operation_id,
39711
+ json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
39712
+ r.tenant_id AS receipt_tenant_id,
39713
+ r.authority AS receipt_authority,
39714
+ r.route AS receipt_route,
39715
+ r.schema_version AS receipt_schema_version,
39716
+ r.kind AS receipt_kind,
39717
+ r.operation_id AS receipt_operation_id,
39718
+ json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
39719
+ FROM todos_task_manifest_bindings b
39720
+ LEFT JOIN todos_task_manifest_receipts r
39721
+ ON r.receipt_id = b.apply_receipt_id
39722
+ AND r.tenant_id = b.tenant_id
39723
+ WHERE b.tenant_id = ?
39724
+ AND json_extract(b.result_json, '$.graph.plan_id') = ?
39725
+ LIMIT 2
39726
+ `).all(this.tenantId, planId);
39727
+ return validateTaskManifestBindingLookupRows(rows, this.tenantId, planId);
39728
+ }
39616
39729
  async markOutboxDelivered(outboxId, deliveredAt) {
39617
39730
  await this.serialized(() => {
39618
39731
  const result = this.db.query(`UPDATE todos_task_manifest_outbox
39619
39732
  SET status = 'delivered', delivered_at = ?, attempts = attempts + 1
39620
- WHERE id = ? AND status = 'pending'`).run(deliveredAt, outboxId);
39733
+ WHERE id = ? AND status = 'pending'
39734
+ AND EXISTS (
39735
+ SELECT 1 FROM todos_task_manifest_receipts r
39736
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
39737
+ AND r.tenant_id = ?
39738
+ )`).run(deliveredAt, outboxId, this.tenantId);
39621
39739
  if (result.changes !== 1) {
39622
39740
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
39623
39741
  }
@@ -39626,23 +39744,28 @@ class SqliteTodosTaskManifestBackend {
39626
39744
  async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
39627
39745
  return this.serialized(() => {
39628
39746
  const existing = this.db.query(`SELECT apply_receipt_id, request_digest, result_json
39629
- FROM todos_task_manifest_receipts WHERE kind = 'compensate' AND idempotency_key = ? LIMIT 1`).get(input.idempotency_key);
39747
+ FROM todos_task_manifest_receipts
39748
+ WHERE tenant_id = ? AND kind = 'compensate' AND idempotency_key = ?
39749
+ LIMIT 1`).get(this.tenantId, input.idempotency_key);
39630
39750
  if (existing) {
39631
39751
  if (existing.apply_receipt_id !== input.receipt_id || existing.request_digest !== requestDigest) {
39632
39752
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation idempotency key is already used");
39633
39753
  }
39634
39754
  return { ...JSON.parse(existing.result_json), duplicate: true };
39635
39755
  }
39636
- const row = this.db.query("SELECT * FROM todos_task_manifest_receipts WHERE receipt_id = ? AND kind = 'apply' LIMIT 1").get(input.receipt_id);
39756
+ 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);
39637
39757
  if (!row)
39638
39758
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
39639
- const binding = this.db.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = ? LIMIT 1").get(String(row["operation_id"]));
39759
+ 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"]));
39640
39760
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
39641
39761
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
39642
39762
  }
39643
39763
  if (binding["state"] !== "applied")
39644
39764
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
39645
- 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);
39765
+ const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
39766
+ JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
39767
+ WHERE r.tenant_id = ? AND o.apply_receipt_id = ? AND o.status = 'delivered'
39768
+ LIMIT 1`).get(this.tenantId, input.receipt_id);
39646
39769
  if (delivered)
39647
39770
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
39648
39771
  const applyResult = parseApplyResult(String(row["result_json"]), false);
@@ -39655,7 +39778,14 @@ class SqliteTodosTaskManifestBackend {
39655
39778
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
39656
39779
  ];
39657
39780
  const storedOutbox = this.db.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
39658
- FROM todos_task_manifest_outbox WHERE apply_receipt_id = ? ORDER BY id`).all(input.receipt_id);
39781
+ FROM todos_task_manifest_outbox
39782
+ WHERE apply_receipt_id = ?
39783
+ AND EXISTS (
39784
+ SELECT 1 FROM todos_task_manifest_receipts r
39785
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
39786
+ AND r.tenant_id = ?
39787
+ )
39788
+ ORDER BY id`).all(input.receipt_id, this.tenantId);
39659
39789
  if (storedOutbox.length !== expectedEffects.length) {
39660
39790
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
39661
39791
  }
@@ -39743,7 +39873,14 @@ class SqliteTodosTaskManifestBackend {
39743
39873
  }
39744
39874
  expectedVerificationIndex += 1;
39745
39875
  }
39746
- this.db.query("UPDATE todos_task_manifest_outbox SET status = 'cancelled' WHERE apply_receipt_id = ? AND status = 'pending'").run(input.receipt_id);
39876
+ this.db.query(`UPDATE todos_task_manifest_outbox
39877
+ SET status = 'cancelled'
39878
+ WHERE apply_receipt_id = ? AND status = 'pending'
39879
+ AND EXISTS (
39880
+ SELECT 1 FROM todos_task_manifest_receipts r
39881
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
39882
+ AND r.tenant_id = ?
39883
+ )`).run(input.receipt_id, this.tenantId);
39747
39884
  for (const table of ["task_tags", "task_dependencies", "task_comments", "task_verifications"]) {
39748
39885
  const column = table === "task_dependencies" ? "task_id" : "task_id";
39749
39886
  this.db.query(`DELETE FROM ${table} WHERE ${column} IN (${placeholders2})`).run(...taskIds);
@@ -39754,11 +39891,11 @@ class SqliteTodosTaskManifestBackend {
39754
39891
  const result = { duplicate: false, receipt, absent: true, readback };
39755
39892
  const resultJson = canonicalJson(result);
39756
39893
  this.db.query(`INSERT INTO todos_task_manifest_receipts (
39757
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
39894
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
39758
39895
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
39759
- ) 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);
39896
+ ) 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);
39760
39897
  const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
39761
- WHERE operation_id = ? AND state = 'applied' AND version = ?`).run(receipt.binding_version, compensationReceiptId, now4, receipt.operation_id, input.if_binding_version);
39898
+ 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);
39762
39899
  if (updated.changes !== 1) {
39763
39900
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding changed during compensation");
39764
39901
  }
@@ -39897,17 +40034,19 @@ class PostgresTodosTaskManifestBackend {
39897
40034
  kind = "postgresql";
39898
40035
  service;
39899
40036
  tableName;
40037
+ tenantId;
39900
40038
  schemaReady = null;
39901
40039
  constructor(client, options = {}) {
39902
40040
  this.client = client;
39903
40041
  this.service = options.service ?? "todos";
39904
40042
  this.tableName = safeIdentifier2(options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE, "tableName");
40043
+ this.tenantId = options.tenantId ?? "default";
39905
40044
  }
39906
40045
  async ensureSchema() {
39907
40046
  this.schemaReady ??= (async () => {
39908
40047
  for (const sql of postgresTodosSyncSchemaSql(this.tableName))
39909
40048
  await this.client.query(sql);
39910
- for (const sql of postgresTodosTaskManifestSchemaSql())
40049
+ for (const sql of postgresTodosTaskManifestSchemaSql(this.tenantId))
39911
40050
  await this.client.query(sql);
39912
40051
  })();
39913
40052
  await this.schemaReady;
@@ -39929,7 +40068,7 @@ class PostgresTodosTaskManifestBackend {
39929
40068
  const { manifest } = input;
39930
40069
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
39931
40070
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
39932
- const existing = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = $1 LIMIT 1 FOR UPDATE", [manifest.operation_id]);
40071
+ 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]);
39933
40072
  if (existing.rows[0]) {
39934
40073
  const binding = existing.rows[0];
39935
40074
  if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
@@ -39940,7 +40079,7 @@ class PostgresTodosTaskManifestBackend {
39940
40079
  }
39941
40080
  return { ...parseJson2(binding["result_json"]), duplicate: true };
39942
40081
  }
39943
- const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE idempotency_key = $1 LIMIT 1", [manifest.idempotency_key]);
40082
+ 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]);
39944
40083
  if (reused.rows[0])
39945
40084
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
39946
40085
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
@@ -40050,10 +40189,11 @@ class PostgresTodosTaskManifestBackend {
40050
40189
  const manifestJson = canonicalJson(manifest);
40051
40190
  const resultJson = canonicalJson(result);
40052
40191
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
40053
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
40192
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
40054
40193
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
40055
- ) VALUES ($1, 'todos', 'todos.task-manifest.v1', 1, 'apply', $2, $3, $4, $5, 1, NULL, $6::jsonb, $7::jsonb, $8)`, [
40194
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, 1, NULL, $7::jsonb, $8::jsonb, $9)`, [
40056
40195
  input.receipt_id,
40196
+ this.tenantId,
40057
40197
  manifest.operation_id,
40058
40198
  manifest.idempotency_key,
40059
40199
  input.request_digest,
@@ -40076,10 +40216,11 @@ class PostgresTodosTaskManifestBackend {
40076
40216
  }
40077
40217
  fault2(faults, "after_outbox_write");
40078
40218
  await tx.query(`INSERT INTO todos_task_manifest_bindings (
40079
- operation_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
40219
+ operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
40080
40220
  manifest_json, result_json, state, version, created_at, updated_at
40081
- ) VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, 'applied', 1, $8, $8)`, [
40221
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
40082
40222
  manifest.operation_id,
40223
+ this.tenantId,
40083
40224
  manifest.idempotency_key,
40084
40225
  input.request_digest,
40085
40226
  input.result_digest,
@@ -40094,17 +40235,50 @@ class PostgresTodosTaskManifestBackend {
40094
40235
  }
40095
40236
  async readExact(receiptId2) {
40096
40237
  await this.ensureSchema();
40097
- const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE receipt_id = $1 AND kind = 'apply' LIMIT 1", [receiptId2]);
40238
+ 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]);
40098
40239
  if (!result.rows[0])
40099
40240
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
40100
40241
  return { ...parseJson2(result.rows[0]["result_json"]), duplicate: false };
40101
40242
  }
40243
+ async lookupBindingByPlanId(planId) {
40244
+ await this.ensureSchema();
40245
+ const result = await this.client.query(`
40246
+ SELECT
40247
+ b.apply_receipt_id AS apply_receipt_id,
40248
+ b.state AS state,
40249
+ b.version AS binding_version,
40250
+ b.tenant_id AS binding_tenant_id,
40251
+ b.operation_id AS binding_operation_id,
40252
+ b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
40253
+ r.tenant_id AS receipt_tenant_id,
40254
+ r.authority AS receipt_authority,
40255
+ r.route AS receipt_route,
40256
+ r.schema_version AS receipt_schema_version,
40257
+ r.kind AS receipt_kind,
40258
+ r.operation_id AS receipt_operation_id,
40259
+ r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
40260
+ FROM todos_task_manifest_bindings b
40261
+ LEFT JOIN todos_task_manifest_receipts r
40262
+ ON r.receipt_id = b.apply_receipt_id
40263
+ AND r.tenant_id = b.tenant_id
40264
+ WHERE b.tenant_id = $1
40265
+ AND b.result_json #>> '{graph,plan_id}' = $2
40266
+ LIMIT 2
40267
+ `, [this.tenantId, planId]);
40268
+ return validateTaskManifestBindingLookupRows(result.rows, this.tenantId, planId);
40269
+ }
40102
40270
  async markOutboxDelivered(outboxId, deliveredAt) {
40103
40271
  await this.ensureSchema();
40104
40272
  await this.client.transaction(async (tx) => {
40105
40273
  const result = await tx.query(`UPDATE todos_task_manifest_outbox
40106
40274
  SET status = 'delivered', delivered_at = $1, attempts = attempts + 1
40107
- WHERE id = $2 AND status = 'pending' RETURNING id`, [deliveredAt, outboxId]);
40275
+ WHERE id = $2 AND status = 'pending'
40276
+ AND EXISTS (
40277
+ SELECT 1 FROM todos_task_manifest_receipts r
40278
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
40279
+ AND r.tenant_id = $3
40280
+ )
40281
+ RETURNING id`, [deliveredAt, outboxId, this.tenantId]);
40108
40282
  if (!result.rows[0])
40109
40283
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
40110
40284
  });
@@ -40114,7 +40288,10 @@ class PostgresTodosTaskManifestBackend {
40114
40288
  return this.client.transaction(async (tx) => {
40115
40289
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${receipt.operation_id}`]);
40116
40290
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fcompensation-idempotency\x1F${input.idempotency_key}`]);
40117
- 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]);
40291
+ const previous = await tx.query(`SELECT apply_receipt_id, request_digest, result_json
40292
+ FROM todos_task_manifest_receipts
40293
+ WHERE tenant_id = $1 AND kind = 'compensate' AND idempotency_key = $2
40294
+ LIMIT 1`, [this.tenantId, input.idempotency_key]);
40118
40295
  if (previous.rows[0]) {
40119
40296
  const row = previous.rows[0];
40120
40297
  if (row["apply_receipt_id"] !== input.receipt_id || row["request_digest"] !== requestDigest) {
@@ -40122,18 +40299,21 @@ class PostgresTodosTaskManifestBackend {
40122
40299
  }
40123
40300
  return { ...parseJson2(row["result_json"]), duplicate: true };
40124
40301
  }
40125
- const applyRows = await tx.query("SELECT * FROM todos_task_manifest_receipts WHERE receipt_id = $1 AND kind = 'apply' LIMIT 1", [input.receipt_id]);
40302
+ 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]);
40126
40303
  const applyRow = applyRows.rows[0];
40127
40304
  if (!applyRow)
40128
40305
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", "Apply receipt not found");
40129
- const bindingRows = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE operation_id = $1 LIMIT 1 FOR UPDATE", [receipt.operation_id]);
40306
+ 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]);
40130
40307
  const binding = bindingRows.rows[0];
40131
40308
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
40132
40309
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
40133
40310
  }
40134
40311
  if (binding["state"] !== "applied")
40135
40312
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
40136
- 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]);
40313
+ const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
40314
+ JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
40315
+ WHERE r.tenant_id = $1 AND o.apply_receipt_id = $2 AND o.status = 'delivered'
40316
+ LIMIT 1`, [this.tenantId, input.receipt_id]);
40137
40317
  if (delivered.rows[0])
40138
40318
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
40139
40319
  const applyResult = parseJson2(applyRow["result_json"]);
@@ -40146,7 +40326,14 @@ class PostgresTodosTaskManifestBackend {
40146
40326
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
40147
40327
  ];
40148
40328
  const outboxRows = await tx.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
40149
- FROM todos_task_manifest_outbox WHERE apply_receipt_id = $1 ORDER BY id`, [input.receipt_id]);
40329
+ FROM todos_task_manifest_outbox
40330
+ WHERE apply_receipt_id = $1
40331
+ AND EXISTS (
40332
+ SELECT 1 FROM todos_task_manifest_receipts r
40333
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
40334
+ AND r.tenant_id = $2
40335
+ )
40336
+ ORDER BY id`, [input.receipt_id, this.tenantId]);
40150
40337
  if (outboxRows.rows.length !== expectedEffects.length) {
40151
40338
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
40152
40339
  }
@@ -40246,7 +40433,14 @@ class PostgresTodosTaskManifestBackend {
40246
40433
  }
40247
40434
  if (stored.rows.length !== managedIds.length)
40248
40435
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: managed graph is incomplete");
40249
- await tx.query("UPDATE todos_task_manifest_outbox SET status = 'cancelled' WHERE apply_receipt_id = $1 AND status = 'pending'", [input.receipt_id]);
40436
+ await tx.query(`UPDATE todos_task_manifest_outbox
40437
+ SET status = 'cancelled'
40438
+ WHERE apply_receipt_id = $1 AND status = 'pending'
40439
+ AND EXISTS (
40440
+ SELECT 1 FROM todos_task_manifest_receipts r
40441
+ WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
40442
+ AND r.tenant_id = $2
40443
+ )`, [input.receipt_id, this.tenantId]);
40250
40444
  const typedIds = [
40251
40445
  ["dependencies", applyResult.graph.dependency_ids],
40252
40446
  ["comments", applyResult.graph.comment_ids],
@@ -40263,10 +40457,11 @@ class PostgresTodosTaskManifestBackend {
40263
40457
  const readback = await this.readback(tx, applyResult.graph);
40264
40458
  const result = { duplicate: false, receipt, absent: true, readback };
40265
40459
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
40266
- receipt_id, authority, route, schema_version, kind, operation_id, idempotency_key,
40460
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
40267
40461
  request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
40268
- ) VALUES ($1, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $2, $3, $4, $5, $6, $7, NULL, $8::jsonb, $9)`, [
40462
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10)`, [
40269
40463
  compensationReceiptId,
40464
+ this.tenantId,
40270
40465
  receipt.operation_id,
40271
40466
  input.idempotency_key,
40272
40467
  requestDigest,
@@ -40278,10 +40473,11 @@ class PostgresTodosTaskManifestBackend {
40278
40473
  ]);
40279
40474
  const updated = await tx.query(`UPDATE todos_task_manifest_bindings
40280
40475
  SET state = 'compensated', version = $1, compensation_receipt_id = $2, updated_at = $3
40281
- WHERE operation_id = $4 AND state = 'applied' AND version = $5 RETURNING operation_id`, [
40476
+ WHERE tenant_id = $4 AND operation_id = $5 AND state = 'applied' AND version = $6 RETURNING operation_id`, [
40282
40477
  receipt.binding_version,
40283
40478
  compensationReceiptId,
40284
40479
  now4,
40480
+ this.tenantId,
40285
40481
  receipt.operation_id,
40286
40482
  input.if_binding_version
40287
40483
  ]);
@@ -40321,6 +40517,13 @@ var FAULT_POINTS = [
40321
40517
  "after_outbox_write",
40322
40518
  "after_receipt_write"
40323
40519
  ];
40520
+ function resolveTenantId(value) {
40521
+ const tenantId = value ?? "default";
40522
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenantId)) {
40523
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
40524
+ }
40525
+ return tenantId;
40526
+ }
40324
40527
  function normalize(input, now4) {
40325
40528
  const parsed = parseTodosTaskManifest(input);
40326
40529
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
@@ -40408,15 +40611,18 @@ function sanitizeManifest(manifest) {
40408
40611
  class PackageOwnedTodosTaskManifestAuthority {
40409
40612
  backend;
40410
40613
  options;
40614
+ tenantId;
40411
40615
  constructor(backend, options = {}) {
40412
40616
  this.backend = backend;
40413
40617
  this.options = options;
40618
+ this.tenantId = resolveTenantId(options.tenantId);
40414
40619
  }
40415
40620
  async capability() {
40416
40621
  return {
40417
40622
  authority: "todos",
40418
40623
  route: TODOS_TASK_MANIFEST_ROUTE,
40419
40624
  schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
40625
+ tenant_id: this.tenantId,
40420
40626
  backend: this.backend.kind,
40421
40627
  deterministic_ids: true,
40422
40628
  immutable_receipts: true,
@@ -40455,6 +40661,22 @@ class PackageOwnedTodosTaskManifestAuthority {
40455
40661
  }
40456
40662
  return this.backend.readExact(receiptId2).then((result) => this.bounded(result));
40457
40663
  }
40664
+ async lookupBinding(input) {
40665
+ const request = parseTodosTaskManifestBindingLookup(input);
40666
+ if (request.max_items !== 1) {
40667
+ 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 });
40668
+ }
40669
+ if (request.authority !== "todos" || request.route !== TODOS_TASK_MANIFEST_ROUTE || request.schema_version !== TODOS_TASK_MANIFEST_SCHEMA_VERSION || request.tenant_id !== this.tenantId) {
40670
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAPABILITY_MISMATCH", "Task-manifest binding lookup does not match this authority identity");
40671
+ }
40672
+ return this.bounded({
40673
+ authority: "todos",
40674
+ route: TODOS_TASK_MANIFEST_ROUTE,
40675
+ schema_version: TODOS_TASK_MANIFEST_SCHEMA_VERSION,
40676
+ tenant_id: this.tenantId,
40677
+ ...await this.backend.lookupBindingByPlanId(request.plan_id)
40678
+ });
40679
+ }
40458
40680
  markOutboxDelivered(outboxId) {
40459
40681
  if (!outboxId || outboxId.length > 200) {
40460
40682
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "outboxId must be a bounded exact identifier");
@@ -40494,13 +40716,15 @@ function createSqliteTodosTaskManifestAuthority(options) {
40494
40716
  if (!options?.database) {
40495
40717
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An explicit SQLite Database is required");
40496
40718
  }
40497
- return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database), options);
40719
+ const tenantId = resolveTenantId(options.tenantId);
40720
+ return new PackageOwnedTodosTaskManifestAuthority(new SqliteTodosTaskManifestBackend(options.database, tenantId), { ...options, tenantId });
40498
40721
  }
40499
40722
  function createPostgresTodosTaskManifestAuthority(client, options = {}) {
40500
40723
  if (!client || typeof client.transaction !== "function") {
40501
40724
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE", "An authoritative PostgreSQL transaction(callback) client is required");
40502
40725
  }
40503
- return new PackageOwnedTodosTaskManifestAuthority(new PostgresTodosTaskManifestBackend(client, options), options);
40726
+ const tenantId = resolveTenantId(options.tenantId);
40727
+ return new PackageOwnedTodosTaskManifestAuthority(new PostgresTodosTaskManifestBackend(client, { ...options, tenantId }), { ...options, tenantId });
40504
40728
  }
40505
40729
  // src/task-manifest/http.ts
40506
40730
  var JSON_HEADERS2 = { "Content-Type": "application/json" };
@@ -40514,6 +40738,7 @@ function status(error) {
40514
40738
  case "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE":
40515
40739
  return 400;
40516
40740
  case "TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND":
40741
+ case "TODOS_TASK_MANIFEST_BINDING_NOT_FOUND":
40517
40742
  return 404;
40518
40743
  case "TODOS_TASK_MANIFEST_ATOMICITY_UNAVAILABLE":
40519
40744
  return 503;
@@ -40576,6 +40801,11 @@ async function handleTodosTaskManifestHttpRequest(request, url, authority, baseP
40576
40801
  }
40577
40802
  return json2({ result: await authority.readExact(input.receipt_id) });
40578
40803
  }
40804
+ if (action === "bindings/lookup") {
40805
+ return json2({
40806
+ result: await authority.lookupBinding(await body(request))
40807
+ });
40808
+ }
40579
40809
  if (action === "compensate") {
40580
40810
  return json2({ result: await authority.compensate(await body(request)) }, 201);
40581
40811
  }
@@ -40635,6 +40865,9 @@ class TodosTaskManifestHttpClient {
40635
40865
  async readExact(receiptId2) {
40636
40866
  return (await this.request("/read-exact", { method: "POST", body: JSON.stringify({ receipt_id: receiptId2 }) })).result;
40637
40867
  }
40868
+ async lookupBinding(input) {
40869
+ return (await this.request("/bindings/lookup", { method: "POST", body: JSON.stringify(input) })).result;
40870
+ }
40638
40871
  async markOutboxDelivered(outboxId) {
40639
40872
  await this.request("/outbox/delivered", { method: "POST", body: JSON.stringify({ outbox_id: outboxId }) });
40640
40873
  }
@@ -61988,6 +62221,7 @@ export {
61988
62221
  pauseFocusSession,
61989
62222
  patrolTasks,
61990
62223
  parseTodosTaskManifestCompensation,
62224
+ parseTodosTaskManifestBindingLookup,
61991
62225
  parseTodosTaskManifest,
61992
62226
  parseTmuxTarget,
61993
62227
  parseStorageMode,