@hasna/todos 0.15.29 → 0.15.32

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 (71) hide show
  1. package/dist/cli/cloud-router.d.ts +13 -1
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/project-registration-commands.d.ts +2 -0
  4. package/dist/cli/commands/project-registration-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
  7. package/dist/cli/index.js +2835 -334
  8. package/dist/contracts.js +52 -2
  9. package/dist/db/task-crud.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1447 -144
  13. package/dist/lib/task-parent-integrity.d.ts +14 -0
  14. package/dist/lib/task-parent-integrity.d.ts.map +1 -0
  15. package/dist/mcp/index.js +2031 -135
  16. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/project-registration/adoption-validation.d.ts +3 -0
  19. package/dist/project-registration/adoption-validation.d.ts.map +1 -0
  20. package/dist/project-registration/authority.d.ts +3 -1
  21. package/dist/project-registration/authority.d.ts.map +1 -1
  22. package/dist/project-registration/backend.d.ts +24 -1
  23. package/dist/project-registration/backend.d.ts.map +1 -1
  24. package/dist/project-registration/http.d.ts +3 -1
  25. package/dist/project-registration/http.d.ts.map +1 -1
  26. package/dist/project-registration/index.d.ts +2 -1
  27. package/dist/project-registration/index.d.ts.map +1 -1
  28. package/dist/project-registration/page-validation.d.ts +5 -0
  29. package/dist/project-registration/page-validation.d.ts.map +1 -0
  30. package/dist/project-registration/postgres.d.ts +13 -1
  31. package/dist/project-registration/postgres.d.ts.map +1 -1
  32. package/dist/project-registration/sqlite.d.ts +13 -1
  33. package/dist/project-registration/sqlite.d.ts.map +1 -1
  34. package/dist/project-registration/types.d.ts +68 -1
  35. package/dist/project-registration/types.d.ts.map +1 -1
  36. package/dist/project-registration.js +831 -58
  37. package/dist/registry.js +52 -2
  38. package/dist/release-provenance.json +5 -5
  39. package/dist/sdk/index.d.ts +1 -1
  40. package/dist/sdk/index.d.ts.map +1 -1
  41. package/dist/sdk/index.js +151 -0
  42. package/dist/sdk/v1.generated.d.ts +282 -0
  43. package/dist/sdk/v1.generated.d.ts.map +1 -1
  44. package/dist/server/index.js +2022 -126
  45. package/dist/server/openapi.d.ts +2856 -1357
  46. package/dist/server/openapi.d.ts.map +1 -1
  47. package/dist/server/v1.d.ts.map +1 -1
  48. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  49. package/dist/storage/postgres-sync.d.ts +8 -0
  50. package/dist/storage/postgres-sync.d.ts.map +1 -1
  51. package/dist/storage.js +232 -19
  52. package/dist/task-manifest/authority.d.ts +13 -1
  53. package/dist/task-manifest/authority.d.ts.map +1 -1
  54. package/dist/task-manifest/backend.d.ts +5 -0
  55. package/dist/task-manifest/backend.d.ts.map +1 -1
  56. package/dist/task-manifest/index.d.ts +2 -2
  57. package/dist/task-manifest/index.d.ts.map +1 -1
  58. package/dist/task-manifest/plan-slug.d.ts +20 -0
  59. package/dist/task-manifest/plan-slug.d.ts.map +1 -1
  60. package/dist/task-manifest/postgres.d.ts +1 -0
  61. package/dist/task-manifest/postgres.d.ts.map +1 -1
  62. package/dist/task-manifest/schema-sql.d.ts.map +1 -1
  63. package/dist/task-manifest/schema.d.ts.map +1 -1
  64. package/dist/task-manifest/sqlite.d.ts +1 -0
  65. package/dist/task-manifest/sqlite.d.ts.map +1 -1
  66. package/dist/task-manifest/types.d.ts +21 -1
  67. package/dist/task-manifest/types.d.ts.map +1 -1
  68. package/dist/task-manifest.js +592 -61
  69. package/dist/types/index.d.ts +4 -2
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7917,6 +7917,46 @@ function guardPlanRowsSqlite(planIds, db) {
7917
7917
  }
7918
7918
  }
7919
7919
 
7920
+ // src/lib/task-parent-integrity.ts
7921
+ function parentCycleError(taskId, parentId) {
7922
+ return new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentId} to task ${taskId} would create or retain a parent cycle`);
7923
+ }
7924
+ function assertTaskParentIntegrity(taskId, parentId, getTask) {
7925
+ if (parentId === undefined || parentId === null)
7926
+ return;
7927
+ const visited = new Set;
7928
+ let cursor = parentId;
7929
+ while (cursor) {
7930
+ if (cursor === taskId || visited.has(cursor)) {
7931
+ throw parentCycleError(taskId, parentId);
7932
+ }
7933
+ visited.add(cursor);
7934
+ const parent = getTask(cursor);
7935
+ if (!parent)
7936
+ throw new TaskNotFoundError(cursor);
7937
+ cursor = parent.parent_id;
7938
+ }
7939
+ }
7940
+ async function assertTaskParentIntegrityAsync(taskId, parentId, getTask) {
7941
+ if (parentId === undefined || parentId === null)
7942
+ return;
7943
+ const visited = new Set;
7944
+ let cursor = parentId;
7945
+ while (cursor) {
7946
+ if (cursor === taskId || visited.has(cursor)) {
7947
+ throw parentCycleError(taskId, parentId);
7948
+ }
7949
+ visited.add(cursor);
7950
+ const parent = await getTask(cursor);
7951
+ if (!parent)
7952
+ throw new TaskNotFoundError(cursor);
7953
+ cursor = parent.parent_id;
7954
+ }
7955
+ }
7956
+ var init_task_parent_integrity = __esm(() => {
7957
+ init_types();
7958
+ });
7959
+
7920
7960
  // src/lib/creator-identity.ts
7921
7961
  function canonicalAgentRef(value) {
7922
7962
  return value.trim().toLowerCase();
@@ -9440,6 +9480,7 @@ function createTaskStored(input, d) {
9440
9480
  let id = uuid();
9441
9481
  for (let attempt = 0;attempt < 3; attempt++) {
9442
9482
  try {
9483
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
9443
9484
  d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, created_by, assigned_from_project, task_type, machine_id)
9444
9485
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
9445
9486
  id,
@@ -9818,6 +9859,7 @@ function updateTaskStored(id, input, db) {
9818
9859
  throw new VersionConflictError(id, input.version, task.version);
9819
9860
  }
9820
9861
  input = sanitizeUpdateTaskInput(input);
9862
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
9821
9863
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
9822
9864
  const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
9823
9865
  if (linkedProjectId) {
@@ -9871,6 +9913,10 @@ function updateTaskStored(id, input, db) {
9871
9913
  sets.push("project_id = ?");
9872
9914
  params.push(input.project_id);
9873
9915
  }
9916
+ if (input.parent_id !== undefined) {
9917
+ sets.push("parent_id = ?");
9918
+ params.push(input.parent_id);
9919
+ }
9874
9920
  if (input.assigned_to !== undefined) {
9875
9921
  sets.push("assigned_to = ?");
9876
9922
  params.push(input.assigned_to);
@@ -9986,6 +10032,8 @@ function updateTaskStored(id, input, db) {
9986
10032
  logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
9987
10033
  if (input.title !== undefined && input.title !== task.title)
9988
10034
  logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
10035
+ if (input.parent_id !== undefined && input.parent_id !== task.parent_id)
10036
+ logTaskChange(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
9989
10037
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
9990
10038
  logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
9991
10039
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
@@ -10043,7 +10091,8 @@ function updateTask(id, input, db) {
10043
10091
  if (!before)
10044
10092
  throw new TaskNotFoundError(id);
10045
10093
  const guardedPlanIds = [before.plan_id, input.plan_id];
10046
- if (!guardedPlanIds.some(Boolean))
10094
+ const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
10095
+ if (!needsSerializedWrite)
10047
10096
  return updateTaskStored(id, input, d);
10048
10097
  return d.transaction(() => {
10049
10098
  guardPlanRowsSqlite(guardedPlanIds, d);
@@ -10079,6 +10128,7 @@ var init_task_crud = __esm(() => {
10079
10128
  init_checklists();
10080
10129
  init_storage_tombstones();
10081
10130
  init_prewrite_secrets();
10131
+ init_task_parent_integrity();
10082
10132
  });
10083
10133
 
10084
10134
  // src/db/task-status.ts
@@ -12783,7 +12833,7 @@ var init_dispatches = __esm(() => {
12783
12833
  // package.json
12784
12834
  var package_default = {
12785
12835
  name: "@hasna/todos",
12786
- version: "0.15.29",
12836
+ version: "0.15.32",
12787
12837
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12788
12838
  type: "module",
12789
12839
  main: "dist/index.js",
@@ -27505,6 +27555,7 @@ function assertSafeIdentifier(value) {
27505
27555
 
27506
27556
  // src/storage/postgres-adapter.ts
27507
27557
  init_redaction();
27558
+ init_task_parent_integrity();
27508
27559
 
27509
27560
  // src/task-manifest/canonical.ts
27510
27561
  import { createHash as createHash11 } from "crypto";
@@ -27548,8 +27599,8 @@ function createPostgresTodosStorageAdapter(options) {
27548
27599
  resolveRef: (ref) => store.resolveTaskRef(ref),
27549
27600
  list: (filter = {}) => store.listTasks(filter),
27550
27601
  count: (filter = {}) => store.countTasks(filter),
27551
- update: (id, input) => updateTask2(id, input, store),
27552
- delete: (id, context) => store.delete("tasks", id, context),
27602
+ update: (id, input, context) => updateTask2(id, input, store, context),
27603
+ delete: (id, context) => store.deleteTaskHierarchy(id, context),
27553
27604
  start: (id, agentId) => startTask2(id, agentId, store),
27554
27605
  complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
27555
27606
  fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
@@ -28082,22 +28133,70 @@ class PostgresJsonRecordStore {
28082
28133
  return "identical";
28083
28134
  throw new Error(divergentAuditHistoryReplayError(value.id));
28084
28135
  }
28085
- async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
28136
+ async withTaskParentIntegrityTransaction(fn) {
28137
+ if (typeof this.options.client.transaction !== "function") {
28138
+ throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
28139
+ }
28140
+ return this.options.client.transaction(async (client) => {
28141
+ await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
28142
+ return fn(client);
28143
+ });
28144
+ }
28145
+ async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
28086
28146
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
28087
- if (planIds.length === 0)
28147
+ if (planIds.length === 0 && !parentGuard)
28088
28148
  return this.upsert("tasks", value, context);
28089
28149
  await this.ensureSchema();
28150
+ if (parentGuard && !queryClient) {
28151
+ return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
28152
+ }
28153
+ const client = queryClient ?? this.options.client;
28090
28154
  const updatedAt = value.updated_at;
28091
28155
  const targetPlanId = value.plan_id;
28092
- const result = await this.options.client.query(`/* todos:task-plan-membership-guard */ WITH
28156
+ const result = await client.query(`/* todos:task-plan-membership-guard todos:task-parent-integrity-guard */ WITH RECURSIVE
28157
+ locked_task AS MATERIALIZED (
28158
+ SELECT payload FROM ${this.tableName}
28159
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
28160
+ FOR UPDATE
28161
+ ),
28093
28162
  locked_plans AS MATERIALIZED (
28094
28163
  SELECT object_id, payload FROM ${this.tableName}
28095
28164
  WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
28096
28165
  AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
28097
28166
  ORDER BY object_id
28098
28167
  FOR UPDATE
28168
+ ), parent_chain(object_id, payload, path, cycle) AS (
28169
+ SELECT parent.object_id, parent.payload, ARRAY[parent.object_id], false
28170
+ FROM ${this.tableName} AS parent
28171
+ WHERE $10::boolean
28172
+ AND $11::text IS NOT NULL
28173
+ AND parent.service = $1
28174
+ AND parent.object_type = 'tasks'
28175
+ AND parent.object_id = $11
28176
+ AND parent.deleted_at IS NULL
28177
+ UNION ALL
28178
+ SELECT ancestor.object_id,
28179
+ ancestor.payload,
28180
+ chain.path || ancestor.object_id,
28181
+ ancestor.object_id = ANY(chain.path)
28182
+ FROM parent_chain AS chain
28183
+ JOIN ${this.tableName} AS ancestor
28184
+ ON ancestor.service = $1
28185
+ AND ancestor.object_type = 'tasks'
28186
+ AND ancestor.object_id = chain.payload->>'parent_id'
28187
+ AND ancestor.deleted_at IS NULL
28188
+ WHERE NOT chain.cycle
28099
28189
  ), validation AS (
28100
28190
  SELECT
28191
+ (NOT $10::boolean OR NOT $13::boolean OR EXISTS (SELECT 1 FROM locked_task)) AS task_found,
28192
+ (NOT $10::boolean OR NOT $13::boolean
28193
+ OR (SELECT (payload->>'version')::integer FROM locked_task) = $12::integer) AS version_matches,
28194
+ (NOT $10::boolean OR $11::text IS NULL
28195
+ OR EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $11)) AS parent_found,
28196
+ (NOT $10::boolean OR $11::text IS NULL
28197
+ OR ($11::text <> $2
28198
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
28199
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
28101
28200
  (SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
28102
28201
  ($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
28103
28202
  (SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
@@ -28118,7 +28217,13 @@ class PostgresJsonRecordStore {
28118
28217
  )
28119
28218
  SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
28120
28219
  FROM guarded
28121
- WHERE guarded.all_plans_found AND guarded.target_plan_found AND NOT guarded.project_conflict
28220
+ WHERE guarded.task_found
28221
+ AND guarded.version_matches
28222
+ AND guarded.parent_found
28223
+ AND guarded.parent_acyclic
28224
+ AND guarded.all_plans_found
28225
+ AND guarded.target_plan_found
28226
+ AND NOT guarded.project_conflict
28122
28227
  ON CONFLICT (service, object_type, object_id) DO UPDATE SET
28123
28228
  payload = EXCLUDED.payload,
28124
28229
  updated_at = EXCLUDED.updated_at,
@@ -28131,8 +28236,10 @@ class PostgresJsonRecordStore {
28131
28236
  AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
28132
28237
  RETURNING payload
28133
28238
  )
28134
- SELECT guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
28135
- (SELECT payload FROM stored) AS payload
28239
+ SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
28240
+ guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
28241
+ (SELECT payload FROM stored) AS payload,
28242
+ (SELECT payload FROM locked_task) AS current_payload
28136
28243
  FROM guarded`, [
28137
28244
  this.service,
28138
28245
  value.id,
@@ -28142,9 +28249,26 @@ class PostgresJsonRecordStore {
28142
28249
  numberValue3(value.version),
28143
28250
  jsonbParam(planIds),
28144
28251
  targetPlanId,
28145
- explicitProject
28252
+ explicitProject,
28253
+ Boolean(parentGuard),
28254
+ parentGuard?.parentId ?? null,
28255
+ parentGuard?.expectedVersion ?? null,
28256
+ parentGuard?.operation === "update"
28146
28257
  ]);
28147
28258
  const row = result.rows[0];
28259
+ if (parentGuard && !row?.task_found) {
28260
+ throw new TaskNotFoundError(value.id);
28261
+ }
28262
+ if (parentGuard && !row?.version_matches) {
28263
+ const current = row?.current_payload ? payloadRecord2(row.current_payload) : await this.get("tasks", value.id);
28264
+ throw new VersionConflictError(value.id, parentGuard.expectedVersion, current?.version ?? -1);
28265
+ }
28266
+ if (parentGuard && !row?.parent_found && parentGuard.parentId) {
28267
+ throw new TaskNotFoundError(parentGuard.parentId);
28268
+ }
28269
+ if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
28270
+ throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
28271
+ }
28148
28272
  if (!row?.all_plans_found || !row.target_plan_found) {
28149
28273
  throw new PlanProjectLinkError("PLAN_PROJECT_LINK_PLAN_NOT_FOUND", `Plan membership changed through a missing plan: ${targetPlanId ?? planIds.join(", ")}`, { plan_ids: planIds, target_plan_id: targetPlanId });
28150
28274
  }
@@ -28152,6 +28276,8 @@ class PostgresJsonRecordStore {
28152
28276
  throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
28153
28277
  }
28154
28278
  if (!row.payload) {
28279
+ if (row.current_payload)
28280
+ return payloadRecord2(row.current_payload);
28155
28281
  return await requireRecord("tasks", value.id, this);
28156
28282
  }
28157
28283
  return payloadRecord2(row.payload);
@@ -28477,6 +28603,80 @@ class PostgresJsonRecordStore {
28477
28603
  version: numberValue3(existing["version"])
28478
28604
  }, context);
