@inneranimalmedia/agentsam-sdk 2.6.10 → 2.6.11

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 (261) hide show
  1. package/README.md +3 -0
  2. package/TRADEMARKS.md +22 -0
  3. package/apps/README.md +3 -0
  4. package/apps/cad-creator/LICENSE +21 -0
  5. package/apps/cad-creator/README.md +4 -1
  6. package/apps/cad-creator/shared/cad/package.json +1 -1
  7. package/apps/client-cms-editor/LICENSE +21 -0
  8. package/apps/client-cms-editor/README.md +3 -0
  9. package/apps/client-cms-editor/backend/package.json +2 -2
  10. package/apps/client-cms-editor/frontend/package.json +3 -3
  11. package/apps/client-cms-editor/package-lock.json +43 -43
  12. package/apps/client-cms-editor/package.json +3 -3
  13. package/apps/client-cms-editor/reference/harvest/HARVEST_RECEIPT.json +1 -1
  14. package/apps/client-cms-editor/reference/harvest/README.md +1 -1
  15. package/apps/client-cms-editor/reference/harvest/plans/ARCHIVE-INTAKE.md +1 -1
  16. package/apps/client-cms-editor/reference/harvest/plans/HARVEST-INDEX.md +1 -1
  17. package/apps/client-cms-editor/reference/harvest/plans/TOOLING-LAB.md +1 -1
  18. package/apps/client-cms-editor/shared/cms/package.json +1 -1
  19. package/docs/MINI.md +4 -2
  20. package/docs/REPO_FILE_MAP.txt +0 -1
  21. package/docs/SOURCE_ARCHITECTURE.md +1 -1
  22. package/docs/architecture/DUAL_ANALYTICS_PRODUCT.md +140 -0
  23. package/docs/content-studio/REVISION_GATE_2026-09-27.md +1 -1
  24. package/docs/engineering/go-portability/01-product-project-deployment-authority.md +69 -0
  25. package/docs/engineering/provider-internal-renames/01-lane-initialization.md +20 -0
  26. package/docs/engineering/provider-internal-renames/01a-runtime-environment-contract.md +115 -0
  27. package/docs/engineering/provider-internal-renames/02-cfoa.md +57 -0
  28. package/docs/engineering/provider-internal-renames/03-gclioa.md +32 -0
  29. package/docs/engineering/provider-internal-renames/04-goaude.md +50 -0
  30. package/docs/engineering/provider-internal-renames/05-gdrv.md +33 -0
  31. package/docs/engineering/provider-internal-renames/06-verification.md +103 -0
  32. package/docs/engineering/provider-internal-renames/README.md +77 -0
  33. package/docs/engineering/provider-internal-renames/runtime-environment-contract.json +220 -0
  34. package/docs/engineering/test-diagnostics/go-cli-test-hang-20261003.md +49 -0
  35. package/docs/engineering/test-diagnostics/go-skip-deploy-docker-fix.md +90 -0
  36. package/docs/knowledge-service-verification.md +3 -3
  37. package/docs/plans/AGENTSAM-SUSPEND-QUEUE-KNOWLEDGE-RDP-2026-10-03.md +870 -0
  38. package/docs/plans/LOCAL-STUDIO-GRADUATION-2026-09-26.md +9 -9
  39. package/docs/plans/MACHINE-FIRST-APPS-TUI-2026-09-24.md +11 -11
  40. package/docs/plans/RECONCILIATION-2026-09-19.md +6 -6
  41. package/docs/plans/SAM_WORK_PROTOCOL.md +667 -0
  42. package/docs/plans/TOMORROW-EXTENSIVE-TODO-2026-09-26.md +2 -2
  43. package/docs/plans/UNIVERSAL-INGEST-MACHINE-FIRST-2026-09-24.md +3 -3
  44. package/examples/hyperspace-demo/src/main.tsx +227 -110
  45. package/migrations/d1/0020_agentsam_receipt_catalog.sql +35 -0
  46. package/migrations/runtime/0006_agent_run_suspend_activity.sql +53 -0
  47. package/migrations/runtime/0007_queue_control.sql +40 -0
  48. package/migrations/runtime/0008_agent_run_dependencies.sql +24 -0
  49. package/migrations/runtime/0008_analytics_legacy_bootstrap.sql +43 -0
  50. package/migrations/runtime/0009_analytics_authority.sql +108 -0
  51. package/package.json +26 -11
  52. package/packages/agentsam-brand/README.md +3 -0
  53. package/packages/agentsam-brand/package.json +2 -2
  54. package/packages/agentsam-contracts/dist/adapter.d.ts +22 -0
  55. package/packages/agentsam-contracts/dist/adapter.js +0 -0
  56. package/packages/agentsam-contracts/dist/agent.d.ts +66 -0
  57. package/packages/agentsam-contracts/dist/agent.js +15 -0
  58. package/packages/agentsam-contracts/dist/artifacts.d.ts +70 -0
  59. package/packages/agentsam-contracts/dist/artifacts.js +0 -0
  60. package/packages/agentsam-contracts/dist/authority.d.ts +21 -0
  61. package/packages/agentsam-contracts/dist/authority.js +0 -0
  62. package/packages/agentsam-contracts/dist/context.d.ts +14 -0
  63. package/packages/agentsam-contracts/dist/context.js +0 -0
  64. package/packages/agentsam-contracts/dist/errors.d.ts +121 -0
  65. package/packages/agentsam-contracts/dist/errors.js +5 -0
  66. package/packages/agentsam-contracts/dist/events.d.ts +53 -0
  67. package/packages/agentsam-contracts/dist/events.js +0 -0
  68. package/packages/agentsam-contracts/dist/execution.d.ts +38 -0
  69. package/packages/agentsam-contracts/dist/execution.js +0 -0
  70. package/packages/agentsam-contracts/dist/hooks.d.ts +31 -0
  71. package/packages/agentsam-contracts/dist/hooks.js +0 -0
  72. package/packages/agentsam-contracts/dist/identity.d.ts +15 -0
  73. package/packages/agentsam-contracts/dist/identity.js +0 -0
  74. package/packages/agentsam-contracts/dist/index.d.ts +16 -0
  75. package/packages/agentsam-contracts/dist/index.js +164 -0
  76. package/packages/agentsam-contracts/dist/models.d.ts +9 -0
  77. package/packages/agentsam-contracts/dist/models.js +0 -0
  78. package/packages/agentsam-contracts/dist/providers.d.ts +27 -0
  79. package/packages/agentsam-contracts/dist/providers.js +0 -0
  80. package/packages/agentsam-contracts/dist/repository.d.ts +56 -0
  81. package/packages/agentsam-contracts/dist/repository.js +15 -0
  82. package/packages/agentsam-contracts/dist/run.d.ts +113 -0
  83. package/packages/agentsam-contracts/dist/run.js +132 -0
  84. package/packages/agentsam-contracts/dist/tools.d.ts +82 -0
  85. package/packages/agentsam-contracts/dist/tools.js +0 -0
  86. package/packages/agentsam-contracts/dist/widgets.d.ts +38 -0
  87. package/packages/agentsam-contracts/dist/widgets.js +0 -0
  88. package/packages/agentsam-contracts/package.json +9 -1
  89. package/packages/agentsam-contracts/scripts/build.mjs +1 -1
  90. package/packages/agentsam-contracts/src/agent.ts +78 -3
  91. package/packages/agentsam-contracts/src/index.ts +2 -0
  92. package/packages/agentsam-contracts/src/run.ts +320 -0
  93. package/packages/agentsam-contracts/src/widgets.ts +58 -0
  94. package/packages/agentsam-contracts/test/contracts.test.mjs +1 -1
  95. package/packages/agentsam-contracts/test/run-contract.test.mjs +94 -0
  96. package/packages/agentsam-errors/package.json +1 -1
  97. package/packages/agentsam-goap/README.md +3 -0
  98. package/packages/agentsam-goap/package.json +2 -2
  99. package/packages/agentsam-hooks/package.json +2 -2
  100. package/packages/agentsam-hooks/src/adapters/agents.js +48 -22
  101. package/packages/agentsam-hooks/src/index.d.ts +16 -0
  102. package/packages/agentsam-knowledge/package.json +1 -1
  103. package/packages/agentsam-knowledge/src/backends/index.js +1 -1
  104. package/packages/agentsam-merch/README.md +62 -0
  105. package/packages/agentsam-merch/bin/agentsam-merch.mjs +16 -0
  106. package/packages/agentsam-merch/package.json +65 -0
  107. package/packages/agentsam-merch/profiles/completeful/dtg-back-large.json +42 -0
  108. package/packages/agentsam-merch/profiles/completeful/laser-engraving.json +39 -0
  109. package/packages/agentsam-merch/profiles/generic/embroidery-digitization.json +22 -0
  110. package/packages/agentsam-merch/schemas/collection-candidate.schema.json +66 -0
  111. package/packages/agentsam-merch/schemas/manufacturing-profile.schema.json +221 -0
  112. package/packages/agentsam-merch/src/adapters-completeful.js +50 -0
  113. package/packages/agentsam-merch/src/cli.js +93 -0
  114. package/packages/agentsam-merch/src/collection-lab.js +60 -0
  115. package/packages/agentsam-merch/src/compatibility.js +501 -0
  116. package/packages/agentsam-merch/src/derivatives.js +112 -0
  117. package/packages/agentsam-merch/src/index.d.ts +316 -0
  118. package/packages/agentsam-merch/src/index.js +46 -0
  119. package/packages/agentsam-merch/src/node/index.d.ts +58 -0
  120. package/packages/agentsam-merch/src/node/index.js +6 -0
  121. package/packages/agentsam-merch/src/node/raster.js +142 -0
  122. package/packages/agentsam-merch/src/node/vector.js +239 -0
  123. package/packages/agentsam-merch/src/product-concepts.js +118 -0
  124. package/packages/agentsam-merch/src/profile-registry.js +104 -0
  125. package/packages/agentsam-merch/src/profile.js +158 -0
  126. package/packages/agentsam-merch/src/transforms.js +215 -0
  127. package/packages/agentsam-merch/test/merch.test.mjs +332 -0
  128. package/packages/agentsam-merch/test/node-transforms.test.mjs +149 -0
  129. package/packages/agentsam-nav/dist/browser.global.js +71 -34
  130. package/packages/agentsam-nav/dist/browser.js +3179 -781
  131. package/packages/agentsam-nav/dist/index.js +3171 -773
  132. package/packages/agentsam-nav/package.json +1 -1
  133. package/packages/agentsam-queue-control/README.md +4 -1
  134. package/packages/agentsam-queue-control/package.json +4 -3
  135. package/packages/agentsam-queue-control/schemas/job-envelope.schema.json +111 -11
  136. package/packages/agentsam-queue-control/src/adapters/cloudflare.js +34 -6
  137. package/packages/agentsam-queue-control/src/adapters/memory.js +11 -2
  138. package/packages/agentsam-queue-control/src/adapters/sqlite.js +259 -0
  139. package/packages/agentsam-queue-control/src/contracts.js +207 -3
  140. package/packages/agentsam-queue-control/src/control-plane.js +62 -2
  141. package/packages/agentsam-queue-control/src/index.js +1 -0
  142. package/packages/agentsam-queue-control/test/queue-control.test.mjs +310 -0
  143. package/packages/agentsam-queue-control/test/sqlite-adapter.test.mjs +91 -0
  144. package/packages/agentsam-rapid-rust/README.md +3 -0
  145. package/packages/agentsam-repository/README.md +3 -0
  146. package/packages/agentsam-repository/package.json +1 -1
  147. package/packages/agentsam-vault/package.json +1 -1
  148. package/packages/agentsam-workbench/README.md +43 -0
  149. package/packages/agentsam-workbench/package.json +13 -3
  150. package/packages/agentsam-workbench/src/agent/AgentConversationSurface.tsx +108 -0
  151. package/packages/agentsam-workbench/src/agent/AgentPanel.tsx +25 -8
  152. package/packages/agentsam-workbench/src/agent/AgentRuntimeField.tsx +152 -0
  153. package/packages/agentsam-workbench/src/agent/Composer.tsx +10 -1
  154. package/packages/agentsam-workbench/src/agent/RunMode.ts +26 -0
  155. package/packages/agentsam-workbench/src/agent/Thread.tsx +18 -3
  156. package/packages/agentsam-workbench/src/agent/index.ts +3 -0
  157. package/packages/agentsam-workbench/src/index.ts +4 -0
  158. package/packages/agentsam-workbench/src/manufacturing/PreflightIndicator.tsx +203 -0
  159. package/packages/agentsam-workbench/src/manufacturing/index.ts +2 -0
  160. package/packages/agentsam-workbench/src/manufacturing/loading.ts +147 -0
  161. package/packages/agentsam-workbench/src/widgets/BuiltinWidget.tsx +22 -0
  162. package/packages/agentsam-workbench/src/widgets/CountdownWidget.tsx +68 -0
  163. package/packages/agentsam-workbench/src/widgets/WidgetFrame.tsx +56 -0
  164. package/packages/agentsam-workbench/src/widgets/countdown.ts +25 -0
  165. package/packages/agentsam-workbench/src/widgets/index.ts +6 -0
  166. package/packages/agentsam-workbench/src/widgets/registry.ts +13 -0
  167. package/packages/agentsam-workbench/src/widgets/useCountdown.ts +128 -0
  168. package/packages/agentsam-workbench/src/widgets/widgets.css +102 -0
  169. package/packages/connectors/{cloudflare → cfoa}/package.json +3 -3
  170. package/packages/heuristic-theme/package.json +1 -1
  171. package/packages/identity/README.md +3 -0
  172. package/packages/identity/backend/README.md +3 -0
  173. package/packages/identity/package.json +1 -1
  174. package/packages/identity/src/frontend/auth-portal/README.md +3 -0
  175. package/packages/identity/src/index.js +1 -1
  176. package/packages/identity/src/oauth/README.md +4 -1
  177. package/packages/identity/src/oauth/credentials.js +1 -1
  178. package/packages/identity/src/server/worker-router.js +1 -1
  179. package/packages/identity/tests/google-desktop-identity-login.test.mjs +1 -1
  180. package/packages/identity/tests/lane1-packed-consumer.test.mjs +1 -1
  181. package/packages/identity/tests/oauth-presets-accounts-ssot.test.mjs +1 -1
  182. package/packages/providers/completeful/package.json +1 -1
  183. package/packages/theme-church-site/package.json +1 -1
  184. package/packages/theme-companions-site/package.json +1 -1
  185. package/packages/theme-floors-site/package.json +1 -1
  186. package/packages/theme-fuelnfree-site/package.json +1 -1
  187. package/packages/theme-handyman-site/package.json +1 -1
  188. package/packages/theme-insurance-site/package.json +1 -1
  189. package/packages/theme-scenes/package.json +1 -1
  190. package/packages/theme-shinshu-site/package.json +1 -1
  191. package/packages/work-graph/package.json +1 -1
  192. package/protocol/models/README.md +3 -0
  193. package/protocol/receipts/agentsam.receipt.v1.json +95 -0
  194. package/protocol/theme-refinery/receipts/products-registry-batch-20260924.json +6 -6
  195. package/protocol/theme-refinery/receipts/theme-harvest-20260924.json +6 -6
  196. package/skills/README.md +3 -0
  197. package/src/acp/agent-executor.js +106 -0
  198. package/src/acp/client.js +316 -0
  199. package/src/acp/dependencies.js +426 -0
  200. package/src/acp/http.js +207 -0
  201. package/src/acp/index.js +22 -0
  202. package/src/acp/runs.js +125 -0
  203. package/src/acp/runtime-selection.js +180 -0
  204. package/src/cli/command-catalog.js +3 -0
  205. package/src/cli.js +24 -0
  206. package/src/commands/cloudflare-login.js +1 -1
  207. package/src/commands/cloudflare.js +2 -2
  208. package/src/commands/connections.js +1 -1
  209. package/src/commands/go.js +76 -6
  210. package/src/commands/merch.js +5 -0
  211. package/src/commands/mini.js +1 -1
  212. package/src/commands/receipts.js +140 -0
  213. package/src/commands/run.js +253 -0
  214. package/src/go/container.js +1 -1
  215. package/src/go/contract.js +61 -18
  216. package/src/go/discover.js +0 -4
  217. package/src/go/index.js +13 -1
  218. package/src/go/probe.js +109 -0
  219. package/src/go/{cloudflare.js → providers/cloudflare.js} +36 -136
  220. package/src/go/verify.js +1 -1
  221. package/src/indexing/ingest/inventory.js +1 -1
  222. package/src/lib/deploy/local-studio.js +2 -2
  223. package/src/lib/mini/scaffold.js +1 -1
  224. package/src/lib/whoami-capabilities.js +1 -1
  225. package/src/local/runtime-store.js +8 -0
  226. package/src/mcp/cloudflare-bundles.js +1 -1
  227. package/src/plugins/index.js +1 -0
  228. package/src/plugins/registry.js +59 -6
  229. package/src/receipts/contract.js +68 -0
  230. package/src/receipts/index.js +20 -0
  231. package/src/receipts/prune.js +64 -0
  232. package/src/receipts/sinks.js +285 -0
  233. package/templates/mini/gadget/app.js +15 -40
  234. package/templates/mini/gadget/index.html +7 -7
  235. package/templates/mini/shared/style.css +1 -1
  236. package/packages/agentsam-repository/src/goap.js.bak-20260922011251 +0 -670
  237. /package/packages/connectors/{cloudflare → cfoa}/OAUTH_STORAGE_AUDIT.md +0 -0
  238. /package/packages/connectors/{cloudflare → cfoa}/src/api-client.js +0 -0
  239. /package/packages/connectors/{cloudflare → cfoa}/src/capabilities.js +0 -0
  240. /package/packages/connectors/{cloudflare → cfoa}/src/credential.js +0 -0
  241. /package/packages/connectors/{cloudflare → cfoa}/src/families/brand-protection.js +0 -0
  242. /package/packages/connectors/{cloudflare → cfoa}/src/families/keyless.js +0 -0
  243. /package/packages/connectors/{cloudflare → cfoa}/src/families/pages.js +0 -0
  244. /package/packages/connectors/{cloudflare → cfoa}/src/families/scanner.js +0 -0
  245. /package/packages/connectors/{cloudflare → cfoa}/src/families/snippets.js +0 -0
  246. /package/packages/connectors/{cloudflare → cfoa}/src/families/tag-gateway.js +0 -0
  247. /package/packages/connectors/{cloudflare → cfoa}/src/families/tags.js +0 -0
  248. /package/packages/connectors/{cloudflare → cfoa}/src/families/token-validation.js +0 -0
  249. /package/packages/connectors/{cloudflare → cfoa}/src/families/vectorize.js +0 -0
  250. /package/packages/connectors/{cloudflare → cfoa}/src/families/workflows.js +0 -0
  251. /package/packages/connectors/{cloudflare → cfoa}/src/index.js +0 -0
  252. /package/packages/connectors/{cloudflare → cfoa}/src/oauth-persist.js +0 -0
  253. /package/packages/connectors/{cloudflare → cfoa}/src/owner.js +0 -0
  254. /package/packages/connectors/{cloudflare → cfoa}/src/permission-options.js +0 -0
  255. /package/packages/connectors/{cloudflare → cfoa}/src/routes.js +0 -0
  256. /package/packages/connectors/{cloudflare → cfoa}/src/scope-catalog.js +0 -0
  257. /package/packages/connectors/{cloudflare → cfoa}/src/vault.js +0 -0
  258. /package/packages/connectors/{cloudflare → cfoa}/tests/capabilities.test.mjs +0 -0
  259. /package/packages/connectors/{cloudflare → cfoa}/tests/connector.test.mjs +0 -0
  260. /package/packages/connectors/{cloudflare → cfoa}/tests/oauth-persist.test.mjs +0 -0
  261. /package/packages/identity/src/oauth/{google-desktop-exchange.js → goaude.js} +0 -0