28479
28605
  }
28606
+ async deleteTaskHierarchy(id, context = {}) {
28607
+ await this.ensureSchema();
28608
+ return this.withTaskParentIntegrityTransaction(async (client) => {
28609
+ const timestamp3 = new Date().toISOString();
28610
+ const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
28611
+ task_tree(object_id, path, cycle) AS (
28612
+ SELECT task.object_id, ARRAY[task.object_id], false
28613
+ FROM ${this.tableName} AS task
28614
+ WHERE task.service = $1
28615
+ AND task.object_type = 'tasks'
28616
+ AND task.object_id = $2
28617
+ AND task.deleted_at IS NULL
28618
+ UNION ALL
28619
+ SELECT child.object_id,
28620
+ tree.path || child.object_id,
28621
+ child.object_id = ANY(tree.path)
28622
+ FROM task_tree AS tree
28623
+ JOIN ${this.tableName} AS child
28624
+ ON child.service = $1
28625
+ AND child.object_type = 'tasks'
28626
+ AND child.payload->>'parent_id' = tree.object_id
28627
+ AND child.deleted_at IS NULL
28628
+ WHERE NOT tree.cycle
28629
+ ), tombstoned AS (
28630
+ UPDATE ${this.tableName} AS task
28631
+ SET deleted_at = $3::timestamptz,
28632
+ updated_at = $3::timestamptz,
28633
+ source_machine_id = $4
28634
+ WHERE task.service = $1
28635
+ AND task.object_type = 'tasks'
28636
+ AND task.deleted_at IS NULL
28637
+ AND task.object_id IN (
28638
+ SELECT object_id FROM task_tree WHERE NOT cycle
28639
+ )
28640
+ RETURNING task.object_id
28641
+ ), tombstoned_related AS (
28642
+ UPDATE ${this.tableName} AS related
28643
+ SET deleted_at = $3::timestamptz,
28644
+ updated_at = $3::timestamptz,
28645
+ source_machine_id = $4
28646
+ WHERE related.service = $1
28647
+ AND related.deleted_at IS NULL
28648
+ AND (
28649
+ (
28650
+ related.object_type = 'dependencies'
28651
+ AND (
28652
+ related.payload->>'task_id' IN (
28653
+ SELECT object_id FROM task_tree WHERE NOT cycle
28654
+ )
28655
+ OR related.payload->>'depends_on' IN (
28656
+ SELECT object_id FROM task_tree WHERE NOT cycle
28657
+ )
28658
+ )
28659
+ )
28660
+ OR (
28661
+ related.object_type IN ('comments', 'verifications', 'commits', 'refs')
28662
+ AND related.payload->>'task_id' IN (
28663
+ SELECT object_id FROM task_tree WHERE NOT cycle
28664
+ )
28665
+ )
28666
+ )
28667
+ RETURNING related.object_id
28668
+ )
28669
+ SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
28670
+ (SELECT count(*) FROM tombstoned) AS deleted_count,
28671
+ (SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
28672
+ this.service,
28673
+ id,
28674
+ timestamp3,
28675
+ context.requestId ?? this.sourceMachineId ?? null
28676
+ ]);
28677
+ return Boolean(result.rows[0]?.found);
28678
+ });
28679
+ }
28480
28680
  async getPlanProjectLinkReceipt(receiptId) {
28481
28681
  const value = await this.get("plan_project_link_receipts", receiptId);
28482
28682
  return value ? assertPlanProjectLinkReceipt(value) : null;
@@ -28897,9 +29097,8 @@ class PostgresJsonRecordStore {
28897
29097
  }
28898
29098
  async function createTask2(input, store, context) {
28899
29099
  const timestamp3 = new Date().toISOString();
28900
- if (input.parent_id && !await store.get("tasks", input.parent_id)) {
28901
- throw new TaskNotFoundError(input.parent_id);
28902
- }
29100
+ const taskId = randomUUID3();
29101
+ await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
28903
29102
  const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
28904
29103
  const requestedProjectId = input.project_id ?? context?.projectId ?? null;
28905
29104
  if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
@@ -28908,7 +29107,7 @@ async function createTask2(input, store, context) {
28908
29107
  const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
28909
29108
  const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
28910
29109
  const task2 = {
28911
- id: randomUUID3(),
29110
+ id: taskId,
28912
29111
  short_id: shortId,
28913
29112
  project_id: effectiveProjectId,
28914
29113
  parent_id: input.parent_id ?? null,
@@ -28964,15 +29163,16 @@ async function createTask2(input, store, context) {
28964
29163
  synced_at: null,
28965
29164
  archived_at: null
28966
29165
  };
28967
- const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, task2.plan_id ? [task2.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context);
29166
+ const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, task2.plan_id ? [task2.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context, input.parent_id ? { operation: "create", expectedVersion: 0, parentId: input.parent_id } : undefined);
28968
29167
  await logTaskChange2(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
28969
29168
  return storedTask;
28970
29169
  }
28971
- async function updateTask2(id, input, store) {
29170
+ async function updateTask2(id, input, store, context) {
28972
29171
  const existing = await requireRecord("tasks", id, store);
28973
29172
  if (existing.version !== input.version) {
28974
- throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
29173
+ throw new VersionConflictError(id, input.version, existing.version);
28975
29174
  }
29175
+ await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
28976
29176
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
28977
29177
  const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
28978
29178
  if (linkedPlan?.project_id) {
@@ -28997,10 +29197,19 @@ async function updateTask2(id, input, store) {
28997
29197
  metadata: input.metadata ?? existing.metadata,
28998
29198
  requires_approval: input.requires_approval ?? existing.requires_approval,
28999
29199
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
29200
+ parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
29000
29201
  created_by: existing.created_by,
29001
29202
  completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
29002
29203
  };
29003
- return store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined);
29204
+ const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
29205
+ operation: "update",
29206
+ expectedVersion: input.version,
29207
+ parentId: input.parent_id !== undefined ? input.parent_id : existing.parent_id
29208
+ });
29209
+ if (input.parent_id !== undefined && input.parent_id !== existing.parent_id) {
29210
+ await logTaskChange2(id, "update", "parent_id", existing.parent_id, input.parent_id, existing.assigned_to ?? existing.agent_id, store, context);
29211
+ }
29212
+ return storedTask;
29004
29213
  }
29005
29214
  async function startTask2(id, agentId, store) {
29006
29215
  const task2 = await requireRecord("tasks", id, store);
@@ -29064,7 +29273,11 @@ async function patchTask(task2, patch, store) {
29064
29273
  version: task2.version + 1,
29065
29274
  updated_at: new Date().toISOString()
29066
29275
  };
29067
- return store.upsertTaskWithPlanMembershipGuard(updated, [task2.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"));
29276
+ return store.upsertTaskWithPlanMembershipGuard(updated, [task2.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"), {}, {
29277
+ operation: "update",
29278
+ expectedVersion: task2.version,
29279
+ parentId: updated.parent_id
29280
+ });
29068
29281
  }
29069
29282
  var CLOUD_LOCK_EXPIRY_MINUTES = 30;
29070
29283
  function sameCloudLockHolder(stored, incoming) {
@@ -34184,7 +34397,7 @@ function createLocalPrGroupLedger(db = getDatabase()) {
34184
34397
  return new PrGroupLedger(new SqlitePrGroupLedgerPersistence(db));
34185
34398
  }
34186
34399
  // src/project-registration/authority.ts
34187
- import { createHash as createHash14 } from "crypto";
34400
+ import { createHash as createHash15 } from "crypto";
34188
34401
  // src/project-registration/types.ts
34189
34402
  var TODOS_PROJECT_REGISTRATION_ROUTE = "todos.project-registration.v1";
34190
34403
  var TODOS_PROJECT_REGISTRATION_CALLER_ROUTE = "projects.full-registration.v1";
@@ -34478,6 +34691,7 @@ class PostgresTodosProjectRegistrationTransaction {
34478
34691
  AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
34479
34692
  ORDER BY payload->>'created_at' ASC, object_id ASC
34480
34693
  LIMIT 1
34694
+ FOR UPDATE
34481
34695
  `, [this.service, path, taskListSlug]);
34482
34696
  return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
34483
34697
  }
@@ -34488,6 +34702,7 @@ class PostgresTodosProjectRegistrationTransaction {
34488
34702
  AND payload->>'project_id' = $2 AND payload->>'slug' = $3
34489
34703
  ORDER BY payload->>'created_at' ASC, object_id ASC
34490
34704
  LIMIT 1
34705
+ FOR UPDATE
34491
34706
  `, [this.service, projectId, slug]);
34492
34707
  return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
34493
34708
  }
@@ -34498,10 +34713,24 @@ class PostgresTodosProjectRegistrationTransaction {
34498
34713
  return await this.storage.taskLists.create(input);
34499
34714
  }
34500
34715
  async getProject(id) {
34501
- return await this.storage.projects.get(id);
34716
+ const result = await this.client.query(`
34717
+ SELECT payload FROM ${this.tableName}
34718
+ WHERE service = $1 AND object_type = 'projects' AND object_id = $2
34719
+ AND deleted_at IS NULL
34720
+ LIMIT 1
34721
+ FOR SHARE
34722
+ `, [this.service, id]);
34723
+ return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
34502
34724
  }
34503
34725
  async getTaskList(id) {
34504
- return await this.storage.taskLists.get(id);
34726
+ const result = await this.client.query(`
34727
+ SELECT payload FROM ${this.tableName}
34728
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
34729
+ AND deleted_at IS NULL
34730
+ LIMIT 1
34731
+ FOR SHARE
34732
+ `, [this.service, id]);
34733
+ return result.rows[0] ? parsePayload3(result.rows[0].payload) : null;
34505
34734
  }
34506
34735
  async lockCompensationWrites() {
34507
34736
  await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
@@ -34581,11 +34810,102 @@ class PostgresTodosProjectRegistrationBackend {
34581
34810
  async getTaskList(id) {
34582
34811
  return (await this.direct()).getTaskList(id);
34583
34812
  }
34813
+ async getProjectResourceCollectionRevision(input) {
34814
+ await this.ensureSchema();
34815
+ const result = await this.client.query(`
34816
+ WITH resources(kind_rank, target_id, revision) AS (
34817
+ SELECT 0, object_id, COALESCE(payload->>'updated_at', updated_at::text)
34818
+ FROM ${this.tableName}
34819
+ WHERE service = $1 AND object_type = 'projects'
34820
+ AND deleted_at IS NULL AND object_id = $2
34821
+ UNION ALL
34822
+ SELECT 1, object_id, COALESCE(payload->>'updated_at', updated_at::text)
34823
+ FROM ${this.tableName}
34824
+ WHERE service = $1 AND object_type = 'task_lists'
34825
+ AND deleted_at IS NULL AND object_id = $3
34826
+ AND payload->>'project_id' = $2
34827
+ UNION ALL
34828
+ SELECT 2, object_id, COALESCE(payload->>'updated_at', updated_at::text)
34829
+ FROM ${this.tableName}
34830
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
34831
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
34832
+ UNION ALL
34833
+ SELECT 3, object_id, COALESCE(payload->>'updated_at', updated_at::text)
34834
+ FROM ${this.tableName}
34835
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
34836
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
34837
+ )
34838
+ SELECT 'md5:' || md5(COALESCE(string_agg(
34839
+ kind_rank::text || chr(31) || target_id || chr(31) || revision,
34840
+ chr(30) ORDER BY kind_rank ASC, target_id ASC
34841
+ ), '')) AS revision
34842
+ FROM resources
34843
+ `, [
34844
+ this.service,
34845
+ input.todos_project_id,
34846
+ input.task_list_id,
34847
+ input.include_anchors
34848
+ ]);
34849
+ const revision = result.rows[0]?.revision;
34850
+ if (!revision) {
34851
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "could not derive the hosted project-resource collection revision");
34852
+ }
34853
+ return revision;
34854
+ }
34855
+ async listProjectResourceCandidates(input) {
34856
+ await this.ensureSchema();
34857
+ const afterRank = input.after?.kind_rank ?? -1;
34858
+ const afterId = input.after?.target_id ?? "";
34859
+ const result = await this.client.query(`
34860
+ WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
34861
+ SELECT 'project'::text, 0, object_id, NULL::text,
34862
+ COALESCE(payload->>'updated_at', updated_at::text)
34863
+ FROM ${this.tableName}
34864
+ WHERE service = $1 AND object_type = 'projects'
34865
+ AND deleted_at IS NULL AND object_id = $2
34866
+ UNION ALL
34867
+ SELECT 'task_list'::text, 1, object_id, payload->>'project_id',
34868
+ COALESCE(payload->>'updated_at', updated_at::text)
34869
+ FROM ${this.tableName}
34870
+ WHERE service = $1 AND object_type = 'task_lists'
34871
+ AND deleted_at IS NULL AND object_id = $3
34872
+ AND payload->>'project_id' = $2
34873
+ UNION ALL
34874
+ SELECT 'plan'::text, 2, object_id, payload->>'project_id',
34875
+ COALESCE(payload->>'updated_at', updated_at::text)
34876
+ FROM ${this.tableName}
34877
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
34878
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
34879
+ UNION ALL
34880
+ SELECT 'task'::text, 3, object_id,
34881
+ COALESCE(payload->>'plan_id', payload->>'project_id'),
34882
+ COALESCE(payload->>'updated_at', updated_at::text)
34883
+ FROM ${this.tableName}
34884
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
34885
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
34886
+ )
34887
+ SELECT kind, kind_rank, target_id, parent_id, revision
34888
+ FROM resources
34889
+ WHERE kind_rank > $5 OR (kind_rank = $5 AND target_id > $6)
34890
+ ORDER BY kind_rank ASC, target_id ASC
34891
+ LIMIT $7
34892
+ `, [
34893
+ this.service,
34894
+ input.todos_project_id,
34895
+ input.task_list_id,
34896
+ input.include_anchors,
34897
+ afterRank,
34898
+ afterId,
34899
+ input.limit
34900
+ ]);
34901
+ return result.rows;
34902
+ }
34584
34903
  }
34585
34904
 
34586
34905
  // src/project-registration/sqlite.ts
34587
34906
  init_database();
34588
34907
  init_storage_tombstones();
34908
+ import { createHash as createHash14 } from "crypto";
34589
34909
  var sqliteTransactionTails2 = new WeakMap;
34590
34910
  var PROJECT_REFERENCE_COLUMNS = new Set([
34591
34911
  "project_id",
@@ -35239,6 +35559,64 @@ class SqliteTodosProjectRegistrationBackend {
35239
35559
  getTaskList(id) {
35240
35560
  return this.direct.getTaskList(id);
35241
35561
  }
35562
+ async getProjectResourceCollectionRevision(input) {
35563
+ const digest = createHash14("sha256");
35564
+ const rows = this.db.query(`
35565
+ WITH resources(kind_rank, target_id, revision) AS (
35566
+ SELECT 0, id, updated_at
35567
+ FROM projects
35568
+ WHERE id = ?
35569
+ UNION ALL
35570
+ SELECT 1, id, updated_at
35571
+ FROM task_lists
35572
+ WHERE id = ? AND project_id = ?
35573
+ UNION ALL
35574
+ SELECT 2, id, updated_at
35575
+ FROM plans
35576
+ WHERE ? = 1 AND project_id = ?
35577
+ UNION ALL
35578
+ SELECT 3, id, updated_at
35579
+ FROM tasks
35580
+ WHERE ? = 1 AND project_id = ?
35581
+ )
35582
+ SELECT kind_rank, target_id, revision
35583
+ FROM resources
35584
+ ORDER BY kind_rank ASC, target_id ASC
35585
+ `).iterate(input.todos_project_id, input.task_list_id, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id);
35586
+ for (const row of rows) {
35587
+ digest.update(`${row.kind_rank}\x00${row.target_id}\x00${row.revision}
35588
+ `);
35589
+ }
35590
+ return `sha256:${digest.digest("hex")}`;
35591
+ }
35592
+ async listProjectResourceCandidates(input) {
35593
+ const afterRank = input.after?.kind_rank ?? -1;
35594
+ const afterId = input.after?.target_id ?? "";
35595
+ return this.db.query(`
35596
+ WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
35597
+ SELECT 'project', 0, id, NULL, updated_at
35598
+ FROM projects
35599
+ WHERE id = ?
35600
+ UNION ALL
35601
+ SELECT 'task_list', 1, id, project_id, updated_at
35602
+ FROM task_lists
35603
+ WHERE id = ? AND project_id = ?
35604
+ UNION ALL
35605
+ SELECT 'plan', 2, id, project_id, updated_at
35606
+ FROM plans
35607
+ WHERE ? = 1 AND project_id = ?
35608
+ UNION ALL
35609
+ SELECT 'task', 3, id, COALESCE(plan_id, project_id), updated_at
35610
+ FROM tasks
35611
+ WHERE ? = 1 AND project_id = ?
35612
+ )
35613
+ SELECT kind, kind_rank, target_id, parent_id, revision
35614
+ FROM resources
35615
+ WHERE kind_rank > ? OR (kind_rank = ? AND target_id > ?)
35616
+ ORDER BY kind_rank ASC, target_id ASC
35617
+ LIMIT ?
35618
+ `).all(input.todos_project_id, input.task_list_id, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, input.include_anchors ? 1 : 0, input.todos_project_id, afterRank, afterRank, afterId, input.limit);
35619
+ }
35242
35620
  }
35243
35621
 
35244
35622
  // src/project-registration/authority.ts
@@ -35250,6 +35628,8 @@ var AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
35250
35628
  var PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
35251
35629
  var SHA256_PATTERN = /^[0-9a-f]{64}$/;
35252
35630
  var IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
35631
+ var PROJECT_RESOURCE_PAGE_LIMIT = 500;
35632
+ var PROJECT_RESOURCE_CURSOR_VERSION = 1;
35253
35633
 
35254
35634
  class WriteBoundaryError extends Error {
35255
35635
  point;
@@ -35277,7 +35657,7 @@ function canonicalize4(value) {
35277
35657
  return out;
35278
35658
  }
35279
35659
  function digestProjectRegistrationValue(value) {
35280
- return createHash14("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
35660
+ return createHash15("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
35281
35661
  }
35282
35662
  function deriveTodosProjectRegistrationIdempotencyKey(input) {
35283
35663
  return `prk_${digestProjectRegistrationValue({
@@ -35389,35 +35769,68 @@ function projectRecord(project) {
35389
35769
  return {
35390
35770
  target_id: project.id,
35391
35771
  revision: project.updated_at,
35392
- digest: digestProjectRegistrationValue({
35393
- id: project.id,
35394
- name: project.name,
35395
- path: project.path,
35396
- description: project.description,
35397
- task_list_id: project.task_list_id,
35398
- task_prefix: project.task_prefix,
35399
- task_counter: project.task_counter,
35400
- created_at: project.created_at,
35401
- updated_at: project.updated_at
35402
- })
35772
+ digest: projectRegistrationDigest(project)
35403
35773
  };
35404
35774
  }
35405
35775
  function taskListRecord(taskList) {
35406
35776
  return {
35407
35777
  target_id: taskList.id,
35408
35778
  revision: taskList.updated_at,
35409
- digest: digestProjectRegistrationValue({
35410
- id: taskList.id,
35411
- project_id: taskList.project_id,
35412
- slug: taskList.slug,
35413
- name: taskList.name,
35414
- description: taskList.description,
35415
- metadata: taskList.metadata,
35416
- created_at: taskList.created_at,
35417
- updated_at: taskList.updated_at
35779
+ digest: taskListRegistrationDigest(taskList)
35780
+ };
35781
+ }
35782
+ function boundExistingProjectRecord(project) {
35783
+ return {
35784
+ target_id: project.id,
35785
+ revision: project.created_at,
35786
+ digest: projectRegistrationDigest({
35787
+ ...project,
35788
+ updated_at: project.created_at
35418
35789
  })
35419
35790
  };
35420
35791
  }
35792
+ function boundExistingTaskListRecord(taskList) {
35793
+ return {
35794
+ target_id: taskList.id,
35795
+ revision: taskList.created_at,
35796
+ digest: taskListRegistrationDigest({
35797
+ ...taskList,
35798
+ updated_at: taskList.created_at
35799
+ })
35800
+ };
35801
+ }
35802
+ function projectRegistrationDigest(project) {
35803
+ return digestProjectRegistrationValue({
35804
+ id: project.id,
35805
+ name: project.name,
35806
+ path: project.path,
35807
+ description: project.description,
35808
+ task_list_id: project.task_list_id,
35809
+ task_prefix: project.task_prefix,
35810
+ task_counter: project.task_counter,
35811
+ created_at: project.created_at,
35812
+ updated_at: project.updated_at
35813
+ });
35814
+ }
35815
+ function taskListRegistrationDigest(taskList) {
35816
+ return digestProjectRegistrationValue({
35817
+ id: taskList.id,
35818
+ project_id: taskList.project_id,
35819
+ slug: taskList.slug,
35820
+ name: taskList.name,
35821
+ description: taskList.description,
35822
+ metadata: taskList.metadata,
35823
+ created_at: taskList.created_at,
35824
+ updated_at: taskList.updated_at
35825
+ });
35826
+ }
35827
+ function canonicalValuesEqual(left, right) {
35828
+ try {
35829
+ return canonicalProjectRegistrationJson(left) === canonicalProjectRegistrationJson(right);
35830
+ } catch {
35831
+ return false;
35832
+ }
35833
+ }
35421
35834
  function receiptId(input) {
35422
35835
  return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
35423
35836
  }
@@ -35455,9 +35868,37 @@ function normalizedCallDigest(request) {
35455
35868
  project_slug: request.project_slug,
35456
35869
  project_name: request.project_name,
35457
35870
  desired: request.desired,
35871
+ bind_existing: request.bind_existing === true,
35458
35872
  accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
35459
35873
  });
35460
35874
  }
35875
+ function legacyNormalizedCallDigestBeforeBindExisting(request) {
35876
+ return digestProjectRegistrationValue({
35877
+ authority_route: request.authority_route,
35878
+ package_version: request.package_version,
35879
+ authority_id: request.authority_id,
35880
+ tenant_id: request.tenant_id,
35881
+ corpus_id: request.corpus_id,
35882
+ operation_id: request.operation_id,
35883
+ step_id: request.step_id,
35884
+ resource_kind: request.resource_kind,
35885
+ direction: request.direction,
35886
+ target_selector: request.target_selector,
35887
+ idempotency_key: request.idempotency_key,
35888
+ request_digest: request.request_digest,
35889
+ precondition_digest: request.precondition_digest,
35890
+ project_id: request.project_id,
35891
+ project_slug: request.project_slug,
35892
+ project_name: request.project_name,
35893
+ desired: request.desired,
35894
+ accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
35895
+ });
35896
+ }
35897
+ function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
35898
+ if (accepted.normalized_call_digest === callDigest)
35899
+ return true;
35900
+ return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
35901
+ }
35461
35902
  function assertCommonRequest(request, capability2) {
35462
35903
  assertBounds(request);
35463
35904
  assertResourceKind(request.resource_kind);
@@ -35499,6 +35940,9 @@ function assertCommonRequest(request, capability2) {
35499
35940
  if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
35500
35941
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
35501
35942
  }
35943
+ if (request.bind_existing !== undefined && typeof request.bind_existing !== "boolean") {
35944
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "bind_existing must be boolean when supplied");
35945
+ }
35502
35946
  const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
35503
35947
  operation_id: request.operation_id,
35504
35948
  step_id: request.step_id,
@@ -35520,7 +35964,7 @@ function assertForwardRequest(request, capability2) {
35520
35964
  const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
35521
35965
  const expectedPreconditionDigest = digestProjectRegistrationValue({
35522
35966
  target_selector: request.target_selector,
35523
- expected: "absent"
35967
+ expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
35524
35968
  });
35525
35969
  if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
35526
35970
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
@@ -35599,7 +36043,7 @@ function receiptBase(request, callDigest, capability2) {
35599
36043
  normalized_call_digest: callDigest
35600
36044
  };
35601
36045
  }
35602
- function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt2) {
36046
+ function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt2, createdByOperation = true) {
35603
36047
  return makeReceipt({
35604
36048
  ...receiptBase(request, callDigest, capability2),
35605
36049
  outcome: "accepted",
@@ -35609,7 +36053,7 @@ function makeAcceptedReceipt(request, callDigest, capability2, record, createdAt
35609
36053
  result_digest: record.digest,
35610
36054
  duplicate_of_receipt_id: null,
35611
36055
  accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
35612
- created_by_operation: true
36056
+ created_by_operation: createdByOperation
35613
36057
  }, createdAt2);
35614
36058
  }
35615
36059
  function makeDuplicateReceipt(request, callDigest, capability2, accepted, createdAt2) {
@@ -35671,6 +36115,53 @@ function bindingFor(request, callDigest, timestamp3, capability2) {
35671
36115
  updated_at: timestamp3
35672
36116
  };
35673
36117
  }
36118
+ function encodeProjectResourceCursor(input) {
36119
+ return Buffer.from(JSON.stringify({
36120
+ version: PROJECT_RESOURCE_CURSOR_VERSION,
36121
+ ...input
36122
+ }), "utf8").toString("base64url");
36123
+ }
36124
+ function decodeProjectResourceCursor(cursor, expected) {
36125
+ if (!cursor)
36126
+ return null;
36127
+ let parsed;
36128
+ try {
36129
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
36130
+ } catch {
36131
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
36132
+ }
36133
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
36134
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
36135
+ }
36136
+ const value = parsed;
36137
+ if (value["version"] !== PROJECT_RESOURCE_CURSOR_VERSION || value["source_project_id"] !== expected.source_project_id || value["include_anchors"] !== expected.include_anchors || !Number.isSafeInteger(value["kind_rank"]) || Number(value["kind_rank"]) < 0 || Number(value["kind_rank"]) > 3 || typeof value["target_id"] !== "string" || !UUID_PATTERN.test(value["target_id"]) || typeof value["collection_revision"] !== "string" || value["collection_revision"].length < 16 || value["collection_revision"].length > 128) {
36138
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor does not match this project-resource query");
36139
+ }
36140
+ return {
36141
+ kind_rank: Number(value["kind_rank"]),
36142
+ target_id: value["target_id"],
36143
+ collection_revision: value["collection_revision"]
36144
+ };
36145
+ }
36146
+ function projectResourceFromCandidate(sourceProjectId, candidate) {
36147
+ const scope = candidate.kind === "project" || candidate.kind === "task_list" ? "collection" : "resource";
36148
+ return {
36149
+ source_project_id: sourceProjectId,
36150
+ kind: candidate.kind,
36151
+ scope,
36152
+ target_id: candidate.target_id,
36153
+ parent_id: candidate.parent_id,
36154
+ revision: candidate.revision,
36155
+ digest: digestProjectRegistrationValue({
36156
+ source_project_id: sourceProjectId,
36157
+ kind: candidate.kind,
36158
+ scope,
36159
+ target_id: candidate.target_id,
36160
+ parent_id: candidate.parent_id,
36161
+ revision: candidate.revision
36162
+ })
36163
+ };
36164
+ }
35674
36165
 
35675
36166
  class PackageOwnedTodosProjectRegistrationAuthority {
35676
36167
  backend;
@@ -35692,6 +36183,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35692
36183
  immutable_receipts: true,
35693
36184
  exact_terminal_lookup: true,
35694
36185
  exact_readback: true,
36186
+ bind_existing_adoption: true,
36187
+ prior_registration_adoption_validation: true,
36188
+ project_resource_enumeration: true,
36189
+ project_resource_page_limit: PROJECT_RESOURCE_PAGE_LIMIT,
35695
36190
  conditional_inverse: true,
35696
36191
  ambiguous_outcome_reconciliation: true
35697
36192
  };
@@ -35752,7 +36247,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35752
36247
  if (!accepted2) {
35753
36248
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
35754
36249
  }
35755
- if (accepted2.normalized_call_digest !== callDigest) {
36250
+ if (!acceptedCallMatches(request, accepted2, callDigest)) {
35756
36251
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
35757
36252
  }
35758
36253
  return this.duplicateFor(transaction, request, callDigest, accepted2);
@@ -35766,7 +36261,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35766
36261
  });
35767
36262
  if (!accepted)
35768
36263
  return null;
35769
- if (accepted.normalized_call_digest === callDigest) {
36264
+ if (acceptedCallMatches(request, accepted, callDigest)) {
35770
36265
  return this.duplicateFor(transaction, request, callDigest, accepted);
35771
36266
  }
35772
36267
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
@@ -35777,6 +36272,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35777
36272
  const slug2 = taskListSlug(request.project_slug);
35778
36273
  const conflict2 = await transaction.findProjectConflict(path, slug2);
35779
36274
  if (conflict2) {
36275
+ if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === slug2) {
36276
+ return {
36277
+ record: boundExistingProjectRecord(conflict2),
36278
+ created_by_operation: false
36279
+ };
36280
+ }
35780
36281
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
35781
36282
  }
35782
36283
  await this.fault("before_object_write", request);
@@ -35788,7 +36289,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35788
36289
  task_prefix: deterministicTaskPrefix(request.project_slug)
35789
36290
  });
35790
36291
  await this.fault("after_object_write", request);
35791
- return projectRecord(project);
36292
+ return {
36293
+ record: projectRecord(project),
36294
+ created_by_operation: true
36295
+ };
35792
36296
  }
35793
36297
  const todosProjectId = String(request.desired["todos_project_id"]);
35794
36298
  const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
@@ -35802,6 +36306,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35802
36306
  const slug = taskListSlug(request.project_slug);
35803
36307
  const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
35804
36308
  if (conflict) {
36309
+ if (request.bind_existing === true && conflict.project_id === todosProjectId && conflict.slug === slug) {
36310
+ return {
36311
+ record: boundExistingTaskListRecord(conflict),
36312
+ created_by_operation: false
36313
+ };
36314
+ }
35805
36315
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
35806
36316
  }
35807
36317
  await this.fault("before_object_write", request);
@@ -35818,7 +36328,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35818
36328
  if (taskList.project_id !== todosProjectId) {
35819
36329
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
35820
36330
  }
35821
- return taskListRecord(taskList);
36331
+ return {
36332
+ record: taskListRecord(taskList),
36333
+ created_by_operation: true
36334
+ };
35822
36335
  }
35823
36336
  async create(request) {
35824
36337
  const startedAt = Date.now();
@@ -35840,9 +36353,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35840
36353
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp3, this.capabilityValue));
35841
36354
  if (!claimed) {
35842
36355
  const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
35843
- if (binding?.state === "accepted" && binding.normalized_call_digest === callDigest && binding.accepted_receipt_id) {
36356
+ if (binding?.state === "accepted" && binding.accepted_receipt_id) {
35844
36357
  const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
35845
- if (accepted2) {
36358
+ if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
35846
36359
  return this.duplicateFor(transaction, request, callDigest, accepted2);
35847
36360
  }
35848
36361
  }
@@ -35853,15 +36366,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35853
36366
  await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
35854
36367
  return recordOrTerminal;
35855
36368
  }
35856
- const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
36369
+ const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal.record, this.now(), recordOrTerminal.created_by_operation);
35857
36370
  await this.fault("before_receipt_write", request);
35858
36371
  const stored = await insertDeterministicReceipt(transaction, accepted);
35859
36372
  await this.fault("after_receipt_write", request);
35860
36373
  await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
35861
- target_id: recordOrTerminal.target_id,
36374
+ target_id: recordOrTerminal.record.target_id,
35862
36375
  accepted_receipt_id: stored.receipt_id,
35863
- result_revision: recordOrTerminal.revision,
35864
- result_digest: recordOrTerminal.digest,
36376
+ result_revision: recordOrTerminal.record.revision,
36377
+ result_digest: recordOrTerminal.record.digest,
35865
36378
  updated_at: this.now()
35866
36379
  });
35867
36380
  return stored;
@@ -35909,7 +36422,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35909
36422
  direction: request.direction
35910
36423
  });
35911
36424
  if (accepted) {
35912
- return accepted.normalized_call_digest === callDigest ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
36425
+ return acceptedCallMatches(request, accepted, callDigest) ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
35913
36426
  }
35914
36427
  const timestamp3 = this.now();
35915
36428
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp3, this.capabilityValue));
@@ -35989,6 +36502,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
35989
36502
  }
35990
36503
  return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
35991
36504
  }
36505
+ async listProjectResources(request) {
36506
+ const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
36507
+ if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
36508
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
36509
+ }
36510
+ if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
36511
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
36512
+ }
36513
+ const includeAnchors = request.include_anchors === true;
36514
+ const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
36515
+ if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
36516
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted Todos project binding exists for this exact Projects workspace id", { source_project_id: sourceProjectId });
36517
+ }
36518
+ const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
36519
+ if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
36520
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
36521
+ source_project_id: sourceProjectId,
36522
+ todos_project_id: projectBinding.target_id
36523
+ });
36524
+ }
36525
+ const cursor = decodeProjectResourceCursor(request.cursor, {
36526
+ source_project_id: sourceProjectId,
36527
+ include_anchors: includeAnchors
36528
+ });
36529
+ const collectionInput = {
36530
+ todos_project_id: projectBinding.target_id,
36531
+ task_list_id: taskListBinding.target_id,
36532
+ include_anchors: includeAnchors
36533
+ };
36534
+ const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
36535
+ if (cursor && cursor.collection_revision !== collectionRevision) {
36536
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
36537
+ source_project_id: sourceProjectId,
36538
+ expected_collection_revision: cursor.collection_revision,
36539
+ current_collection_revision: collectionRevision
36540
+ });
36541
+ }
36542
+ const candidates = await this.backend.listProjectResourceCandidates({
36543
+ ...collectionInput,
36544
+ after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
36545
+ limit: request.limit + 1
36546
+ });
36547
+ const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
36548
+ if (verifiedCollectionRevision !== collectionRevision) {
36549
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
36550
+ source_project_id: sourceProjectId,
36551
+ expected_collection_revision: collectionRevision,
36552
+ current_collection_revision: verifiedCollectionRevision
36553
+ });
36554
+ }
36555
+ const hasMore = candidates.length > request.limit;
36556
+ const pageCandidates = candidates.slice(0, request.limit);
36557
+ const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
36558
+ const last = pageCandidates.at(-1);
36559
+ return {
36560
+ authority: "todos",
36561
+ route: this.capabilityValue.route,
36562
+ package_version: this.capabilityValue.package_version,
36563
+ authority_id: this.capabilityValue.authority_id,
36564
+ tenant_id: this.capabilityValue.tenant_id,
36565
+ corpus_id: this.capabilityValue.corpus_id,
36566
+ source_project_id: sourceProjectId,
36567
+ todos_project_id: projectBinding.target_id,
36568
+ task_list_id: taskListBinding.target_id,
36569
+ include_anchors: includeAnchors,
36570
+ collection_revision: collectionRevision,
36571
+ limit: request.limit,
36572
+ count: resources.length,
36573
+ resources,
36574
+ has_more: hasMore,
36575
+ next_cursor: hasMore && last ? encodeProjectResourceCursor({
36576
+ source_project_id: sourceProjectId,
36577
+ include_anchors: includeAnchors,
36578
+ collection_revision: collectionRevision,
36579
+ kind_rank: last.kind_rank,
36580
+ target_id: last.target_id
36581
+ }) : null,
36582
+ complete: !hasMore,
36583
+ truncated: false
36584
+ };
36585
+ }
36586
+ async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
36587
+ const startedAt = Date.now();
36588
+ if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
36589
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
36590
+ }
36591
+ requireString(sourceRequest.package_version, "package_version", {
36592
+ max: 128,
36593
+ pattern: PACKAGE_VERSION_PATTERN
36594
+ });
36595
+ assertForwardRequest(sourceRequest, {
36596
+ ...this.capabilityValue,
36597
+ package_version: sourceRequest.package_version
36598
+ });
36599
+ const validation = await this.backend.transaction(async (transaction) => {
36600
+ const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
36601
+ if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
36602
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
36603
+ }
36604
+ const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
36605
+ if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
36606
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
36607
+ }
36608
+ const receiptLineageMatches = (receipt) => receipt.authority === "todos" && receipt.route === sourceRequest.authority_route && receipt.package_version === sourceRequest.package_version && receipt.authority_id === sourceRequest.authority_id && receipt.tenant_id === sourceRequest.tenant_id && receipt.corpus_id === sourceRequest.corpus_id && receipt.operation_id === sourceRequest.operation_id && receipt.step_id === sourceRequest.step_id && receipt.resource_kind === sourceRequest.resource_kind && receipt.direction === "forward" && receipt.target_selector === sourceRequest.target_selector && receipt.idempotency_key === sourceRequest.idempotency_key && receipt.request_digest === sourceRequest.request_digest && receipt.precondition_digest === sourceRequest.precondition_digest && acceptedCallMatches(sourceRequest, receipt);
36609
+ if (!receiptLineageMatches(storedSource) || !receiptLineageMatches(accepted) || storedSource.outcome === "duplicate_of_accepted" && (storedSource.duplicate_of_receipt_id !== accepted.receipt_id || storedSource.target_id !== accepted.target_id || storedSource.result_revision !== accepted.result_revision || storedSource.result_digest !== accepted.result_digest)) {
36610
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
36611
+ }
36612
+ const binding = await transaction.getBinding(authorityScope(this.capabilityValue), sourceRequest.resource_kind, sourceRequest.target_selector);
36613
+ if (!binding || binding.state !== "accepted" || binding.operation_id !== sourceRequest.operation_id || binding.step_id !== sourceRequest.step_id || binding.direction !== "forward" || binding.idempotency_key !== sourceRequest.idempotency_key || binding.request_digest !== sourceRequest.request_digest || binding.precondition_digest !== sourceRequest.precondition_digest || binding.normalized_call_digest !== accepted.normalized_call_digest || binding.target_id !== accepted.target_id || binding.accepted_receipt_id !== accepted.receipt_id || binding.result_revision !== accepted.result_revision || binding.result_digest !== accepted.result_digest) {
36614
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
36615
+ }
36616
+ const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
36617
+ if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
36618
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
36619
+ }
36620
+ let stableMatch = false;
36621
+ if (sourceRequest.resource_kind === "task_list") {
36622
+ stableMatch = taskListRegistrationDigest({
36623
+ ...current,
36624
+ updated_at: accepted.result_revision
36625
+ }) === accepted.result_digest;
36626
+ } else {
36627
+ const project = current;
36628
+ if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
36629
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
36630
+ }
36631
+ for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
36632
+ if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
36633
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
36634
+ }
36635
+ if (projectRegistrationDigest({
36636
+ ...project,
36637
+ task_counter: priorTaskCounter,
36638
+ updated_at: accepted.result_revision
36639
+ }) === accepted.result_digest) {
36640
+ stableMatch = true;
36641
+ break;
36642
+ }
36643
+ }
36644
+ }
36645
+ if (!stableMatch) {
36646
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
36647
+ }
36648
+ return {
36649
+ valid: true,
36650
+ resource_kind: sourceRequest.resource_kind,
36651
+ target_id: accepted.target_id,
36652
+ source_receipt_id: storedSource.receipt_id,
36653
+ accepted_receipt_id: accepted.receipt_id,
36654
+ source_outcome: storedSource.outcome,
36655
+ created_at: current.created_at,
36656
+ current_revision: current.updated_at,
36657
+ accepted_result_digest: accepted.result_digest
36658
+ };
36659
+ });
36660
+ assertWithinBounds(validation, sourceRequest, startedAt);
36661
+ return validation;
36662
+ }
35992
36663
  async storedAcceptedReceipt(request, supplied) {
35993
36664
  const stored = await this.backend.getReceiptById(supplied.receipt_id);
35994
36665
  if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
@@ -36160,6 +36831,65 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
36160
36831
  cursorTableName
36161
36832
  }), authorityOptions);
36162
36833
  }
36834
+ // src/project-registration/adoption-validation.ts
36835
+ var VALIDATION_KEYS = [
36836
+ "valid",
36837
+ "resource_kind",
36838
+ "target_id",
36839
+ "source_receipt_id",
36840
+ "accepted_receipt_id",
36841
+ "source_outcome",
36842
+ "created_at",
36843
+ "current_revision",
36844
+ "accepted_result_digest"
36845
+ ];
36846
+ function isRecord2(value) {
36847
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
36848
+ }
36849
+ function isNonEmptyString(value) {
36850
+ return typeof value === "string" && value.length > 0;
36851
+ }
36852
+ function hasExactKeys(value, expected) {
36853
+ const actual = Object.keys(value).sort();
36854
+ const wanted = [...expected].sort();
36855
+ return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
36856
+ }
36857
+ function adoptionRejected(message) {
36858
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", `TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED: ${message}`);
36859
+ }
36860
+ function assertTodosPriorRegistrationAdoptionValidationEnvelope(value, input) {
36861
+ if (!isRecord2(input) || !hasExactKeys(input, [
36862
+ "source_request",
36863
+ "source_receipt",
36864
+ "current_record"
36865
+ ])) {
36866
+ adoptionRejected("prior-adoption validation input is incomplete");
36867
+ }
36868
+ const request = input["source_request"];
36869
+ const receipt = input["source_receipt"];
36870
+ const current = input["current_record"];
36871
+ if (!isRecord2(request) || !isRecord2(receipt) || !isRecord2(current)) {
36872
+ adoptionRejected("prior-adoption validation input records are incomplete");
36873
+ }
36874
+ const resourceKind = request["resource_kind"];
36875
+ const sourceOutcome = receipt["outcome"];
36876
+ const acceptedReceiptId = sourceOutcome === "accepted" ? receipt["receipt_id"] : sourceOutcome === "duplicate_of_accepted" ? receipt["duplicate_of_receipt_id"] : null;
36877
+ if (resourceKind !== "project" && resourceKind !== "task_list" || request["direction"] !== "forward" || sourceOutcome !== "accepted" && sourceOutcome !== "duplicate_of_accepted" || !isNonEmptyString(acceptedReceiptId) || !isNonEmptyString(receipt["receipt_id"]) || !isNonEmptyString(receipt["target_id"]) || !isNonEmptyString(receipt["result_revision"]) || !isNonEmptyString(receipt["result_digest"]) || !isNonEmptyString(current["id"]) || !isNonEmptyString(current["created_at"]) || !isNonEmptyString(current["updated_at"]) || receipt["authority"] !== "todos" || receipt["route"] !== request["authority_route"] || receipt["package_version"] !== request["package_version"] || receipt["authority_id"] !== request["authority_id"] || receipt["tenant_id"] !== request["tenant_id"] || receipt["corpus_id"] !== request["corpus_id"] || receipt["operation_id"] !== request["operation_id"] || receipt["step_id"] !== request["step_id"] || receipt["resource_kind"] !== resourceKind || receipt["direction"] !== "forward" || receipt["idempotency_key"] !== request["idempotency_key"] || receipt["request_digest"] !== request["request_digest"] || receipt["precondition_digest"] !== request["precondition_digest"] || receipt["accepted_receipt_id"] !== null || receipt["target_id"] !== current["id"] || receipt["result_revision"] !== current["created_at"]) {
36878
+ adoptionRejected("prior-adoption validation input does not carry one complete accepted receipt and current target incarnation");
36879
+ }
36880
+ if (sourceOutcome === "accepted" && receipt["duplicate_of_receipt_id"] !== null || sourceOutcome === "duplicate_of_accepted" && receipt["duplicate_of_receipt_id"] !== acceptedReceiptId) {
36881
+ adoptionRejected("prior-adoption validation source receipt lineage is incomplete");
36882
+ }
36883
+ if (!isRecord2(value) || !hasExactKeys(value, ["validation"]) || !isRecord2(value["validation"]) || !hasExactKeys(value["validation"], VALIDATION_KEYS)) {
36884
+ adoptionRejected("prior-adoption validation response envelope is incomplete");
36885
+ }
36886
+ const validation = value["validation"];
36887
+ if (validation["valid"] !== true || validation["resource_kind"] !== resourceKind || validation["target_id"] !== current["id"] || validation["source_receipt_id"] !== receipt["receipt_id"] || validation["accepted_receipt_id"] !== acceptedReceiptId || validation["source_outcome"] !== sourceOutcome || validation["created_at"] !== current["created_at"] || validation["current_revision"] !== current["updated_at"] || validation["accepted_result_digest"] !== receipt["result_digest"]) {
36888
+ adoptionRejected("prior-adoption validation response does not prove the exact accepted receipt and current target");
36889
+ }
36890
+ return validation;
36891
+ }
36892
+
36163
36893
  // src/project-registration/http.ts
36164
36894
  var JSON_HEADERS = { "Content-Type": "application/json" };
36165
36895
  function json(body, status = 200) {
@@ -36206,6 +36936,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
36206
36936
  if ((action === "" || action === "capability") && method === "GET") {
36207
36937
  return json({ capability: await authority.capability() });
36208
36938
  }
36939
+ if (action === "resources" && method === "GET") {
36940
+ const sourceProjectId = url.searchParams.get("source_project_id");
36941
+ const limit = Number(url.searchParams.get("limit") ?? "100");
36942
+ const includeAnchorsRaw = url.searchParams.get("include_anchors");
36943
+ const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
36944
+ return json({
36945
+ page: await authority.listProjectResources({
36946
+ source_project_id: sourceProjectId,
36947
+ limit,
36948
+ include_anchors: includeAnchors,
36949
+ cursor: url.searchParams.get("cursor") ?? undefined
36950
+ })
36951
+ });
36952
+ }
36209
36953
  if (method !== "POST")
36210
36954
  return json({ error: "method not allowed" }, 405);
36211
36955
  const body = await readJson(req);
@@ -36228,6 +36972,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
36228
36972
  record: await authority.readExact(body)
36229
36973
  });
36230
36974
  }
36975
+ if (action === "validate-prior-adoption") {
36976
+ const input = body;
36977
+ return json({
36978
+ validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
36979
+ });
36980
+ }
36231
36981
  if (action === "compensate") {
36232
36982
  return json({
36233
36983
  receipt: await authority.compensate(body)
@@ -36302,6 +37052,28 @@ class TodosProjectRegistrationHttpClient {
36302
37052
  async lookupReceipt(request) {
36303
37053
  return this.request("/receipts/lookup", { method: "POST", body: JSON.stringify(request) });
36304
37054
  }
37055
+ async listProjectResources(request) {
37056
+ const query = new URLSearchParams({
37057
+ source_project_id: request.source_project_id,
37058
+ limit: String(request.limit),
37059
+ include_anchors: String(request.include_anchors === true),
37060
+ ...request.cursor ? { cursor: request.cursor } : {}
37061
+ });
37062
+ const body = await this.request(`/resources?${query.toString()}`);
37063
+ return body.page;
37064
+ }
37065
+ async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
37066
+ const input = {
37067
+ source_request: withoutTarget(sourceRequest),
37068
+ source_receipt: sourceReceipt,
37069
+ current_record: currentRecord
37070
+ };
37071
+ const body = await this.request("/validate-prior-adoption", {
37072
+ method: "POST",
37073
+ body: JSON.stringify(input)
37074
+ });
37075
+ return assertTodosPriorRegistrationAdoptionValidationEnvelope(body, input);
37076
+ }
36305
37077
  async compensate(request) {
36306
37078
  const body = await this.request("/compensate", { method: "POST", body: JSON.stringify(withoutTarget(request)) });
36307
37079
  return body.receipt;
@@ -40295,7 +41067,9 @@ init_types();
40295
41067
 
40296
41068
  // src/task-manifest/types.ts
40297
41069
  var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1";
41070
+ var TODOS_TASK_MANIFEST_CALLER_ROUTE = "accounts.task-manifest.v1";
40298
41071
  var TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1;
41072
+ var TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE = "deterministic-v1";
40299
41073
  function supportsIdempotentOutboxDelivery(capability2) {
40300
41074
  return capability2 !== null && typeof capability2 === "object" && capability2["idempotent_outbox_delivery"] === true;
40301
41075
  }
@@ -40325,6 +41099,8 @@ var TODOS_TASK_MANIFEST_BOUNDS = {
40325
41099
  };
40326
41100
  var key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
40327
41101
  var identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
41102
+ var digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
41103
+ var idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
40328
41104
  var uuid2 = exports_external.string().uuid();
40329
41105
  var scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
40330
41106
  var boundedScalarRecord = (limit, field2) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
@@ -40367,7 +41143,9 @@ var effect = exports_external.object({
40367
41143
  var schema = exports_external.object({
40368
41144
  version: exports_external.literal(1),
40369
41145
  operation_id: identifier,
40370
- idempotency_key: identifier,
41146
+ step_id: identifier,
41147
+ idempotency_key: idempotencyKey,
41148
+ precondition_digest: digest,
40371
41149
  project_id: uuid2,
40372
41150
  task_list_id: uuid2.optional(),
40373
41151
  if_binding_version: exports_external.number().int().min(0).optional(),
@@ -40383,7 +41161,10 @@ var schema = exports_external.object({
40383
41161
  }).strict();
40384
41162
  var compensationSchema = exports_external.object({
40385
41163
  receipt_id: uuid2,
40386
- idempotency_key: identifier,
41164
+ operation_id: identifier,
41165
+ step_id: identifier,
41166
+ idempotency_key: idempotencyKey,
41167
+ precondition_digest: digest,
40387
41168
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
40388
41169
  }).strict();
40389
41170
  var bindingLookupSchema = exports_external.object({
@@ -40468,10 +41249,31 @@ function parseTodosTaskManifestBindingLookup(input) {
40468
41249
  }
40469
41250
 
40470
41251
  // src/task-manifest/plan-slug.ts
41252
+ var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
40471
41253
  function taskManifestPlanSlug(manifest, planId) {
40472
41254
  const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
40473
41255
  return `${base}-${planId}`;
40474
41256
  }
41257
+ function sqliteLegacyTaskManifestPlanSlug(rows, planId, targetBase) {
41258
+ const target = rows.find((row) => row.id === planId);
41259
+ if (!target)
41260
+ return null;
41261
+ const used = new Set;
41262
+ const ordered = [...rows].filter((row) => row.project_id === target.project_id).sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id));
41263
+ for (const row of ordered) {
41264
+ const base = row.id === planId ? normalizeSlug(targetBase ?? row.name) || "plan" : normalizeSlug(row.slug || row.name) || "plan";
41265
+ let candidate = base;
41266
+ let suffix = 2;
41267
+ while (used.has(candidate)) {
41268
+ candidate = `${base}-${suffix}`;
41269
+ suffix += 1;
41270
+ }
41271
+ if (row.id === planId)
41272
+ return candidate;
41273
+ used.add(candidate);
41274
+ }
41275
+ return null;
41276
+ }
40475
41277
 
40476
41278
  // src/task-manifest/backend.ts
40477
41279
  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;
@@ -40485,11 +41287,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
40485
41287
  const row = rows[0];
40486
41288
  const bindingVersion = Number(row.binding_version);
40487
41289
  const state = row.state;
40488
- 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") {
41290
+ 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 || row.binding_step_id !== row.receipt_step_id || typeof row.binding_operation_id !== "string" || typeof row.binding_step_id !== "string" || typeof row.apply_receipt_id !== "string" || !UUID_PATTERN2.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
40489
41291
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
40490
41292
  }
40491
41293
  return {
40492
41294
  plan_id: planId,
41295
+ operation_id: row.binding_operation_id,
41296
+ step_id: row.binding_step_id,
40493
41297
  apply_receipt_id: row.apply_receipt_id,
40494
41298
  binding_version: bindingVersion,
40495
41299
  state
@@ -40597,9 +41401,15 @@ function sqliteTodosTaskManifestSchemaSql() {
40597
41401
  schema_version INTEGER NOT NULL CHECK(schema_version = 1),
40598
41402
  kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
40599
41403
  operation_id TEXT NOT NULL,
41404
+ step_id TEXT NOT NULL,
40600
41405
  idempotency_key TEXT NOT NULL,
40601
41406
  request_digest TEXT NOT NULL,
41407
+ precondition_digest TEXT NOT NULL,
40602
41408
  result_digest TEXT NOT NULL,
41409
+ slug_provenance TEXT,
41410
+ outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
41411
+ reason TEXT,
41412
+ duplicate_of_receipt_id TEXT,
40603
41413
  binding_version INTEGER NOT NULL,
40604
41414
  apply_receipt_id TEXT,
40605
41415
  manifest_json TEXT,
@@ -40610,9 +41420,13 @@ function sqliteTodosTaskManifestSchemaSql() {
40610
41420
  CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
40611
41421
  operation_id TEXT PRIMARY KEY,
40612
41422
  tenant_id TEXT NOT NULL,
41423
+ step_id TEXT NOT NULL,
40613
41424
  idempotency_key TEXT NOT NULL UNIQUE,
40614
41425
  request_digest TEXT NOT NULL,
41426
+ precondition_digest TEXT NOT NULL,
40615
41427
  result_digest TEXT NOT NULL,
41428
+ slug_provenance TEXT,
41429
+ outcome TEXT NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
40616
41430
  apply_receipt_id TEXT NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
40617
41431
  manifest_json TEXT NOT NULL,
40618
41432
  result_json TEXT NOT NULL,
@@ -40633,6 +41447,27 @@ function sqliteTodosTaskManifestSchemaSql() {
40633
41447
  created_at TEXT NOT NULL,
40634
41448
  delivered_at TEXT
40635
41449
  );
41450
+ CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
41451
+ receipt_id TEXT PRIMARY KEY,
41452
+ tenant_id TEXT NOT NULL,
41453
+ authority TEXT NOT NULL CHECK(authority = 'todos'),
41454
+ route TEXT NOT NULL,
41455
+ schema_version INTEGER NOT NULL CHECK(schema_version = 1),
41456
+ kind TEXT NOT NULL CHECK(kind IN ('apply', 'compensate')),
41457
+ operation_id TEXT NOT NULL,
41458
+ step_id TEXT NOT NULL,
41459
+ idempotency_key TEXT NOT NULL,
41460
+ request_digest TEXT NOT NULL,
41461
+ precondition_digest TEXT NOT NULL,
41462
+ result_digest TEXT NOT NULL,
41463
+ outcome TEXT NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
41464
+ reason TEXT NOT NULL,
41465
+ binding_version INTEGER NOT NULL,
41466
+ apply_receipt_id TEXT,
41467
+ manifest_json TEXT,
41468
+ result_json TEXT NOT NULL,
41469
+ created_at TEXT NOT NULL
41470
+ );
40636
41471
  CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_outbox_receipt
40637
41472
  ON todos_task_manifest_outbox(apply_receipt_id, status);
40638
41473
  CREATE TRIGGER IF NOT EXISTS todos_task_manifest_receipts_immutable_update
@@ -40643,6 +41478,14 @@ function sqliteTodosTaskManifestSchemaSql() {
40643
41478
  BEFORE DELETE ON todos_task_manifest_receipts BEGIN
40644
41479
  SELECT RAISE(ABORT, 'todos task manifest receipts are immutable');
40645
41480
  END;
41481
+ CREATE TRIGGER IF NOT EXISTS todos_task_manifest_terminal_receipts_immutable_update
41482
+ BEFORE UPDATE ON todos_task_manifest_terminal_receipts BEGIN
41483
+ SELECT RAISE(ABORT, 'todos task manifest terminal receipts are immutable');
41484
+ END;
41485
+ CREATE TRIGGER IF NOT EXISTS todos_task_manifest_terminal_receipts_immutable_delete
41486
+ BEFORE DELETE ON todos_task_manifest_terminal_receipts BEGIN
41487
+ SELECT RAISE(ABORT, 'todos task manifest terminal receipts are immutable');
41488
+ END;
40646
41489
  `;
40647
41490
  }
40648
41491
  function sqliteTableHasColumn(db, tableName, columnName) {
@@ -40659,6 +41502,24 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
40659
41502
  if (!sqliteTableHasColumn(db, tableName, "tenant_id")) {
40660
41503
  db.exec(`ALTER TABLE "${tableName}" ADD COLUMN tenant_id TEXT NOT NULL DEFAULT ${tenantDefault}`);
40661
41504
  }
41505
+ if (!sqliteTableHasColumn(db, tableName, "slug_provenance")) {
41506
+ db.exec(`ALTER TABLE "${tableName}" ADD COLUMN slug_provenance TEXT`);
41507
+ }
41508
+ }
41509
+ const defaults = [
41510
+ ["todos_task_manifest_receipts", "step_id", "TEXT NOT NULL DEFAULT 'legacy-apply'"],
41511
+ ["todos_task_manifest_receipts", "precondition_digest", `TEXT NOT NULL DEFAULT '${"0".repeat(64)}'`],
41512
+ ["todos_task_manifest_receipts", "outcome", "TEXT NOT NULL DEFAULT 'accepted'"],
41513
+ ["todos_task_manifest_receipts", "reason", "TEXT"],
41514
+ ["todos_task_manifest_receipts", "duplicate_of_receipt_id", "TEXT"],
41515
+ ["todos_task_manifest_bindings", "step_id", "TEXT NOT NULL DEFAULT 'legacy-apply'"],
41516
+ ["todos_task_manifest_bindings", "precondition_digest", `TEXT NOT NULL DEFAULT '${"0".repeat(64)}'`],
41517
+ ["todos_task_manifest_bindings", "outcome", "TEXT NOT NULL DEFAULT 'accepted'"]
41518
+ ];
41519
+ for (const [tableName, columnName, definition] of defaults) {
41520
+ if (!sqliteTableHasColumn(db, tableName, columnName)) {
41521
+ db.exec(`ALTER TABLE "${tableName}" ADD COLUMN "${columnName}" ${definition}`);
41522
+ }
40662
41523
  }
40663
41524
  db.exec(`
40664
41525
  CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_receipts_tenant
@@ -40668,6 +41529,12 @@ function ensureSqliteTodosTaskManifestSchema(db, tenantId) {
40668
41529
  tenant_id,
40669
41530
  json_extract(result_json, '$.graph.plan_id')
40670
41531
  );
41532
+ DROP INDEX IF EXISTS idx_todos_task_manifest_terminal_receipts_lookup;
41533
+ DROP INDEX IF EXISTS idx_todos_task_manifest_terminal_receipts_identity;
41534
+ CREATE INDEX IF NOT EXISTS idx_todos_task_manifest_terminal_receipts_lookup
41535
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id);
41536
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_task_manifest_terminal_receipts_identity
41537
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id);
40671
41538
  `);
40672
41539
  }
40673
41540
  function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
@@ -40681,9 +41548,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
40681
41548
  schema_version integer NOT NULL CHECK(schema_version = 1),
40682
41549
  kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
40683
41550
  operation_id text NOT NULL,
41551
+ step_id text NOT NULL,
40684
41552
  idempotency_key text NOT NULL,
40685
41553
  request_digest text NOT NULL,
41554
+ precondition_digest text NOT NULL,
40686
41555
  result_digest text NOT NULL,
41556
+ slug_provenance text,
41557
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
41558
+ reason text,
41559
+ duplicate_of_receipt_id text,
40687
41560
  binding_version integer NOT NULL,
40688
41561
  apply_receipt_id text,
40689
41562
  manifest_json jsonb,
@@ -40695,12 +41568,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
40695
41568
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
40696
41569
  `ALTER TABLE todos_task_manifest_receipts
40697
41570
  ALTER COLUMN tenant_id DROP DEFAULT`,
41571
+ `ALTER TABLE todos_task_manifest_receipts
41572
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
41573
+ `ALTER TABLE todos_task_manifest_receipts
41574
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
41575
+ `ALTER TABLE todos_task_manifest_receipts
41576
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
41577
+ `ALTER TABLE todos_task_manifest_receipts
41578
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
41579
+ `ALTER TABLE todos_task_manifest_receipts
41580
+ ADD COLUMN IF NOT EXISTS reason text`,
41581
+ `ALTER TABLE todos_task_manifest_receipts
41582
+ ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
40698
41583
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
40699
41584
  operation_id text PRIMARY KEY,
40700
41585
  tenant_id text NOT NULL,
41586
+ step_id text NOT NULL,
40701
41587
  idempotency_key text NOT NULL UNIQUE,
40702
41588
  request_digest text NOT NULL,
41589
+ precondition_digest text NOT NULL,
40703
41590
  result_digest text NOT NULL,
41591
+ slug_provenance text,
41592
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
40704
41593
  apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
40705
41594
  manifest_json jsonb NOT NULL,
40706
41595
  result_json jsonb NOT NULL,
@@ -40714,6 +41603,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
40714
41603
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
40715
41604
  `ALTER TABLE todos_task_manifest_bindings
40716
41605
  ALTER COLUMN tenant_id DROP DEFAULT`,
41606
+ `ALTER TABLE todos_task_manifest_bindings
41607
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
41608
+ `ALTER TABLE todos_task_manifest_bindings
41609
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
41610
+ `ALTER TABLE todos_task_manifest_bindings
41611
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
41612
+ `ALTER TABLE todos_task_manifest_bindings
41613
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
40717
41614
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
40718
41615
  id text PRIMARY KEY,
40719
41616
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -40725,10 +41622,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
40725
41622
  created_at timestamptz NOT NULL,
40726
41623
  delivered_at timestamptz
40727
41624
  )`,
41625
+ `CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
41626
+ receipt_id text PRIMARY KEY,
41627
+ tenant_id text NOT NULL,
41628
+ authority text NOT NULL CHECK(authority = 'todos'),
41629
+ route text NOT NULL,
41630
+ schema_version integer NOT NULL CHECK(schema_version = 1),
41631
+ kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
41632
+ operation_id text NOT NULL,
41633
+ step_id text NOT NULL,
41634
+ idempotency_key text NOT NULL,
41635
+ request_digest text NOT NULL,
41636
+ precondition_digest text NOT NULL,
41637
+ result_digest text NOT NULL,
41638
+ outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
41639
+ reason text NOT NULL,
41640
+ binding_version integer NOT NULL,
41641
+ apply_receipt_id text,
41642
+ manifest_json jsonb,
41643
+ result_json jsonb NOT NULL,
41644
+ created_at timestamptz NOT NULL
41645
+ )`,
40728
41646
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
40729
41647
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
40730
41648
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
40731
41649
  ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
41650
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
41651
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
41652
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
41653
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
41654
+ `CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
41655
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
40732
41656
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
40733
41657
  ON todos_task_manifest_bindings(
40734
41658
  tenant_id,
@@ -40741,6 +41665,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
40741
41665
  `DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
40742
41666
  `CREATE TRIGGER todos_task_manifest_receipts_immutable
40743
41667
  BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
41668
+ FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
41669
+ `DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
41670
+ `CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
41671
+ BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
40744
41672
  FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
40745
41673
  ];
40746
41674
  }
@@ -40752,7 +41680,76 @@ function fault(faults, point) {
40752
41680
  throw new Error(`Injected task-manifest fault at ${point}`);
40753
41681
  }
40754
41682
  function parseApplyResult(value, duplicate) {
40755
- return { ...JSON.parse(value), duplicate };
41683
+ const parsed = JSON.parse(value);
41684
+ return {
41685
+ ...parsed,
41686
+ duplicate,
41687
+ receipt: {
41688
+ ...parsed.receipt,
41689
+ step_id: parsed.receipt.step_id ?? "legacy-apply",
41690
+ precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
41691
+ outcome: parsed.receipt.outcome ?? "accepted",
41692
+ reason: parsed.receipt.reason ?? null,
41693
+ duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
41694
+ }
41695
+ };
41696
+ }
41697
+ function terminalApplyResult(input, reason) {
41698
+ const receipt = {
41699
+ receipt_id: input.terminal_receipt_id,
41700
+ authority: "todos",
41701
+ route: "todos.task-manifest.v1",
41702
+ schema_version: 1,
41703
+ kind: "apply",
41704
+ operation_id: input.manifest.operation_id,
41705
+ step_id: input.manifest.step_id,
41706
+ idempotency_key: input.manifest.idempotency_key,
41707
+ request_digest: input.request_digest,
41708
+ precondition_digest: input.manifest.precondition_digest,
41709
+ result_digest: canonicalDigest({
41710
+ outcome: "terminal_nonacceptance",
41711
+ reason,
41712
+ operation_id: input.manifest.operation_id,
41713
+ step_id: input.manifest.step_id,
41714
+ request_digest: input.request_digest
41715
+ }),
41716
+ outcome: "terminal_nonacceptance",
41717
+ reason,
41718
+ duplicate_of_receipt_id: null,
41719
+ binding_version: 0,
41720
+ apply_receipt_id: null,
41721
+ created_at: input.now
41722
+ };
41723
+ return {
41724
+ duplicate: false,
41725
+ receipt,
41726
+ graph: input.graph,
41727
+ readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
41728
+ outbox_ids: [],
41729
+ result_digest: receipt.result_digest
41730
+ };
41731
+ }
41732
+ function validateCompensationPlanSlug(db, manifest, planId, slugProvenance) {
41733
+ const plan = db.query("SELECT id, project_id, name, slug, created_at FROM plans WHERE id = ? LIMIT 1").get(planId);
41734
+ if (!plan) {
41735
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
41736
+ }
41737
+ if (slugProvenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
41738
+ if (plan.slug !== taskManifestPlanSlug(manifest, planId)) {
41739
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
41740
+ }
41741
+ return;
41742
+ }
41743
+ if (slugProvenance !== null && slugProvenance !== undefined) {
41744
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
41745
+ }
41746
+ if (plan.slug === null)
41747
+ return;
41748
+ const rows = db.query("SELECT id, project_id, name, slug, created_at FROM plans WHERE project_id IS ? ORDER BY created_at ASC, id ASC").all(plan.project_id);
41749
+ const expected = sqliteLegacyTaskManifestPlanSlug(rows, planId, manifest.plan.key || manifest.plan.name);
41750
+ if (expected === null || plan.slug !== expected) {
41751
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy plan slug was not produced by SQLite allocation");
41752
+ }
40756
41753
  }
40757
41754
 
40758
41755
  class SqliteTodosTaskManifestBackend {
@@ -40792,33 +41789,49 @@ class SqliteTodosTaskManifestBackend {
40792
41789
  async apply(input, faults) {
40793
41790
  return this.serialized(() => {
40794
41791
  const { manifest } = input;
41792
+ const terminal = this.db.query(`SELECT result_json
41793
+ FROM todos_task_manifest_terminal_receipts
41794
+ WHERE tenant_id = ?
41795
+ AND kind = 'apply'
41796
+ AND (receipt_id = ? OR (operation_id = ? AND step_id = ?))
41797
+ ORDER BY created_at ASC, receipt_id ASC
41798
+ LIMIT 1`).get(this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id);
41799
+ if (terminal)
41800
+ return parseApplyResult(terminal.result_json, true);
40795
41801
  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);
40796
41802
  if (binding) {
40797
- if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
40798
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
41803
+ if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest || binding["step_id"] !== manifest.step_id || binding["precondition_digest"] !== manifest.precondition_digest) {
41804
+ return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
41805
+ }
41806
+ if (binding["outcome"] === "terminal_nonacceptance") {
41807
+ const terminalResult = this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
41808
+ return { ...terminalResult, duplicate: true };
40799
41809
  }
40800
41810
  if (binding["state"] !== "applied") {
40801
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Operation was already compensated");
41811
+ return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
40802
41812
  }
40803
41813
  return parseApplyResult(String(binding["result_json"]), true);
40804
41814
  }
40805
41815
  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);
40806
41816
  if (idempotency)
40807
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used by another operation");
41817
+ return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
41818
+ if (manifest.idempotency_key !== input.expected_idempotency_key) {
41819
+ return this.persistTerminal(input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
41820
+ }
40808
41821
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
40809
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "New manifest binding version must be 0");
41822
+ return this.persistTerminal(input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
40810
41823
  }
40811
41824
  if (!this.db.query("SELECT 1 AS found FROM projects WHERE id = ? LIMIT 1").get(manifest.project_id)) {
40812
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Project does not exist");
41825
+ return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
40813
41826
  }
40814
41827
  if (manifest.task_list_id && !this.db.query("SELECT 1 AS found FROM task_lists WHERE id = ? AND project_id = ? LIMIT 1").get(manifest.task_list_id, manifest.project_id)) {
40815
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Task list does not belong to the project");
41828
+ return this.persistTerminal(input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
40816
41829
  }
40817
41830
  const allIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids];
40818
41831
  for (const id of allIds) {
40819
41832
  for (const table of ["plans", "tasks", "task_comments", "task_verifications"]) {
40820
41833
  if (this.db.query(`SELECT 1 AS found FROM ${table} WHERE id = ? LIMIT 1`).get(id)) {
40821
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Deterministic id already exists: ${id}`);
41834
+ return this.persistTerminal(input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
40822
41835
  }
40823
41836
  }
40824
41837
  }
@@ -40874,9 +41887,14 @@ class SqliteTodosTaskManifestBackend {
40874
41887
  schema_version: 1,
40875
41888
  kind: "apply",
40876
41889
  operation_id: manifest.operation_id,
41890
+ step_id: manifest.step_id,
40877
41891
  idempotency_key: manifest.idempotency_key,
40878
41892
  request_digest: input.request_digest,
41893
+ precondition_digest: manifest.precondition_digest,
40879
41894
  result_digest: input.result_digest,
41895
+ outcome: "accepted",
41896
+ reason: null,
41897
+ duplicate_of_receipt_id: null,
40880
41898
  binding_version: 1,
40881
41899
  apply_receipt_id: null,
40882
41900
  created_at: input.now
@@ -40892,9 +41910,10 @@ class SqliteTodosTaskManifestBackend {
40892
41910
  const resultJson = canonicalJson(result);
40893
41911
  const manifestJson = canonicalJson(manifest);
40894
41912
  this.db.query(`INSERT INTO todos_task_manifest_receipts (
40895
- receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
40896
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
40897
- ) 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);
41913
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
41914
+ request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
41915
+ duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
41916
+ ) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, ?, ?, ?, 'accepted', NULL, NULL, 1, NULL, ?, ?, ?)`).run(input.receipt_id, this.tenantId, manifest.operation_id, manifest.step_id, manifest.idempotency_key, input.request_digest, manifest.precondition_digest, input.result_digest, TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE, manifestJson, resultJson, input.now);
40898
41917
  for (const entry2 of input.outbox) {
40899
41918
  this.db.query(`INSERT INTO todos_task_manifest_outbox (
40900
41919
  id, apply_receipt_id, topic, payload, payload_digest, status, created_at
@@ -40902,18 +41921,37 @@ class SqliteTodosTaskManifestBackend {
40902
41921
  }
40903
41922
  fault(faults, "after_outbox_write");
40904
41923
  this.db.query(`INSERT INTO todos_task_manifest_bindings (
40905
- operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
40906
- manifest_json, result_json, state, version, created_at, updated_at
40907
- ) 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);
41924
+ operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest, result_digest,
41925
+ slug_provenance, outcome, apply_receipt_id, manifest_json, result_json, state, version, created_at, updated_at
41926
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?, ?, 'applied', 1, ?, ?)`).run(manifest.operation_id, this.tenantId, manifest.step_id, manifest.idempotency_key, input.request_digest, manifest.precondition_digest, input.result_digest, TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE, input.receipt_id, manifestJson, resultJson, input.now, input.now);
40908
41927
  fault(faults, "after_receipt_write");
40909
41928
  return result;
40910
41929
  });
40911
41930
  }
41931
+ persistTerminal(input, reason) {
41932
+ const result = terminalApplyResult(input, reason);
41933
+ const resultJson = canonicalJson(result);
41934
+ this.db.query(`INSERT OR IGNORE INTO todos_task_manifest_terminal_receipts (
41935
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
41936
+ idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
41937
+ binding_version, apply_receipt_id, manifest_json, result_json, created_at
41938
+ ) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'apply', ?, ?, ?, ?, ?, ?, 'terminal_nonacceptance', ?, 0, NULL, ?, ?, ?)`).run(result.receipt.receipt_id, this.tenantId, input.manifest.operation_id, input.manifest.step_id, input.manifest.idempotency_key, input.request_digest, input.manifest.precondition_digest, result.receipt.result_digest, reason, canonicalJson(input.manifest), resultJson, input.now);
41939
+ const stored = this.db.query(`SELECT receipt_id, result_json
41940
+ FROM todos_task_manifest_terminal_receipts
41941
+ WHERE tenant_id = ? AND kind = 'apply'
41942
+ AND (receipt_id = ? OR (operation_id = ? AND step_id = ?))
41943
+ ORDER BY created_at ASC, receipt_id ASC
41944
+ LIMIT 1`).get(this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id);
41945
+ return stored ? parseApplyResult(stored.result_json, stored.receipt_id !== result.receipt.receipt_id) : result;
41946
+ }
40912
41947
  async readExact(receiptId2) {
40913
41948
  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);
40914
- if (!row)
41949
+ if (row)
41950
+ return parseApplyResult(row.result_json, false);
41951
+ const terminal = this.db.query("SELECT result_json FROM todos_task_manifest_terminal_receipts WHERE tenant_id = ? AND receipt_id = ? AND kind = 'apply' LIMIT 1").get(this.tenantId, receiptId2);
41952
+ if (!terminal)
40915
41953
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
40916
- return parseApplyResult(row.result_json, false);
41954
+ return parseApplyResult(terminal.result_json, false);
40917
41955
  }
40918
41956
  async lookupBindingByPlanId(planId) {
40919
41957
  const rows = this.db.query(`
@@ -40923,6 +41961,7 @@ class SqliteTodosTaskManifestBackend {
40923
41961
  b.version AS binding_version,
40924
41962
  b.tenant_id AS binding_tenant_id,
40925
41963
  b.operation_id AS binding_operation_id,
41964
+ b.step_id AS binding_step_id,
40926
41965
  json_extract(b.result_json, '$.graph.plan_id') AS binding_plan_id,
40927
41966
  r.tenant_id AS receipt_tenant_id,
40928
41967
  r.authority AS receipt_authority,
@@ -40930,6 +41969,7 @@ class SqliteTodosTaskManifestBackend {
40930
41969
  r.schema_version AS receipt_schema_version,
40931
41970
  r.kind AS receipt_kind,
40932
41971
  r.operation_id AS receipt_operation_id,
41972
+ r.step_id AS receipt_step_id,
40933
41973
  json_extract(r.result_json, '$.graph.plan_id') AS receipt_plan_id
40934
41974
  FROM todos_task_manifest_bindings b
40935
41975
  LEFT JOIN todos_task_manifest_receipts r
@@ -40992,6 +42032,12 @@ class SqliteTodosTaskManifestBackend {
40992
42032
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
40993
42033
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
40994
42034
  }
42035
+ const storedStepId = String(row["step_id"] ?? "legacy-apply");
42036
+ const storedRequestDigest = String(row["request_digest"]);
42037
+ const storedPreconditionDigest = String(row["precondition_digest"] ?? "0".repeat(64));
42038
+ if (String(row["receipt_id"]) !== input.receipt_id || String(row["operation_id"]) !== input.operation_id || String(binding["operation_id"]) !== String(row["operation_id"]) || String(binding["step_id"] ?? "legacy-apply") !== storedStepId || String(binding["idempotency_key"]) !== String(row["idempotency_key"]) || String(binding["request_digest"]) !== storedRequestDigest || String(binding["precondition_digest"] ?? "0".repeat(64)) !== storedPreconditionDigest || String(binding["apply_receipt_id"]) !== input.receipt_id || binding["slug_provenance"] !== row["slug_provenance"]) {
42039
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
42040
+ }
40995
42041
  if (binding["state"] !== "applied")
40996
42042
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not in applied state");
40997
42043
  const delivered = this.db.query(`SELECT o.id FROM todos_task_manifest_outbox o
@@ -41002,10 +42048,16 @@ class SqliteTodosTaskManifestBackend {
41002
42048
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
41003
42049
  const applyResult = parseApplyResult(String(row["result_json"]), false);
41004
42050
  const manifest = JSON.parse(String(row["manifest_json"]));
42051
+ const manifestRecord = manifest;
42052
+ const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
41005
42053
  const expectedEffects = [
41006
42054
  {
41007
42055
  topic: "todos.task-manifest.applied",
41008
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
42056
+ payload: {
42057
+ operation_id: manifest.operation_id,
42058
+ ...applyStepId ? { step_id: applyStepId } : {},
42059
+ project_id: manifest.project_id
42060
+ }
41009
42061
  },
41010
42062
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
41011
42063
  ];
@@ -41045,8 +42097,10 @@ class SqliteTodosTaskManifestBackend {
41045
42097
  if (canonicalJson(actualReadback) !== canonicalJson(applyResult.readback)) {
41046
42098
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: graph changed since apply", { actualReadback });
41047
42099
  }
41048
- const plan = this.db.query("SELECT project_id, name, description, status, task_list_id, slug FROM plans WHERE id = ? LIMIT 1").get(applyResult.graph.plan_id);
41049
- if (!plan || plan["project_id"] !== manifest.project_id || plan["name"] !== manifest.plan.name || plan["description"] !== (manifest.plan.description ?? null) || plan["status"] !== (manifest.plan.status ?? "active") || plan["task_list_id"] !== (manifest.task_list_id ?? null) || plan["slug"] !== taskManifestPlanSlug(manifest, applyResult.graph.plan_id)) {
42100
+ const slugProvenance = row["slug_provenance"];
42101
+ validateCompensationPlanSlug(this.db, manifest, applyResult.graph.plan_id, slugProvenance);
42102
+ const plan = this.db.query("SELECT project_id, name, description, status, task_list_id FROM plans WHERE id = ? LIMIT 1").get(applyResult.graph.plan_id);
42103
+ if (!plan || plan["project_id"] !== manifest.project_id || plan["name"] !== manifest.plan.name || plan["description"] !== (manifest.plan.description ?? null) || plan["status"] !== (manifest.plan.status ?? "active") || plan["task_list_id"] !== (manifest.task_list_id ?? null)) {
41050
42104
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan changed since apply");
41051
42105
  }
41052
42106
  for (const task3 of manifest.tasks) {
@@ -41123,9 +42177,10 @@ class SqliteTodosTaskManifestBackend {
41123
42177
  const result = { duplicate: false, receipt, absent: true, readback };
41124
42178
  const resultJson = canonicalJson(result);
41125
42179
  this.db.query(`INSERT INTO todos_task_manifest_receipts (
41126
- receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
41127
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
41128
- ) 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);
42180
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
42181
+ request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
42182
+ duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
42183
+ ) VALUES (?, ?, 'todos', 'todos.task-manifest.v1', 1, 'compensate', ?, ?, ?, ?, ?, ?, NULL, 'accepted', NULL, NULL, ?, ?, NULL, ?, ?)`).run(compensationReceiptId, this.tenantId, receipt.operation_id, receipt.step_id, input.idempotency_key, requestDigest, input.precondition_digest, receipt.result_digest, receipt.binding_version, input.receipt_id, resultJson, now4);
41129
42184
  const updated = this.db.query(`UPDATE todos_task_manifest_bindings SET state = 'compensated', version = ?, compensation_receipt_id = ?, updated_at = ?
41130
42185
  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);
41131
42186
  if (updated.changes !== 1) {
@@ -41159,6 +42214,39 @@ function safeIdentifier2(value, field2) {
41159
42214
  function parseJson2(value) {
41160
42215
  return typeof value === "string" ? JSON.parse(value) : value;
41161
42216
  }
42217
+ function parseApplyResult2(value, duplicate) {
42218
+ const parsed = parseJson2(value);
42219
+ return {
42220
+ ...parsed,
42221
+ duplicate,
42222
+ receipt: {
42223
+ ...parsed.receipt,
42224
+ step_id: parsed.receipt.step_id ?? "legacy-apply",
42225
+ precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
42226
+ outcome: parsed.receipt.outcome ?? "accepted",
42227
+ reason: parsed.receipt.reason ?? null,
42228
+ duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
42229
+ }
42230
+ };
42231
+ }
42232
+ function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
42233
+ if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
42234
+ const expected = taskManifestPlanSlug(manifest, planId);
42235
+ if (slug !== expected) {
42236
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
42237
+ }
42238
+ return expected;
42239
+ }
42240
+ if (provenance !== null && provenance !== undefined) {
42241
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
42242
+ }
42243
+ if (slug === null || slug === undefined)
42244
+ return null;
42245
+ if (slug !== null && slug !== undefined) {
42246
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
42247
+ }
42248
+ return null;
42249
+ }
41162
42250
  function timestamp3(value) {
41163
42251
  return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
41164
42252
  }
@@ -41166,6 +42254,41 @@ function fault2(faults, point) {
41166
42254
  if (faults.points.has(point))
41167
42255
  throw new Error(`Injected task-manifest fault at ${point}`);
41168
42256
  }
42257
+ function terminalApplyResult2(input, reason) {
42258
+ const receipt = {
42259
+ receipt_id: input.terminal_receipt_id,
42260
+ authority: "todos",
42261
+ route: "todos.task-manifest.v1",
42262
+ schema_version: 1,
42263
+ kind: "apply",
42264
+ operation_id: input.manifest.operation_id,
42265
+ step_id: input.manifest.step_id,
42266
+ idempotency_key: input.manifest.idempotency_key,
42267
+ request_digest: input.request_digest,
42268
+ precondition_digest: input.manifest.precondition_digest,
42269
+ result_digest: canonicalDigest({
42270
+ outcome: "terminal_nonacceptance",
42271
+ reason,
42272
+ operation_id: input.manifest.operation_id,
42273
+ step_id: input.manifest.step_id,
42274
+ request_digest: input.request_digest
42275
+ }),
42276
+ outcome: "terminal_nonacceptance",
42277
+ reason,
42278
+ duplicate_of_receipt_id: null,
42279
+ binding_version: 0,
42280
+ apply_receipt_id: null,
42281
+ created_at: input.now
42282
+ };
42283
+ return {
42284
+ duplicate: false,
42285
+ receipt,
42286
+ graph: input.graph,
42287
+ readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
42288
+ outbox_ids: [],
42289
+ result_digest: receipt.result_digest
42290
+ };
42291
+ }
41169
42292
  function receiptFromRow3(row) {
41170
42293
  return {
41171
42294
  receipt_id: String(row["receipt_id"]),
@@ -41174,9 +42297,14 @@ function receiptFromRow3(row) {
41174
42297
  schema_version: 1,
41175
42298
  kind: row["kind"],
41176
42299
  operation_id: String(row["operation_id"]),
42300
+ step_id: String(row["step_id"] ?? "legacy-apply"),
41177
42301
  idempotency_key: String(row["idempotency_key"]),
41178
42302
  request_digest: String(row["request_digest"]),
42303
+ precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
41179
42304
  result_digest: String(row["result_digest"]),
42305
+ outcome: row["outcome"] ?? "accepted",
42306
+ reason: row["reason"] == null ? null : row["reason"],
42307
+ duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
41180
42308
  binding_version: Number(row["binding_version"]),
41181
42309
  apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
41182
42310
  created_at: timestamp3(row["created_at"])
@@ -41294,46 +42422,89 @@ class PostgresTodosTaskManifestBackend {
41294
42422
  now4
41295
42423
  ]);
41296
42424
  }
42425
+ async persistTerminal(tx, input, reason) {
42426
+ const result = terminalApplyResult2(input, reason);
42427
+ const resultJson = canonicalJson(result);
42428
+ await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
42429
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
42430
+ idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
42431
+ binding_version, apply_receipt_id, manifest_json, result_json, created_at
42432
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
42433
+ 'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
42434
+ ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
42435
+ result.receipt.receipt_id,
42436
+ this.tenantId,
42437
+ input.manifest.operation_id,
42438
+ input.manifest.step_id,
42439
+ input.manifest.idempotency_key,
42440
+ input.request_digest,
42441
+ input.manifest.precondition_digest,
42442
+ result.receipt.result_digest,
42443
+ reason,
42444
+ canonicalJson(input.manifest),
42445
+ resultJson,
42446
+ input.now
42447
+ ]);
42448
+ const stored = await tx.query(`SELECT receipt_id, result_json
42449
+ FROM todos_task_manifest_terminal_receipts
42450
+ WHERE tenant_id = $1 AND kind = 'apply'
42451
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
42452
+ ORDER BY created_at ASC, receipt_id ASC
42453
+ LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
42454
+ return stored.rows[0] ? parseApplyResult2(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
42455
+ }
41297
42456
  async apply(input, faults) {
41298
42457
  await this.ensureSchema();
41299
42458
  return this.client.transaction(async (tx) => {
41300
42459
  const { manifest } = input;
41301
42460
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
41302
42461
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
42462
+ const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
42463
+ WHERE tenant_id = $1
42464
+ AND kind = 'apply'
42465
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
42466
+ ORDER BY created_at ASC, receipt_id ASC
42467
+ LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
42468
+ if (terminal.rows[0]) {
42469
+ return parseApplyResult2(terminal.rows[0]["result_json"], true);
42470
+ }
41303
42471
  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]);
41304
42472
  if (existing.rows[0]) {
41305
42473
  const binding = existing.rows[0];
41306
- if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
41307
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
42474
+ if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest || binding["step_id"] !== manifest.step_id || binding["precondition_digest"] !== manifest.precondition_digest) {
42475
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
41308
42476
  }
41309
42477
  if (binding["state"] !== "applied") {
41310
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Operation was already compensated");
42478
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
41311
42479
  }
41312
- return { ...parseJson2(binding["result_json"]), duplicate: true };
42480
+ return parseApplyResult2(binding["result_json"], true);
41313
42481
  }
41314
42482
  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]);
41315
42483
  if (reused.rows[0])
41316
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
42484
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
42485
+ if (manifest.idempotency_key !== input.expected_idempotency_key) {
42486
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
42487
+ }
41317
42488
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
41318
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "New manifest binding version must be 0");
42489
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
41319
42490
  }
41320
42491
  const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
41321
42492
  WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
41322
42493
  if (!project.rows[0])
41323
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Project does not exist");
42494
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
41324
42495
  if (manifest.task_list_id) {
41325
42496
  const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
41326
42497
  WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.task_list_id]);
41327
42498
  const payload = taskList.rows[0] ? parseJson2(taskList.rows[0]["payload"]) : null;
41328
42499
  if (!payload || payload["project_id"] !== manifest.project_id) {
41329
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Task list does not belong to the project");
42500
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
41330
42501
  }
41331
42502
  }
41332
42503
  const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
41333
42504
  const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
41334
42505
  WHERE service = $1 AND object_id IN (${placeholders2(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
41335
42506
  if (conflict.rows[0])
41336
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "A deterministic graph id already exists");
42507
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
41337
42508
  await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
41338
42509
  fault2(faults, "after_plan_write");
41339
42510
  for (const task3 of manifest.tasks) {
@@ -41403,9 +42574,14 @@ class PostgresTodosTaskManifestBackend {
41403
42574
  schema_version: 1,
41404
42575
  kind: "apply",
41405
42576
  operation_id: manifest.operation_id,
42577
+ step_id: manifest.step_id,
41406
42578
  idempotency_key: manifest.idempotency_key,
41407
42579
  request_digest: input.request_digest,
42580
+ precondition_digest: manifest.precondition_digest,
41408
42581
  result_digest: input.result_digest,
42582
+ outcome: "accepted",
42583
+ reason: null,
42584
+ duplicate_of_receipt_id: null,
41409
42585
  binding_version: 1,
41410
42586
  apply_receipt_id: null,
41411
42587
  created_at: input.now
@@ -41422,14 +42598,18 @@ class PostgresTodosTaskManifestBackend {
41422
42598
  const resultJson = canonicalJson(result);
41423
42599
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
41424
42600
  receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
41425
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
41426
- ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, 1, NULL, $7::jsonb, $8::jsonb, $9)`, [
42601
+ step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
42602
+ reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
42603
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8, $9, 'accepted', NULL, NULL, 1, NULL, $10::jsonb, $11::jsonb, $12)`, [
41427
42604
  input.receipt_id,
41428
42605
  this.tenantId,
41429
42606
  manifest.operation_id,
41430
42607
  manifest.idempotency_key,
42608
+ manifest.step_id,
41431
42609
  input.request_digest,
42610
+ manifest.precondition_digest,
41432
42611
  input.result_digest,
42612
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
41433
42613
  manifestJson,
41434
42614
  resultJson,
41435
42615
  input.now
@@ -41448,14 +42628,18 @@ class PostgresTodosTaskManifestBackend {
41448
42628
  }
41449
42629
  fault2(faults, "after_outbox_write");
41450
42630
  await tx.query(`INSERT INTO todos_task_manifest_bindings (
41451
- operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
41452
- manifest_json, result_json, state, version, created_at, updated_at
41453
- ) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
42631
+ operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
42632
+ result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
42633
+ state, version, created_at, updated_at
42634
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
41454
42635
  manifest.operation_id,
41455
42636
  this.tenantId,
42637
+ manifest.step_id,
41456
42638
  manifest.idempotency_key,
41457
42639
  input.request_digest,
42640
+ manifest.precondition_digest,
41458
42641
  input.result_digest,
42642
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
41459
42643
  input.receipt_id,
41460
42644
  manifestJson,
41461
42645
  resultJson,
@@ -41468,9 +42652,12 @@ class PostgresTodosTaskManifestBackend {
41468
42652
  async readExact(receiptId2) {
41469
42653
  await this.ensureSchema();
41470
42654
  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]);
41471
- if (!result.rows[0])
42655
+ if (result.rows[0])
42656
+ return parseApplyResult2(result.rows[0]["result_json"], false);
42657
+ const terminal = await this.client.query("SELECT result_json FROM todos_task_manifest_terminal_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, receiptId2]);
42658
+ if (!terminal.rows[0])
41472
42659
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
41473
- return { ...parseJson2(result.rows[0]["result_json"]), duplicate: false };
42660
+ return parseApplyResult2(terminal.rows[0]["result_json"], false);
41474
42661
  }
41475
42662
  async lookupBindingByPlanId(planId) {
41476
42663
  await this.ensureSchema();
@@ -41481,6 +42668,7 @@ class PostgresTodosTaskManifestBackend {
41481
42668
  b.version AS binding_version,
41482
42669
  b.tenant_id AS binding_tenant_id,
41483
42670
  b.operation_id AS binding_operation_id,
42671
+ b.step_id AS binding_step_id,
41484
42672
  b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
41485
42673
  r.tenant_id AS receipt_tenant_id,
41486
42674
  r.authority AS receipt_authority,
@@ -41488,6 +42676,7 @@ class PostgresTodosTaskManifestBackend {
41488
42676
  r.schema_version AS receipt_schema_version,
41489
42677
  r.kind AS receipt_kind,
41490
42678
  r.operation_id AS receipt_operation_id,
42679
+ r.step_id AS receipt_step_id,
41491
42680
  r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
41492
42681
  FROM todos_task_manifest_bindings b
41493
42682
  LEFT JOIN todos_task_manifest_receipts r
@@ -41574,6 +42763,10 @@ class PostgresTodosTaskManifestBackend {
41574
42763
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
41575
42764
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
41576
42765
  }
42766
+ const appliedReceipt = receiptFromRow3(applyRow);
42767
+ if (appliedReceipt.receipt_id !== input.receipt_id || appliedReceipt.operation_id !== input.operation_id || String(binding["operation_id"]) !== appliedReceipt.operation_id || String(binding["step_id"] ?? "legacy-apply") !== appliedReceipt.step_id || String(binding["idempotency_key"]) !== appliedReceipt.idempotency_key || String(binding["request_digest"]) !== appliedReceipt.request_digest || String(binding["precondition_digest"] ?? "0".repeat(64)) !== appliedReceipt.precondition_digest || String(binding["apply_receipt_id"]) !== input.receipt_id || binding["slug_provenance"] !== applyRow["slug_provenance"]) {
42768
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
42769
+ }
41577
42770
  if (binding["state"] !== "applied")
41578
42771
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
41579
42772
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
@@ -41588,12 +42781,18 @@ class PostgresTodosTaskManifestBackend {
41588
42781
  LIMIT 1`, [this.tenantId, input.receipt_id]);
41589
42782
  if (delivered.rows[0])
41590
42783
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
41591
- const applyResult = parseJson2(applyRow["result_json"]);
42784
+ const applyResult = parseApplyResult2(applyRow["result_json"], false);
41592
42785
  const manifest = parseJson2(applyRow["manifest_json"]);
42786
+ const manifestRecord = manifest;
42787
+ const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
41593
42788
  const expectedEffects = [
41594
42789
  {
41595
42790
  topic: "todos.task-manifest.applied",
41596
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
42791
+ payload: {
42792
+ operation_id: manifest.operation_id,
42793
+ ...applyStepId ? { step_id: applyStepId } : {},
42794
+ project_id: manifest.project_id
42795
+ }
41597
42796
  },
41598
42797
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
41599
42798
  ];
@@ -41641,9 +42840,15 @@ class PostgresTodosTaskManifestBackend {
41641
42840
  }
41642
42841
  const appliedAt = receiptFromRow3(applyRow).created_at;
41643
42842
  const expectedPayloads = new Map;
42843
+ const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
42844
+ WHERE service = $1 AND object_type = 'plans' AND object_id = $2
42845
+ LIMIT 1`, [this.service, applyResult.graph.plan_id]);
42846
+ const actualPlan = planRow.rows[0] ? parseJson2(planRow.rows[0]["payload"]) : null;
42847
+ const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
42848
+ planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
41644
42849
  expectedPayloads.set(applyResult.graph.plan_id, {
41645
42850
  type: "plans",
41646
- payload: canonicalJson(planPayload({ manifest, graph: applyResult.graph, now: appliedAt }))
42851
+ payload: canonicalJson(planExpected)
41647
42852
  });
41648
42853
  for (const task3 of manifest.tasks)
41649
42854
  expectedPayloads.set(applyResult.graph.task_ids[task3.key], {
@@ -41743,14 +42948,17 @@ class PostgresTodosTaskManifestBackend {
41743
42948
  const readback = await this.readback(tx, applyResult.graph);
41744
42949
  const result = { duplicate: false, receipt, absent: true, readback };
41745
42950
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
41746
- receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
41747
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
41748
- ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10)`, [
42951
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
42952
+ request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
42953
+ duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
42954
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, 'accepted', NULL, NULL, $9, $10, NULL, $11::jsonb, $12)`, [
41749
42955
  compensationReceiptId,
41750
42956
  this.tenantId,
41751
42957
  receipt.operation_id,
42958
+ receipt.step_id,
41752
42959
  input.idempotency_key,
41753
42960
  requestDigest,
42961
+ input.precondition_digest,
41754
42962
  receipt.result_digest,
41755
42963
  receipt.binding_version,
41756
42964
  input.receipt_id,
@@ -41810,36 +43018,83 @@ function resolveTenantId(value) {
41810
43018
  }
41811
43019
  return tenantId;
41812
43020
  }
43021
+ function taskManifestRequestDigest(manifest) {
43022
+ const { idempotency_key: _idempotencyKey, ...request } = manifest;
43023
+ return canonicalDigest(request);
43024
+ }
43025
+ function taskManifestCompensationRequestDigest(request) {
43026
+ return canonicalDigest(request);
43027
+ }
43028
+ function deriveTodosTaskManifestApplyPreconditionDigest(input) {
43029
+ return canonicalDigest({
43030
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
43031
+ direction: "apply",
43032
+ operation_id: input.operation_id,
43033
+ step_id: input.step_id,
43034
+ project_id: input.project_id,
43035
+ task_list_id: input.task_list_id ?? null,
43036
+ expected_binding_version: input.if_binding_version ?? 0
43037
+ });
43038
+ }
43039
+ function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
43040
+ return canonicalDigest({
43041
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
43042
+ direction: "compensate",
43043
+ operation_id: input.operation_id,
43044
+ step_id: input.step_id,
43045
+ apply_receipt_id: input.receipt_id,
43046
+ expected_binding_version: input.if_binding_version
43047
+ });
43048
+ }
43049
+ function deriveTodosTaskManifestIdempotencyKey(input) {
43050
+ return `tmk_${canonicalDigest({
43051
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
43052
+ ...input
43053
+ }).slice(0, 48)}`;
43054
+ }
41813
43055
  function normalize(input, now4) {
41814
43056
  const parsed = parseTodosTaskManifest(input);
41815
43057
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
41816
43058
  if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
41817
43059
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BOUNDS_EXCEEDED", `Task manifest requires ${requestBytes} bytes but the bound is ${TODOS_TASK_MANIFEST_BOUNDS.request_bytes}`, { request_bytes: requestBytes, request_byte_limit: TODOS_TASK_MANIFEST_BOUNDS.request_bytes });
41818
43060
  }
43061
+ const { idempotency_key: _idempotencyKey, ...request } = parsed;
43062
+ const request_digest = taskManifestRequestDigest(request);
41819
43063
  const manifest = sanitizeManifest(parsed);
43064
+ const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
43065
+ if (manifest.precondition_digest !== expectedPreconditionDigest) {
43066
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
43067
+ }
43068
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
43069
+ operation_id: manifest.operation_id,
43070
+ step_id: manifest.step_id,
43071
+ direction: "apply",
43072
+ target_selector: manifest.project_id,
43073
+ request_digest,
43074
+ precondition_digest: manifest.precondition_digest
43075
+ });
41820
43076
  const task_ids = Object.fromEntries(manifest.tasks.map((task3) => [
41821
43077
  task3.key,
41822
- deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task3.key)
43078
+ deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task3.key)
41823
43079
  ]));
41824
43080
  const graph = {
41825
- plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
43081
+ plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
41826
43082
  task_ids,
41827
- comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task3.key, String(index)))),
41828
- verification_ids: manifest.tasks.flatMap((task3) => (task3.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "verification", task3.key, String(index)))),
43083
+ comment_ids: manifest.tasks.flatMap((task3) => (task3.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "comment", task3.key, String(index)))),
43084
+ verification_ids: manifest.tasks.flatMap((task3) => (task3.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "verification", task3.key, String(index)))),
41829
43085
  dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
41830
43086
  };
41831
- const request_digest = canonicalDigest(parsed);
41832
43087
  const effectInputs = [
41833
43088
  {
41834
43089
  topic: "todos.task-manifest.applied",
41835
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
43090
+ payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
41836
43091
  },
41837
43092
  ...manifest.effects ?? []
41838
43093
  ];
41839
43094
  const outbox = effectInputs.map((effect2, index) => {
41840
43095
  const payload = { ...effect2.payload };
41841
43096
  return {
41842
- id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
43097
+ id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
41843
43098
  topic: effect2.topic,
41844
43099
  payload,
41845
43100
  digest: canonicalDigest({ topic: effect2.topic, payload })
@@ -41849,11 +43104,14 @@ function normalize(input, now4) {
41849
43104
  return {
41850
43105
  manifest,
41851
43106
  request_digest,
43107
+ expected_idempotency_key: expectedIdempotencyKey,
41852
43108
  result_digest,
41853
- receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
43109
+ receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
43110
+ terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
41854
43111
  graph,
41855
43112
  outbox,
41856
- now: now4
43113
+ now: now4,
43114
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
41857
43115
  };
41858
43116
  }
41859
43117
  function sanitizeManifest(manifest) {
@@ -41911,6 +43169,10 @@ class PackageOwnedTodosTaskManifestAuthority {
41911
43169
  tenant_id: this.tenantId,
41912
43170
  backend: this.backend.kind,
41913
43171
  deterministic_ids: true,
43172
+ operation_step_identity: true,
43173
+ deterministic_idempotency_keys: true,
43174
+ terminal_nonacceptance_receipts: true,
43175
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
41914
43176
  immutable_receipts: true,
41915
43177
  transactional_outbox: true,
41916
43178
  idempotent_outbox_delivery: true,
@@ -41940,7 +43202,11 @@ class PackageOwnedTodosTaskManifestAuthority {
41940
43202
  async apply(input) {
41941
43203
  const normalized = normalize(input, this.now());
41942
43204
  const faults = await this.prepareFaults();
41943
- return this.bounded(await this.backend.apply(normalized, faults));
43205
+ const result = this.bounded(await this.backend.apply(normalized, faults));
43206
+ if (result.receipt.outcome === "terminal_nonacceptance") {
43207
+ throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
43208
+ }
43209
+ return result;
41944
43210
  }
41945
43211
  readExact(receiptId2) {
41946
43212
  if (!receiptId2 || receiptId2.length > 200) {
@@ -41973,18 +43239,48 @@ class PackageOwnedTodosTaskManifestAuthority {
41973
43239
  async compensate(input) {
41974
43240
  const request = parseTodosTaskManifestCompensation(input);
41975
43241
  const applied = await this.backend.readExact(request.receipt_id);
41976
- const requestDigest = canonicalDigest(request);
41977
- const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", applied.receipt.operation_id, request.idempotency_key, requestDigest);
43242
+ if (applied.receipt.outcome !== "accepted") {
43243
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
43244
+ }
43245
+ if (request.operation_id !== applied.receipt.operation_id) {
43246
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
43247
+ }
43248
+ if (request.step_id === applied.receipt.step_id) {
43249
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
43250
+ }
43251
+ const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
43252
+ if (request.precondition_digest !== expectedPreconditionDigest) {
43253
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
43254
+ }
43255
+ const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
43256
+ const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
43257
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
43258
+ operation_id: request.operation_id,
43259
+ step_id: request.step_id,
43260
+ direction: "compensate",
43261
+ target_selector: request.receipt_id,
43262
+ request_digest: requestDigest,
43263
+ precondition_digest: request.precondition_digest
43264
+ });
43265
+ if (request.idempotency_key !== expectedIdempotencyKey) {
43266
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
43267
+ }
43268
+ const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
41978
43269
  const receipt = {
41979
43270
  receipt_id: compensationReceiptId,
41980
43271
  authority: "todos",
41981
43272
  route: TODOS_TASK_MANIFEST_ROUTE,
41982
43273
  schema_version: 1,
41983
43274
  kind: "compensate",
41984
- operation_id: applied.receipt.operation_id,
43275
+ operation_id: request.operation_id,
43276
+ step_id: request.step_id,
41985
43277
  idempotency_key: request.idempotency_key,
41986
43278
  request_digest: requestDigest,
43279
+ precondition_digest: request.precondition_digest,
41987
43280
  result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
43281
+ outcome: "accepted",
43282
+ reason: null,
43283
+ duplicate_of_receipt_id: null,
41988
43284
  binding_version: request.if_binding_version + 1,
41989
43285
  apply_receipt_id: applied.receipt.receipt_id,
41990
43286
  created_at: this.now()
@@ -42166,7 +43462,7 @@ function createTodosTaskManifestHttpClient(options) {
42166
43462
  return new TodosTaskManifestHttpClient(options);
42167
43463
  }
42168
43464
  // src/ai-tools.ts
42169
- import { createHash as createHash15 } from "crypto";
43465
+ import { createHash as createHash16 } from "crypto";
42170
43466
  // src/cli/cloud-router.ts
42171
43467
  import { resolveStorageClient } from "@hasna/contracts/client/storage";
42172
43468
  import { normalizeStorageMode } from "@hasna/contracts/mode";
@@ -43474,7 +44770,7 @@ function deriveTodosAiUpdateTaskApprovalIdentity(input) {
43474
44770
  expected_version: input.expected_version,
43475
44771
  patch
43476
44772
  });
43477
- const payloadDigest = createHash15("sha256").update(canonical).digest("hex");
44773
+ const payloadDigest = createHash16("sha256").update(canonical).digest("hex");
43478
44774
  return {
43479
44775
  ref: `todos-ai:update_task:${payloadDigest}`,
43480
44776
  payload_digest: payloadDigest
@@ -43629,9 +44925,9 @@ function normalizeUpdateTaskInput(input) {
43629
44925
  if (!Object.hasOwn(record, "expected_version")) {
43630
44926
  throw new Error("expected_version is required");
43631
44927
  }
43632
- const idempotencyKey = boundedRequiredString(record, "idempotency_key", TODOS_AI_UPDATE_TASK_LIMITS.max_idempotency_key_bytes);
43633
- const idempotencyBytes = ENCODER.encode(idempotencyKey).byteLength;
43634
- if (idempotencyBytes < TODOS_AI_UPDATE_TASK_LIMITS.min_idempotency_key_bytes || !/^[A-Za-z0-9._:-]+$/.test(idempotencyKey)) {
44928
+ const idempotencyKey2 = boundedRequiredString(record, "idempotency_key", TODOS_AI_UPDATE_TASK_LIMITS.max_idempotency_key_bytes);
44929
+ const idempotencyBytes = ENCODER.encode(idempotencyKey2).byteLength;
44930
+ if (idempotencyBytes < TODOS_AI_UPDATE_TASK_LIMITS.min_idempotency_key_bytes || !/^[A-Za-z0-9._:-]+$/.test(idempotencyKey2)) {
43635
44931
  throw new Error("idempotency_key must be a bounded stable identifier");
43636
44932
  }
43637
44933
  const patchValue = record["patch"];
@@ -43650,7 +44946,7 @@ function normalizeUpdateTaskInput(input) {
43650
44946
  expected_version: expectedVersion,
43651
44947
  patch,
43652
44948
  changed_fields: changedFields,
43653
- idempotency_key: idempotencyKey,
44949
+ idempotency_key: idempotencyKey2,
43654
44950
  payload_digest: identity.payload_digest,
43655
44951
  approval_ref: identity.ref
43656
44952
  };
@@ -44331,7 +45627,7 @@ init_task_lifecycle();
44331
45627
  init_task_crud();
44332
45628
  init_redaction();
44333
45629
  import { Database as Database3 } from "bun:sqlite";
44334
- import { createHash as createHash16 } from "crypto";
45630
+ import { createHash as createHash17 } from "crypto";
44335
45631
  import { existsSync as existsSync9, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
44336
45632
  import { basename as basename2, dirname as dirname6, join as join9, resolve as resolve10 } from "path";
44337
45633
 
@@ -44614,8 +45910,8 @@ function normalizePath3(input) {
44614
45910
  return resolve10(input);
44615
45911
  }
44616
45912
  function sourceStoreId(sourceDbPath) {
44617
- const digest = createHash16("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
44618
- return `sqlite:${digest}`;
45913
+ const digest2 = createHash17("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
45914
+ return `sqlite:${digest2}`;
44619
45915
  }
44620
45916
  function inferSourceRepoPath(sourceDbPath) {
44621
45917
  const normalized = normalizePath3(sourceDbPath);
@@ -46576,7 +47872,7 @@ init_comments();
46576
47872
 
46577
47873
  // src/db/api-keys.ts
46578
47874
  init_database();
46579
- import { createHash as createHash17, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
47875
+ import { createHash as createHash18, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual3 } from "crypto";
46580
47876
  function rowToRecord(row) {
46581
47877
  return {
46582
47878
  id: row.id,
@@ -46590,7 +47886,7 @@ function rowToRecord(row) {
46590
47886
  };
46591
47887
  }
46592
47888
  function hashApiKey(key2) {
46593
- return createHash17("sha256").update(key2).digest("hex");
47889
+ return createHash18("sha256").update(key2).digest("hex");
46594
47890
  }
46595
47891
  function safeEqualHex(a, b) {
46596
47892
  if (a.length !== b.length)
@@ -50491,7 +51787,7 @@ init_database();
50491
51787
  init_tasks();
50492
51788
  import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
50493
51789
  import { basename as basename5 } from "path";
50494
- import { createHash as createHash18 } from "crypto";
51790
+ import { createHash as createHash19 } from "crypto";
50495
51791
  init_secret_redaction();
50496
51792
  var INBOX_INTAKE_SCHEMA = "todos.inbox_intake.v1";
50497
51793
  var INTAKE_SOURCE_TYPES = [
@@ -50504,7 +51800,7 @@ var INTAKE_SOURCE_TYPES = [
50504
51800
  ];
50505
51801
  var INTAKE_TRIAGE_STATUSES = ["preview", "triaged", "duplicate", "created"];
50506
51802
  function fingerprint2(text) {
50507
- return createHash18("sha256").update(text).digest("hex").slice(0, 16);
51803
+ return createHash19("sha256").update(text).digest("hex").slice(0, 16);
50508
51804
  }
50509
51805
  function loadRawContent(input) {
50510
51806
  if (input.github_url) {
@@ -55964,7 +57260,7 @@ init_database();
55964
57260
  init_tasks();
55965
57261
  init_redaction();
55966
57262
  init_sync_utils();
55967
- import { createHash as createHash19 } from "crypto";
57263
+ import { createHash as createHash20 } from "crypto";
55968
57264
  import { existsSync as existsSync22, readFileSync as readFileSync20, statSync as statSync9 } from "fs";
55969
57265
  import { hostname as hostname3, platform, arch } from "os";
55970
57266
  import { dirname as dirname15, join as join18, resolve as resolve17 } from "path";
@@ -55986,7 +57282,7 @@ var CONFIG_FILES = [
55986
57282
  "dashboard/vite.config.ts"
55987
57283
  ];
55988
57284
  function sha2566(value) {
55989
- return createHash19("sha256").update(value).digest("hex");
57285
+ return createHash20("sha256").update(value).digest("hex");
55990
57286
  }
55991
57287
  function fileRecord(root, relativePath) {
55992
57288
  const path = join18(root, relativePath);
@@ -56101,8 +57397,8 @@ function defaultSnapshotDir() {
56101
57397
  return join18(dirname15(resolve17(dbPath)), "environment-snapshots");
56102
57398
  }
56103
57399
  function snapshotWithId(snapshot) {
56104
- const digest = sha2566(JSON.stringify(snapshot)).slice(0, 24);
56105
- return { id: `env_${digest}`, ...snapshot };
57400
+ const digest2 = sha2566(JSON.stringify(snapshot)).slice(0, 24);
57401
+ return { id: `env_${digest2}`, ...snapshot };
56106
57402
  }
56107
57403
  function captureEnvironmentSnapshot(input = {}) {
56108
57404
  const root = resolve17(input.root || process.cwd());
@@ -56238,7 +57534,7 @@ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
56238
57534
  init_database();
56239
57535
  init_projects();
56240
57536
  init_plans();
56241
- import { createHash as createHash20 } from "crypto";
57537
+ import { createHash as createHash21 } from "crypto";
56242
57538
  import { mkdirSync as mkdirSync18, writeFileSync as writeFileSync16 } from "fs";
56243
57539
  import { dirname as dirname16, join as join19 } from "path";
56244
57540
  var DECISION_RECORD_SCHEMA = "todos.decision_record.v1";
@@ -56294,7 +57590,7 @@ function rowToDecisionRecord(row) {
56294
57590
  }
56295
57591
  function stableSnapshotHash(payload) {
56296
57592
  const { captured_at: _capturedAt, ...rest } = payload;
56297
- return createHash20("sha256").update(JSON.stringify(rest)).digest("hex");
57593
+ return createHash21("sha256").update(JSON.stringify(rest)).digest("hex");
56298
57594
  }
56299
57595
  function createDecisionRecord(input, db) {
56300
57596
  const d = db || getDatabase();
@@ -60787,7 +62083,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
60787
62083
  init_tasks();
60788
62084
  init_task_files();
60789
62085
  import { existsSync as existsSync27, readFileSync as readFileSync24, statSync as statSync10 } from "fs";
60790
- import { createHash as createHash21 } from "crypto";
62086
+ import { createHash as createHash22 } from "crypto";
60791
62087
  import { relative as relative6, resolve as resolve18, join as join25 } from "path";
60792
62088
  var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
60793
62089
  var DEFAULT_EXTENSIONS = new Set([
@@ -60852,7 +62148,7 @@ var SKIP_DIRS2 = new Set([
60852
62148
  ".parcel-cache"
60853
62149
  ]);
60854
62150
  function stableHash(value) {
60855
- return createHash21("sha256").update(value).digest("hex");
62151
+ return createHash22("sha256").update(value).digest("hex");
60856
62152
  }
60857
62153
  function normalizePathForMatch(value) {
60858
62154
  return value.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -61750,7 +63046,7 @@ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
61750
63046
  }
61751
63047
  // src/lib/agent-replay-simulator.ts
61752
63048
  init_redaction();
61753
- import { createHash as createHash22 } from "crypto";
63049
+ import { createHash as createHash23 } from "crypto";
61754
63050
  import { readFileSync as readFileSync25 } from "fs";
61755
63051
  function isObject(value) {
61756
63052
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -61772,7 +63068,7 @@ function stable2(value) {
61772
63068
  return Object.fromEntries(Object.keys(value).sort().map((key2) => [key2, stable2(value[key2])]));
61773
63069
  }
61774
63070
  function fingerprint3(value) {
61775
- return createHash22("sha256").update(JSON.stringify(stable2(value))).digest("hex");
63071
+ return createHash23("sha256").update(JSON.stringify(stable2(value))).digest("hex");
61776
63072
  }
61777
63073
  function unpackFixture(input) {
61778
63074
  if (!isObject(input))
@@ -62011,7 +63307,7 @@ function renderAgentReplaySimulationMarkdown(simulation) {
62011
63307
  }
62012
63308
  // src/lib/local-extensions.ts
62013
63309
  init_config2();
62014
- import { createHash as createHash23, createVerify } from "crypto";
63310
+ import { createHash as createHash24, createVerify } from "crypto";
62015
63311
  import { existsSync as existsSync28, readdirSync as readdirSync6, readFileSync as readFileSync26, statSync as statSync11 } from "fs";
62016
63312
  import { basename as basename6, join as join26, resolve as resolve19 } from "path";
62017
63313
  init_redaction();
@@ -62099,7 +63395,7 @@ function parseJson3(path) {
62099
63395
  return JSON.parse(readFileSync26(path, "utf8"));
62100
63396
  }
62101
63397
  function sha2567(bytes) {
62102
- return `sha256:${createHash23("sha256").update(bytes).digest("hex")}`;
63398
+ return `sha256:${createHash24("sha256").update(bytes).digest("hex")}`;
62103
63399
  }
62104
63400
  function compareVersions(a, b) {
62105
63401
  const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
@@ -64714,6 +66010,8 @@ export {
64714
66010
  testTerminalNotificationRule,
64715
66011
  testLocalEventHook,
64716
66012
  tasksFromTemplate,
66013
+ taskManifestRequestDigest,
66014
+ taskManifestCompensationRequestDigest,
64717
66015
  taskFromTemplate,
64718
66016
  tagToPriority,
64719
66017
  syncWithAgents,
@@ -65448,6 +66746,9 @@ export {
65448
66746
  detectInboxSourceType,
65449
66747
  detectCyclesFromEdges,
65450
66748
  describeTerminalNotificationRule,
66749
+ deriveTodosTaskManifestIdempotencyKey,
66750
+ deriveTodosTaskManifestCompensationPreconditionDigest,
66751
+ deriveTodosTaskManifestApplyPreconditionDigest,
65451
66752
  deriveTodosProjectRegistrationIdempotencyKey,
65452
66753
  deriveTodosAiUpdateTaskApprovalIdentity,
65453
66754
  deriveInboxTitle,
@@ -65692,6 +66993,8 @@ export {
65692
66993
  TODOS_TASK_ROUTE_STATE_SCHEMA_VERSION,
65693
66994
  TODOS_TASK_MANIFEST_SCHEMA_VERSION,
65694
66995
  TODOS_TASK_MANIFEST_ROUTE,
66996
+ TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
66997
+ TODOS_TASK_MANIFEST_CALLER_ROUTE,
65695
66998
  TODOS_TASK_MANIFEST_BOUNDS,
65696
66999
  TODOS_STORAGE_TABLES,
65697
67000
  TODOS_STORAGE_FALLBACK_ENV,