@@ -7,6 +7,7 @@ export const JOB_STATUSES = Object.freeze([
7
7
  'waiting',
8
8
  'batch_waiting',
9
9
  'awaiting_input',
10
+ 'retry_scheduled',
10
11
  'completed',
11
12
  'failed',
12
13
  'cancelled',
@@ -36,6 +37,7 @@ export const DEFAULT_RETRY_POLICY = Object.freeze({
36
37
  backoff: 'exponential',
37
38
  initial_delay_ms: 1_000,
38
39
  max_delay_ms: 60_000,
40
+ jitter: true,
39
41
  });
40
42
 
41
43
  function clean(value) {
@@ -48,6 +50,198 @@ function assertEnum(value, allowed, field) {
48
50
  }
49
51
  }
50
52
 
53
+ function epochSeconds(value, fallback, field = 'timestamp') {
54
+ if (value == null || value === '') return fallback;
55
+ const numeric = Number(value);
56
+ if (!Number.isFinite(numeric) || numeric < 0) throw new TypeError(`${field} must be a non-negative epoch second`);
57
+ return Math.floor(numeric);
58
+ }
59
+
60
+ export function computeRetryDelayMs(retry = DEFAULT_RETRY_POLICY, attempt = 1, random = Math.random) {
61
+ const policy = { ...DEFAULT_RETRY_POLICY, ...(retry || {}) };
62
+ const n = Math.max(1, Number.isInteger(attempt) ? attempt : 1);
63
+ const initial = Math.max(0, Number(policy.initial_delay_ms) || 0);
64
+ const maximum = Math.max(initial, Number(policy.max_delay_ms) || initial);
65
+ let delay = policy.backoff === 'exponential'
66
+ ? initial * (2 ** (n - 1))
67
+ : initial;
68
+ delay = Math.min(maximum, delay);
69
+ if (policy.jitter && delay > 0) {
70
+ const sample = Math.max(0, Math.min(1, Number(random()) || 0));
71
+ delay = Math.floor((delay / 2) + ((delay / 2) * sample));
72
+ }
73
+ return Math.max(0, Math.floor(delay));
74
+ }
75
+
76
+ export function createRetrySchedule(job, { now_ms = Date.now(), random = Math.random, error = null } = {}) {
77
+ assertJobEnvelope(job);
78
+ const nextAttempt = (Number.isInteger(job.attempt) ? job.attempt : 0) + 1;
79
+ const maxAttempts = Math.max(1, Number(job.retry?.max_attempts) || DEFAULT_RETRY_POLICY.max_attempts);
80
+ if (nextAttempt >= maxAttempts) {
81
+ return {
82
+ retryable: false,
83
+ exhausted: true,
84
+ attempt: nextAttempt,
85
+ delay_ms: 0,
86
+ available_at: null,
87
+ job: null,
88
+ };
89
+ }
90
+ const delayMs = computeRetryDelayMs(job.retry, nextAttempt, random);
91
+ const availableAt = Math.ceil((Number(now_ms) + delayMs) / 1000);
92
+ return {
93
+ retryable: true,
94
+ exhausted: false,
95
+ attempt: nextAttempt,
96
+ delay_ms: delayMs,
97
+ available_at: availableAt,
98
+ job: {
99
+ ...job,
100
+ status: 'retry_scheduled',
101
+ attempt: nextAttempt,
102
+ available_at: availableAt,
103
+ lease_owner: null,
104
+ lease_expires_at: null,
105
+ metadata: {
106
+ ...(job.metadata || {}),
107
+ ...(error ? { last_error: String(error?.message || error) } : {}),
108
+ },
109
+ },
110
+ };
111
+ }
112
+
113
+ export function createDeadLetterJob(job, {
114
+ error = null,
115
+ attempt = (Number.isInteger(job?.attempt) ? job.attempt : 0) + 1,
116
+ now = Math.floor(Date.now() / 1000),
117
+ } = {}) {
118
+ assertJobEnvelope(job);
119
+ const originalKey = clean(job.idempotency_key) || job.id;
120
+ return createJobEnvelope({
121
+ id: job.id + ':dlq:' + attempt,
122
+ account_id: job.account_id,
123
+ kind: 'dead_letter',
124
+ logical_queue: 'dead_letter',
125
+ executor: 'queue',
126
+ priority: job.priority === 'urgent' ? 'urgent' : 'high',
127
+ parent_job_id: job.id,
128
+ conversation_id: job.conversation_id,
129
+ agent_id: job.agent_id,
130
+ source_run_id: job.source_run_id,
131
+ step_id: job.step_id,
132
+ idempotency_key: originalKey + ':dlq:' + attempt,
133
+ payload: {
134
+ original_job: job,
135
+ failure: error == null ? null : {
136
+ message: String(error?.message || error),
137
+ code: clean(error?.code) || null,
138
+ },
139
+ },
140
+ metadata: {
141
+ ...(job.metadata || {}),
142
+ dead_letter: true,
143
+ original_job_id: job.id,
144
+ original_kind: job.kind,
145
+ exhausted_attempt: attempt,
146
+ },
147
+ retry: {
148
+ max_attempts: 1,
149
+ backoff: 'fixed',
150
+ initial_delay_ms: 0,
151
+ max_delay_ms: 0,
152
+ jitter: false,
153
+ },
154
+ attempt: 0,
155
+ available_at: now,
156
+ created_at: now,
157
+ });
158
+ }
159
+
160
+ export function isJobAvailable(job, now = Math.floor(Date.now() / 1000)) {
161
+ return (job?.available_at ?? 0) <= now;
162
+ }
163
+
164
+ export function jobExecutionIdentity(job) {
165
+ assertJobEnvelope(job);
166
+ return Object.freeze({
167
+ job_id: job.id,
168
+ run_id: job.source_run_id ?? null,
169
+ step_id: job.step_id ?? null,
170
+ attempt: job.attempt ?? 0,
171
+ idempotency_key: clean(job.idempotency_key) || job.id,
172
+ lease_owner: job.lease_owner ?? null,
173
+ lease_expires_at: job.lease_expires_at ?? null,
174
+ lease_generation: job.lease_generation ?? 0,
175
+ });
176
+ }
177
+
178
+ export function canClaimJobLease(job, { now = Math.floor(Date.now() / 1000), owner = null } = {}) {
179
+ assertJobEnvelope(job);
180
+ if (isTerminalJobStatus(job.status)) return false;
181
+ if (!isJobAvailable(job, now)) return false;
182
+ const leaseOwner = clean(job.lease_owner);
183
+ const leaseExpiresAt = job.lease_expires_at == null
184
+ ? null
185
+ : epochSeconds(job.lease_expires_at, null, 'lease_expires_at');
186
+ if (!leaseOwner || leaseExpiresAt == null || leaseExpiresAt <= now) return true;
187
+ return Boolean(owner && leaseOwner === clean(owner));
188
+ }
189
+
190
+ export function claimJobLease(job, { owner, now = Math.floor(Date.now() / 1000), ttl_seconds = 60 } = {}) {
191
+ assertJobEnvelope(job);
192
+ const leaseOwner = clean(owner);
193
+ if (!leaseOwner) throw new TypeError('lease owner is required');
194
+ const ttl = Math.max(1, Math.floor(Number(ttl_seconds) || 0));
195
+ if (!canClaimJobLease(job, { now, owner: leaseOwner })) {
196
+ const error = new Error(isJobAvailable(job, now) ? 'job_lease_held' : 'job_not_available');
197
+ error.code = isJobAvailable(job, now) ? 'lease_held' : 'job_not_available';
198
+ throw error;
199
+ }
200
+ return {
201
+ ...job,
202
+ status: 'claimed',
203
+ lease_owner: leaseOwner,
204
+ lease_expires_at: now + ttl,
205
+ lease_generation: Math.max(0, Number(job.lease_generation) || 0) + 1,
206
+ };
207
+ }
208
+
209
+ export function refreshJobLease(job, { owner, now = Math.floor(Date.now() / 1000), ttl_seconds = 60 } = {}) {
210
+ assertJobEnvelope(job);
211
+ const leaseOwner = clean(owner);
212
+ if (!leaseOwner || leaseOwner !== clean(job.lease_owner)) {
213
+ const error = new Error('job_lease_owner_mismatch');
214
+ error.code = 'lease_owner_mismatch';
215
+ throw error;
216
+ }
217
+ const currentExpiry = epochSeconds(job.lease_expires_at, 0, 'lease_expires_at');
218
+ if (currentExpiry <= now) {
219
+ const error = new Error('job_lease_expired');
220
+ error.code = 'lease_expired';
221
+ throw error;
222
+ }
223
+ const ttl = Math.max(1, Math.floor(Number(ttl_seconds) || 0));
224
+ return { ...job, lease_expires_at: now + ttl };
225
+ }
226
+
227
+ export function releaseJobLease(job, { owner, status = 'queued', available_at = job.available_at } = {}) {
228
+ assertJobEnvelope(job);
229
+ const leaseOwner = clean(owner);
230
+ if (leaseOwner && clean(job.lease_owner) && leaseOwner !== clean(job.lease_owner)) {
231
+ const error = new Error('job_lease_owner_mismatch');
232
+ error.code = 'lease_owner_mismatch';
233
+ throw error;
234
+ }
235
+ assertEnum(status, JOB_STATUSES, 'status');
236
+ return {
237
+ ...job,
238
+ status,
239
+ available_at: epochSeconds(available_at, job.available_at, 'available_at'),
240
+ lease_owner: null,
241
+ lease_expires_at: null,
242
+ };
243
+ }
244
+
51
245
  export function createJobEnvelope(input = {}) {
52
246
  const kind = clean(input.kind);
53
247
  if (!kind) throw new TypeError('kind is required');
@@ -56,6 +250,8 @@ export function createJobEnvelope(input = {}) {
56
250
  if (!accountId) throw new TypeError('account_id is required');
57
251
 
58
252
  const now = input.created_at ?? Math.floor(Date.now() / 1000);
253
+ const availableAt = epochSeconds(input.available_at ?? input.availableAt, now, 'available_at');
254
+ const id = clean(input.id) || `job_${randomUUID().replaceAll('-', '')}`;
59
255
  const priority = input.priority ?? 'normal';
60
256
  assertEnum(priority, JOB_PRIORITIES, 'priority');
61
257
 
@@ -64,7 +260,7 @@ export function createJobEnvelope(input = {}) {
64
260
 
65
261
  return {
66
262
  schema_version: '1',
67
- id: clean(input.id) || `job_${randomUUID().replaceAll('-', '')}`,
263
+ id,
68
264
  account_id: accountId,
69
265
  kind,
70
266
  logical_queue: clean(input.logical_queue) || null,
@@ -74,8 +270,12 @@ export function createJobEnvelope(input = {}) {
74
270
  parent_job_id: clean(input.parent_job_id) || null,
75
271
  conversation_id: clean(input.conversation_id) || null,
76
272
  agent_id: clean(input.agent_id) || null,
77
- source_run_id: clean(input.source_run_id) || null,
78
- idempotency_key: clean(input.idempotency_key) || null,
273
+ source_run_id: clean(input.source_run_id ?? input.run_id) || null,
274
+ step_id: clean(input.step_id) || null,
275
+ idempotency_key: clean(input.idempotency_key ?? input.idempotencyKey) || id,
276
+ lease_owner: clean(input.lease_owner) || null,
277
+ lease_expires_at: input.lease_expires_at == null ? null : epochSeconds(input.lease_expires_at, null, 'lease_expires_at'),
278
+ lease_generation: Number.isInteger(input.lease_generation) ? Math.max(0, input.lease_generation) : 0,
79
279
  payload: input.payload ?? null,
80
280
  payload_ref: clean(input.payload_ref) || null,
81
281
  metadata: input.metadata && typeof input.metadata === 'object' ? input.metadata : {},
@@ -84,6 +284,7 @@ export function createJobEnvelope(input = {}) {
84
284
  ...(input.retry && typeof input.retry === 'object' ? input.retry : {}),
85
285
  },
86
286
  attempt: Number.isInteger(input.attempt) ? input.attempt : 0,
287
+ available_at: availableAt,
87
288
  created_at: now,
88
289
  };
89
290
  }
@@ -97,6 +298,9 @@ export function assertJobEnvelope(job) {
97
298
  assertEnum(job.priority ?? 'normal', JOB_PRIORITIES, 'job.priority');
98
299
  assertEnum(job.executor ?? 'queue', EXECUTOR_KINDS, 'job.executor');
99
300
  assertEnum(job.status ?? 'queued', JOB_STATUSES, 'job.status');
301
+ epochSeconds(job.available_at, job.created_at ?? 0, 'job.available_at');
302
+ if (job.lease_expires_at != null) epochSeconds(job.lease_expires_at, null, 'job.lease_expires_at');
303
+ if (job.lease_owner && job.lease_expires_at == null) throw new TypeError('job.lease_expires_at is required when job.lease_owner is set');
100
304
  return job;
101
305
  }
102
306
 
@@ -1,4 +1,4 @@
1
- import { createJobEnvelope, assertJobEnvelope } from './contracts.js';
1
+ import { createJobEnvelope, assertJobEnvelope, computeRetryDelayMs, createRetrySchedule, createDeadLetterJob } from './contracts.js';
2
2
  import { buildQueueTopology, resolvePhysicalQueue } from './topology.js';
3
3
  import { routeWork, shouldRequireProvisionApproval } from './policy.js';
4
4
  import { JobDispatcher } from './dispatch.js';
@@ -117,6 +117,16 @@ export class QueueControl {
117
117
  return this.adapter.ensureTopology(this.topology, options);
118
118
  }
119
119
 
120
+ async deadLetter(job, error, { attempt = (job.attempt || 0) + 1 } = {}) {
121
+ const deadLetter = createDeadLetterJob(job, { error, attempt });
122
+ const queue = resolvePhysicalQueue(this.topology, 'dead_letter');
123
+ const receipt = await this.adapter.publish(queue, deadLetter, {
124
+ logical_queue: 'dead_letter',
125
+ dead_letter: true,
126
+ });
127
+ return { job: deadLetter, queue, receipt: receipt ?? null };
128
+ }
129
+
120
130
  async consume(messages, context = {}) {
121
131
  const results = [];
122
132
  for (const message of messages) {
@@ -127,12 +137,62 @@ export class QueueControl {
127
137
  if (typeof message?.ack === 'function') message.ack();
128
138
  results.push({ job_id: job.id, ok: true, result });
129
139
  } catch (error) {
130
- if (typeof message?.retry === 'function') message.retry();
140
+ let retry = null;
141
+ if (typeof message?.retry === 'function') {
142
+ const attempt = Number.isInteger(message.attempts) ? message.attempts : (job.attempt + 1);
143
+ const maxAttempts = Math.max(1, Number(job.retry?.max_attempts) || 3);
144
+ if (attempt >= maxAttempts) {
145
+ // Do not ACK an exhausted provider-managed message here. A configured
146
+ // provider DLQ is the crash-safe fallback; retry/failure delivery lets
147
+ // the provider move it after max_retries instead of silently deleting it.
148
+ const delayMs = computeRetryDelayMs(job.retry, attempt);
149
+ const delaySeconds = Math.max(0, Math.ceil(delayMs / 1000));
150
+ message.retry(delaySeconds > 0 ? { delaySeconds } : undefined);
151
+ retry = {
152
+ scheduled: false,
153
+ exhausted: true,
154
+ provider_managed: true,
155
+ delegated_to_provider_dlq: true,
156
+ attempt,
157
+ delay_ms: delayMs,
158
+ delay_seconds: delaySeconds,
159
+ };
160
+ } else {
161
+ const delayMs = computeRetryDelayMs(job.retry, attempt);
162
+ const delaySeconds = Math.max(0, Math.ceil(delayMs / 1000));
163
+ message.retry(delaySeconds > 0 ? { delaySeconds } : undefined);
164
+ retry = { scheduled: true, provider_managed: true, attempt, delay_ms: delayMs, delay_seconds: delaySeconds };
165
+ }
166
+ } else if (job.logical_queue) {
167
+ const schedule = createRetrySchedule(job, { error });
168
+ if (schedule.retryable && schedule.job) {
169
+ const queue = resolvePhysicalQueue(this.topology, job.logical_queue);
170
+ await this.adapter.publish(queue, schedule.job, { logical_queue: job.logical_queue });
171
+ retry = {
172
+ scheduled: true,
173
+ provider_managed: false,
174
+ delay_ms: schedule.delay_ms,
175
+ available_at: schedule.available_at,
176
+ attempt: schedule.attempt,
177
+ };
178
+ } else {
179
+ const deadLetter = await this.deadLetter(job, error, { attempt: schedule.attempt });
180
+ retry = {
181
+ scheduled: false,
182
+ exhausted: true,
183
+ attempt: schedule.attempt,
184
+ dead_lettered: true,
185
+ dead_letter_job_id: deadLetter.job.id,
186
+ dead_letter_queue: deadLetter.queue,
187
+ };
188
+ }
189
+ }
131
190
  results.push({
132
191
  job_id: job.id,
133
192
  ok: false,
134
193
  error: String(error?.message || error),
135
194
  code: error?.code ?? null,
195
+ retry,
136
196
  });
137
197
  }
138
198
  }
@@ -5,3 +5,4 @@ export * from './dispatch.js';
5
5
  export * from './control-plane.js';
6
6
  export * from './adapters/memory.js';
7
7
  export * from './adapters/cloudflare.js';
8
+ export * from './adapters/sqlite.js';
@@ -4,6 +4,17 @@ import assert from 'node:assert/strict';
4
4
  import {
5
5
  QueueControl,
6
6
  MemoryQueueAdapter,
7
+ CloudflareQueueBindingAdapter,
8
+ CloudflareQueueApiAdapter,
9
+ canClaimJobLease,
10
+ claimJobLease,
11
+ computeRetryDelayMs,
12
+ createJobEnvelope,
13
+ createRetrySchedule,
14
+ delaySecondsForAvailableAt,
15
+ jobExecutionIdentity,
16
+ refreshJobLease,
17
+ releaseJobLease,
7
18
  buildQueueTopology,
8
19
  resolvePhysicalQueue,
9
20
  routeWork,
@@ -98,3 +109,302 @@ test('infrastructure provisioning is approval-gated by default', async () => {
98
109
  assert.equal(result.provider, 'memory');
99
110
  assert.ok(result.created.includes('demo-test-jobs'));
100
111
  });
112
+
113
+
114
+ test('job envelopes support delayed availability without making early work runnable', async () => {
115
+ const adapter = new MemoryQueueAdapter();
116
+ const control = new QueueControl({
117
+ adapter,
118
+ topology: buildQueueTopology({ namespace: 'demo', environment: 'test' }),
119
+ });
120
+ const queue = resolvePhysicalQueue(control.topology, 'deployments');
121
+ const createdAt = 1_000;
122
+ const availableAt = 1_060;
123
+ const result = await control.enqueue({
124
+ account_id: 'acct_real',
125
+ kind: 'deploy.cloudflare',
126
+ }, {
127
+ created_at: createdAt,
128
+ availableAt,
129
+ });
130
+
131
+ assert.equal(result.job.available_at, availableAt);
132
+ assert.equal((await adapter.pull(queue, 10, { now: 1_059 })).length, 0);
133
+ assert.equal(adapter.depth(queue), 1);
134
+ assert.equal((await adapter.pull(queue, 10, { now: 1_060 })).length, 1);
135
+ });
136
+
137
+ test('retry scheduling uses bounded exponential backoff with injectable jitter', () => {
138
+ const job = createJobEnvelope({
139
+ account_id: 'acct_real',
140
+ kind: 'repository.index',
141
+ created_at: 1_000,
142
+ retry: {
143
+ max_attempts: 3,
144
+ initial_delay_ms: 1_000,
145
+ max_delay_ms: 10_000,
146
+ backoff: 'exponential',
147
+ jitter: true,
148
+ },
149
+ });
150
+
151
+ assert.equal(computeRetryDelayMs(job.retry, 1, () => 0), 500);
152
+ assert.equal(computeRetryDelayMs(job.retry, 2, () => 1), 2_000);
153
+
154
+ const retry = createRetrySchedule(job, { now_ms: 1_000_000, random: () => 1 });
155
+ assert.equal(retry.retryable, true);
156
+ assert.equal(retry.job.status, 'retry_scheduled');
157
+ assert.equal(retry.job.attempt, 1);
158
+ assert.equal(retry.available_at, 1_001);
159
+
160
+ const exhausted = createRetrySchedule({ ...job, attempt: 2 }, { now_ms: 1_000_000 });
161
+ assert.equal(exhausted.retryable, false);
162
+ assert.equal(exhausted.exhausted, true);
163
+ });
164
+
165
+ test('local queue failures are re-enqueued for a future deterministic retry', async () => {
166
+ const adapter = new MemoryQueueAdapter();
167
+ const control = new QueueControl({
168
+ adapter,
169
+ topology: buildQueueTopology({ namespace: 'demo', environment: 'test' }),
170
+ });
171
+ control.register('repository.*', async () => {
172
+ throw new Error('temporary');
173
+ });
174
+
175
+ await control.enqueue({ account_id: 'acct_real', kind: 'repository.index' }, {
176
+ created_at: 1_000,
177
+ retry: { max_attempts: 3, initial_delay_ms: 1_000, max_delay_ms: 1_000, jitter: false },
178
+ });
179
+ const queue = resolvePhysicalQueue(control.topology, 'indexing');
180
+ const [job] = await adapter.pull(queue, 1, { now: 1_000 });
181
+ const [result] = await control.consume([job]);
182
+
183
+ assert.equal(result.ok, false);
184
+ assert.equal(result.retry.scheduled, true);
185
+ assert.equal(result.retry.provider_managed, false);
186
+ assert.equal(adapter.depth(queue), 1);
187
+ assert.equal((await adapter.pull(queue, 1, { now: result.retry.available_at - 1 })).length, 0);
188
+ assert.equal((await adapter.pull(queue, 1, { now: result.retry.available_at })).length, 1);
189
+ });
190
+
191
+ test('Cloudflare queue binding maps available_at to delaySeconds and rejects delays beyond provider limit', async () => {
192
+ const calls = [];
193
+ const adapter = new CloudflareQueueBindingAdapter({
194
+ bindings: {
195
+ default: {
196
+ async send(body, options) { calls.push({ body, options }); },
197
+ },
198
+ },
199
+ });
200
+ const job = createJobEnvelope({
201
+ account_id: 'acct_real',
202
+ kind: 'deploy.cloudflare',
203
+ created_at: 1_000,
204
+ available_at: 1_600,
205
+ });
206
+
207
+ const receipt = await adapter.publish('demo', job, { now: 1_000 });
208
+ assert.equal(receipt.delay_seconds, 600);
209
+ assert.equal(calls[0].options.delaySeconds, 600);
210
+ assert.equal(delaySecondsForAvailableAt(87_400, 1_000), 86_400);
211
+ assert.throws(() => delaySecondsForAvailableAt(87_401, 1_000), /exceeds_24h/);
212
+ });
213
+
214
+ test('provider-managed retries receive a deterministic retry delay instead of immediate polling', async () => {
215
+ const adapter = new MemoryQueueAdapter();
216
+ const control = new QueueControl({
217
+ adapter,
218
+ topology: buildQueueTopology({ namespace: 'demo', environment: 'test' }),
219
+ });
220
+ control.register('deploy.*', async () => {
221
+ throw new Error('pending');
222
+ });
223
+ const job = createJobEnvelope({
224
+ account_id: 'acct_real',
225
+ kind: 'deploy.cloudflare',
226
+ logical_queue: 'deployments',
227
+ retry: { max_attempts: 3, initial_delay_ms: 2_000, max_delay_ms: 2_000, jitter: false },
228
+ });
229
+ let retryOptions = null;
230
+ const message = {
231
+ body: job,
232
+ attempts: 1,
233
+ retry(options) { retryOptions = options; },
234
+ };
235
+
236
+ const [result] = await control.consume([message]);
237
+ assert.equal(result.retry.provider_managed, true);
238
+ assert.equal(result.retry.delay_seconds, 2);
239
+ assert.deepEqual(retryOptions, { delaySeconds: 2 });
240
+ });
241
+
242
+
243
+ test('job execution identity always carries a stable idempotency key and run/step attribution', () => {
244
+ const job = createJobEnvelope({
245
+ id: 'job_fixed',
246
+ account_id: 'acct_real',
247
+ kind: 'embeddings.generate',
248
+ run_id: 'arun_parent',
249
+ step_id: 'step_embed',
250
+ });
251
+ assert.equal(job.idempotency_key, 'job_fixed');
252
+ assert.deepEqual(jobExecutionIdentity(job), {
253
+ job_id: 'job_fixed',
254
+ run_id: 'arun_parent',
255
+ step_id: 'step_embed',
256
+ attempt: 0,
257
+ idempotency_key: 'job_fixed',
258
+ lease_owner: null,
259
+ lease_expires_at: null,
260
+ lease_generation: 0,
261
+ });
262
+
263
+ const explicit = createJobEnvelope({
264
+ id: 'job_other',
265
+ account_id: 'acct_real',
266
+ kind: 'deploy.cloudflare',
267
+ idempotency_key: 'deploy:commit:abc123',
268
+ });
269
+ assert.equal(explicit.idempotency_key, 'deploy:commit:abc123');
270
+ });
271
+
272
+ test('lease contract supports claim, refresh, release, crash expiry, and rejects live double claims', () => {
273
+ const job = createJobEnvelope({
274
+ account_id: 'acct_real',
275
+ kind: 'repository.index',
276
+ created_at: 1_000,
277
+ });
278
+ const claimed = claimJobLease(job, { owner: 'worker-a', now: 1_000, ttl_seconds: 30 });
279
+ assert.equal(claimed.status, 'claimed');
280
+ assert.equal(claimed.lease_owner, 'worker-a');
281
+ assert.equal(claimed.lease_expires_at, 1_030);
282
+ assert.equal(claimed.lease_generation, 1);
283
+ assert.equal(canClaimJobLease(claimed, { owner: 'worker-b', now: 1_010 }), false);
284
+ assert.throws(
285
+ () => claimJobLease(claimed, { owner: 'worker-b', now: 1_010 }),
286
+ /job_lease_held/,
287
+ );
288
+
289
+ const refreshed = refreshJobLease(claimed, { owner: 'worker-a', now: 1_010, ttl_seconds: 30 });
290
+ assert.equal(refreshed.lease_expires_at, 1_040);
291
+ assert.throws(
292
+ () => refreshJobLease(refreshed, { owner: 'worker-a', now: 1_041, ttl_seconds: 30 }),
293
+ /job_lease_expired/,
294
+ );
295
+
296
+ assert.equal(canClaimJobLease(refreshed, { owner: 'worker-b', now: 1_041 }), true);
297
+ const reclaimed = claimJobLease(refreshed, { owner: 'worker-b', now: 1_041, ttl_seconds: 30 });
298
+ assert.equal(reclaimed.lease_generation, 2);
299
+ assert.equal(reclaimed.lease_owner, 'worker-b');
300
+
301
+ const released = releaseJobLease(reclaimed, { owner: 'worker-b', status: 'queued' });
302
+ assert.equal(released.lease_owner, null);
303
+ assert.equal(released.lease_expires_at, null);
304
+ assert.equal(released.status, 'queued');
305
+ });
306
+
307
+
308
+ test('provider-managed retry exhaustion stops requeueing instead of polling forever', async () => {
309
+ const adapter = new MemoryQueueAdapter();
310
+ const control = new QueueControl({
311
+ adapter,
312
+ topology: buildQueueTopology({ namespace: 'demo', environment: 'test' }),
313
+ });
314
+ control.register('deploy.*', async () => {
315
+ throw new Error('still pending');
316
+ });
317
+ const job = createJobEnvelope({
318
+ account_id: 'acct_real',
319
+ kind: 'deploy.cloudflare',
320
+ logical_queue: 'deployments',
321
+ retry: { max_attempts: 3, initial_delay_ms: 1_000, max_delay_ms: 1_000, jitter: false },
322
+ });
323
+ let retried = false;
324
+ let acked = false;
325
+ const [result] = await control.consume([{
326
+ body: job,
327
+ attempts: 3,
328
+ retry() { retried = true; },
329
+ ack() { acked = true; },
330
+ }]);
331
+ assert.equal(retried, true);
332
+ assert.equal(acked, false);
333
+ assert.deepEqual(result.retry, {
334
+ scheduled: false,
335
+ exhausted: true,
336
+ provider_managed: true,
337
+ delegated_to_provider_dlq: true,
338
+ attempt: 3,
339
+ delay_ms: 1000,
340
+ delay_seconds: 1,
341
+ });
342
+ });
343
+
344
+ test('exhausted local retries are materialized as a deterministic dead-letter job', async () => {
345
+ const topology = buildQueueTopology({ namespace: 'acp', environment: 'test' });
346
+ const adapter = new MemoryQueueAdapter();
347
+ const control = new QueueControl({ adapter, topology });
348
+ control.register('always.fail', async () => {
349
+ throw Object.assign(new Error('boom'), { code: 'provider_unavailable' });
350
+ });
351
+
352
+ const job = createJobEnvelope({
353
+ id: 'job_exhaust_local',
354
+ account_id: 'acct_1',
355
+ kind: 'always.fail',
356
+ logical_queue: 'jobs',
357
+ retry: {
358
+ max_attempts: 1,
359
+ backoff: 'fixed',
360
+ initial_delay_ms: 0,
361
+ max_delay_ms: 0,
362
+ jitter: false,
363
+ },
364
+ });
365
+
366
+ const [result] = await control.consume([job]);
367
+ assert.equal(result.ok, false);
368
+ assert.equal(result.retry.exhausted, true);
369
+ assert.equal(result.retry.dead_lettered, true);
370
+
371
+ const dlq = resolvePhysicalQueue(topology, 'dead_letter');
372
+ const [dead] = await adapter.pull(dlq, 1);
373
+ assert.equal(dead.kind, 'dead_letter');
374
+ assert.equal(dead.payload.original_job.id, 'job_exhaust_local');
375
+ assert.equal(dead.payload.failure.code, 'provider_unavailable');
376
+ assert.equal(dead.metadata.dead_letter, true);
377
+ });
378
+
379
+ test('Cloudflare consumer provisioning carries the topology dead-letter queue', async () => {
380
+ const requests = [];
381
+ const adapter = new CloudflareQueueApiAdapter({
382
+ cloudflareAccountId: 'acct_cf',
383
+ apiToken: 'token',
384
+ fetchImpl: async (url, init = {}) => {
385
+ requests.push({ url, init });
386
+ if (init.method === 'POST' && url.endsWith('/consumers')) {
387
+ return Response.json({ success: true, result: { consumer_id: 'consumer_1' } });
388
+ }
389
+ if (init.method === 'POST') {
390
+ const name = JSON.parse(init.body).queue_name;
391
+ return Response.json({ success: true, result: { queue_id: name + '_id', queue_name: name } });
392
+ }
393
+ return Response.json({ success: true, result: [] });
394
+ },
395
+ });
396
+
397
+ const topology = buildQueueTopology({ namespace: 'acp', environment: 'prod' });
398
+ const jobsQueue = resolvePhysicalQueue(topology, 'jobs');
399
+ await adapter.ensureTopology(topology, {
400
+ consumers: {
401
+ [jobsQueue]: { script_name: 'agentsam-acp-consumer', max_retries: 3 },
402
+ },
403
+ });
404
+
405
+ const consumerRequest = requests.find((row) => row.url.endsWith('/consumers'));
406
+ assert.ok(consumerRequest);
407
+ const body = JSON.parse(consumerRequest.init.body);
408
+ assert.equal(body.dead_letter_queue, resolvePhysicalQueue(topology, 'dead_letter'));
409
+ assert.equal(body.settings.max_retries, 3);
410
+ });