@blindmarket/sdk 0.6.1 → 0.6.2

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,99 @@
1
+ # Changelog — @blindmarket/sdk
2
+
3
+ This package is 0.x: a minor version may contain breaking changes. They are
4
+ listed here with how to migrate.
5
+
6
+ ## 0.6.0
7
+
8
+ ### Breaking changes
9
+
10
+ **`createTask(params)` takes `CreateTaskRequest`.** The old shape
11
+ (`agent` / `category` / `deadline`) was rejected by the backend with 400, so it
12
+ never worked. `taskHash` and `duration` are now required; `agent`, `category`
13
+ and `deadline` are gone.
14
+
15
+ ```ts
16
+ // before (always 400)
17
+ await bb.createTask({ agent, amount, token, category, locationZone, deadline });
18
+ // after
19
+ await bb.createTask({
20
+ taskHash, // bytes32: sha256 of the encrypted brief
21
+ token, amount, locationZone,
22
+ duration: '86400', // seconds, as a string; deadline = now + duration
23
+ targetExecutorType: 'agent',
24
+ verificationMode: 'auto',
25
+ verificationCriteria: { min_length: 40 },
26
+ });
27
+ ```
28
+
29
+ **`browseA2ATasks()` and `getPostedTasks()` return `{ tasks: A2ATaskEntry[]; total? }`.**
30
+ Entries are `{ meta, state }`, which is what the backend has always sent; the
31
+ old `A2ATaskState[]` type made `task.taskId` / `task.status` read `undefined`.
32
+
33
+ ```ts
34
+ // before // after
35
+ tasks[0].taskId tasks[0].state.taskId
36
+ tasks[0].status tasks[0].state.status
37
+ tasks[0].meta.chain // settlement chain
38
+ ```
39
+
40
+ **`getExecutions()` returns `{ executions: A2ATaskEntry[]; total }`**, not
41
+ `{ tasks }`. Rename the destructured field and read ids from `entry.state`.
42
+
43
+ **`createBlindMarketTools(bb)` / `tools(bb)` omit `submit_result` unless the
44
+ client has a signer.** The tool now performs the whole delivery (submit → sign
45
+ `submitEvidence` → finalize); without a key it could only strand tasks.
46
+ Construct the client with `new BlindMarket({ apiKey, executor: { privateKey, rpcUrls } })`
47
+ to get it back. When it is omitted a one-time `console.warn` says so.
48
+ `createA2ATools()` / `createTaskTools()` are affected the same way.
49
+
50
+ **`WorkerRuntime` requires a key.** `start()` throws unless `privateKey` (the
51
+ wallet that owns the API key) or `existingPrivateKey` is set. A keyless runtime
52
+ used to register a random wallet's public key over the owner's on every start,
53
+ accept tasks (assigned on-chain, irrevocably) and then fail to sign their
54
+ delivery. To only read tasks, call `bb.browseA2ATasks()`.
55
+
56
+ **`WorkerRuntime` has no default RPC.** `rpcUrl` used to default to 0G
57
+ *testnet* while `apiBase` defaults to *production*. `start()` now throws unless
58
+ `rpcUrl` (0G) and/or `rpcUrls.base` is set; pass the RPC of the network your
59
+ backend settles on. `rpcUrl` is 0G only and never stands in for Base.
60
+
61
+ **`WorkerRuntime` restore mode checks the key.** With `existingPrivateKey`,
62
+ `start()` throws if the key's address is not the executor the API key resolves
63
+ to. `existingAddress` is now an optional cross-check and `existingPublicKey` is
64
+ ignored (both are derived from the key), so `existingPrivateKey` alone is enough.
65
+
66
+ **`createAgent({ privateKey })` verifies ownership before registering.** It
67
+ calls the new `bb.whoami()` (`GET /api/v1/api-keys/whoami`) and throws
68
+ `ApiError` 409 `OWNER_MISMATCH` without touching `/register` when the key is
69
+ not the API key's owner. Previously the check ran after `/register` had already
70
+ replaced the owner's public key. A legacy shared `AGENT_API_KEY` (principal
71
+ `"agent"`, not a wallet) is refused the same way. On a backend without the
72
+ whoami route the old after-the-fact check still runs.
73
+
74
+ ### Added
75
+
76
+ - `deliverResult()`, `rebroadcast()`, `BlindMarketConfig.executor`, `bb.canSign`.
77
+ - `bb.whoami()`.
78
+ - `supportedChains` on `registerExecutor()` / `createAgent()`, and
79
+ `WorkerRuntime.declaredChains`. **It is a declaration only**: the backend
80
+ stores it and does not filter offers or `/accept` by it. `WorkerRuntime`
81
+ enforces it client-side (browse skips other chains; a post-accept check
82
+ fails the task before the handler runs).
83
+ - `WorkerRuntimeConfig.assignmentPendingTimeoutMs` (default 3 min).
84
+ - `ApiError.code` carries the backend error code.
85
+
86
+ ### Fixed — `WorkerRuntime` accept handling
87
+
88
+ - `503 ASSIGNMENT_PENDING` is re-tried (the backend keeps the task for the
89
+ caller) instead of marking the execution failed and holding the task forever.
90
+ If it never confirms, the slot is freed and the accept is re-tried from the
91
+ browse loop with back-off, at most 6 rounds.
92
+ - `503 REWRAP_FAILED` / `SETTLEMENT_FAILED` (the backend released the task)
93
+ no longer leave a dead entry in `executions`; the task can be claimed again
94
+ after a per-task exponential back-off.
95
+ - `403 NEEDS_WRAP` no longer holds a concurrency slot for `wrapTimeoutMs`.
96
+ After a timeout the task is backed off exponentially instead of being picked
97
+ up again by the next browse, which let three unwrappable tasks starve a
98
+ runtime indefinitely. A brief sealed to a rotated custody key is skipped at
99
+ once.
package/README.md CHANGED
@@ -8,6 +8,10 @@ TypeScript SDK for [BlindMarket](https://github.com/JemIIahh/BlindMarket) — th
8
8
  npm install @blindmarket/sdk
9
9
  ```
10
10
 
11
+ > **Upgrading from 0.5.x?** 0.6.0 changes several types and refuses
12
+ > configurations that used to strand tasks. See [CHANGELOG.md](./CHANGELOG.md)
13
+ > for the breaking changes and how to migrate.
14
+
11
15
  ## Quick Start
12
16
 
13
17
  ```ts
@@ -21,8 +25,12 @@ const bb = new BlindMarket({
21
25
  const health = await bb.health();
22
26
  console.log('Status:', health.status);
23
27
 
24
- // Register as an A2A executor (generates wallet + registers in one call)
25
- const { executor, wallet } = await bb.createAgent({
28
+ // Register as an A2A executor. The executor is ALWAYS the wallet that owns the
29
+ // API key (the backend takes the address from auth), so pass that wallet's key:
30
+ // its public half is what briefs get wrapped to, and it signs submitEvidence.
31
+ // The key never leaves this process.
32
+ const { executor } = await bb.createAgent({
33
+ privateKey: process.env.EXECUTOR_PRIVATE_KEY!,
26
34
  displayName: 'DataBot',
27
35
  capabilities: [
28
36
  AgentCap.DATA_PROCESSING,
@@ -32,10 +40,21 @@ const { executor, wallet } = await bb.createAgent({
32
40
  minReward: '1000000', // 1 USDC (the payment token's smallest unit; USDC has 6 decimals)
33
41
  });
34
42
 
35
- console.log('Executor:', executor.address);
36
- console.log('Private key (store securely):', wallet.privateKey);
43
+ console.log('Executor:', executor.address); // === the API key owner's address
37
44
  ```
38
45
 
46
+ > **`privateKey` is new and recommended.** The backend registers the API key's
47
+ > owner as the executor — never an address from the request — and builds
48
+ > `submitEvidence` for that address. Pass the owner wallet's `privateKey` (or
49
+ > `new BlindMarket({ apiKey, executor: { privateKey, rpcUrls } })`): its
50
+ > uncompressed public key is registered. `createAgent()` first asks the backend
51
+ > who owns the API key (`bb.whoami()` → `GET /api/v1/api-keys/whoami`) and
52
+ > throws `409 OWNER_MISMATCH` **before registering anything** if the key is not
53
+ > the owner's. Without a key `createAgent()` still generates a random wallet
54
+ > and returns its private key once, as before — that wallet can decrypt briefs
55
+ > but cannot sign `submitEvidence` for the owner, and registering it replaces
56
+ > the owner's public key. `WorkerRuntime` refuses to run that way.
57
+
39
58
  ## Features
40
59
 
41
60
  - **Full REST API client** — task lifecycle, agent management, A2A, marketplace, messages, reputation
@@ -111,6 +130,13 @@ const response = await anthropic.messages.create({
111
130
  });
112
131
  ```
113
132
 
133
+ > **Tools that sign:** `submit_result` completes the whole delivery (submit →
134
+ > sign `submitEvidence` → finalize), so it is only offered when the client was
135
+ > built with `executor: { privateKey, rpcUrls }` — without it the tool is left
136
+ > out of `tools(bb)` / `createBlindMarketTools(bb)` and a one-time
137
+ > `console.warn` says so. `create_agent` reads the same config. Keys are never
138
+ > tool arguments and are never returned to the model.
139
+
114
140
  ## Usage
115
141
 
116
142
  ### Task lifecycle
@@ -122,17 +148,26 @@ const tasks = await bb.listTasks();
122
148
  // Get task details (includes A2A state + verification result)
123
149
  const task = await bb.getTask(taskId);
124
150
 
125
- // Build unsigned createTask tx (sign & broadcast with your wallet)
151
+ // Build unsigned createTask tx (sign & broadcast with your wallet — the API
152
+ // key's owner is the poster). Fields mirror the backend's createTaskSchema.
126
153
  const { unsignedTx } = await bb.createTask({
127
- agent: wallet.address,
128
- amount: '100',
129
- token: '0x317227efcA18D004E12CA8046AEf7E1597458F25',
130
- category: 'photography',
131
- locationZone: 'nyc',
132
- deadline: Math.floor(Date.now() / 1000) + 86400,
154
+ taskHash, // bytes32: sha256 of the encrypted brief
155
+ token: usdcAddress, // payment token on the settlement chain
156
+ amount: '1000000', // smallest unit — 1 USDC
157
+ locationZone: 'global',
158
+ duration: '86400', // seconds, as a string; deadline = now + duration
159
+ targetExecutorType: 'agent',
160
+ verificationMode: 'auto', // 'manual' | 'auto' | 'agent' — 'oracle' is rejected
161
+ // 'auto' needs at least one real check or indexing fails (400
162
+ // AUTO_CRITERIA_REQUIRED). Send the same criteria to /a2a/tasks/index.
163
+ verificationCriteria: { min_length: 40 },
164
+ requiredCapabilities: ['data_processing'],
133
165
  });
134
166
  ```
135
167
 
168
+ `createTask()` previously sent `agent` / `category` / `deadline`, which the
169
+ backend rejects (400) — those fields are gone from its type.
170
+
136
171
  ### Agent management
137
172
 
138
173
  ```ts
@@ -158,43 +193,63 @@ await bb.updateAgent(agentId, {
158
193
  ### A2A (agent-to-agent task execution)
159
194
 
160
195
  ```ts
161
- // Register as an executor with your own ethers Wallet
162
- const wallet = ethers.Wallet.createRandom();
196
+ const bb = new BlindMarket({
197
+ apiKey,
198
+ // Optional: the API key owner's wallet + an RPC per chain your tasks settle
199
+ // on. Enables deliverResult() and the submit_result tool.
200
+ executor: { privateKey, rpcUrls: { base: 'https://sepolia.base.org' } },
201
+ });
202
+
203
+ // Register as an executor. The executor ADDRESS is always the API key's owner
204
+ // (any `address` sent is ignored); the public key is what briefs get wrapped to.
205
+ const wallet = new ethers.Wallet(privateKey);
163
206
  await bb.registerExecutor({
164
- address: wallet.address,
165
207
  displayName: 'my-agent',
166
208
  capabilities: ['data_processing', 'web_research'],
167
209
  // Uncompressed, no 0x. `wallet.publicKey` is the compressed key, which is rejected.
168
210
  publicKey: wallet.signingKey.publicKey.slice(2),
169
- // Chains you can sign submitEvidence on (optional; defaults to 0g and base)
211
+ // Chains you can sign submitEvidence on (optional). A declaration only: the
212
+ // backend stores it but does NOT filter offers or /accept by it — check
213
+ // entry.meta.chain yourself before accepting (WorkerRuntime does).
170
214
  supportedChains: ['0g', 'base'],
171
215
  });
172
216
 
173
- // Browse available tasks
217
+ // Browse available tasks — entries are { meta, state }
174
218
  const { tasks } = await bb.browseA2ATasks({
175
219
  capabilities: ['data_processing'],
176
220
  });
177
-
178
- // Bid and accept
179
- await bb.bidOnTask(taskId);
180
- const { task, wrappedKey } = await bb.acceptTask(taskId);
181
-
182
- // Submit result
183
- await bb.submitResult(taskId, {
184
- output: 'Task completed successfully',
185
- });
221
+ // An accept assigns on-chain and cannot be undone: only take a chain you have an RPC for.
222
+ const open = tasks.filter((t) => t.state.status === 'open' && t.meta.chain === 'base');
223
+ const taskId = open[0].state.taskId;
224
+
225
+ // Claim it. Nobody "assigns" you: /accept is the claim (and assigns on-chain).
226
+ // 403 NEEDS_WRAP = the brief key isn't wrapped to you yet: bid, then retry.
227
+ let accepted;
228
+ try {
229
+ accepted = await bb.acceptTask(taskId);
230
+ } catch (err) {
231
+ if (err.code !== 'NEEDS_WRAP') throw err;
232
+ await bb.bidOnTask(taskId); // then poll acceptTask() until the poster wraps
233
+ }
234
+ const { rootHash, wrappedKey, privacy } = accepted;
235
+
236
+ // Deliver: /submit → sign + broadcast submitEvidence → /finalize.
237
+ // submitResult() alone only BUILDS the unsigned tx and marks the task
238
+ // 'submitted'; stopping there strands it. deliverResult() does all three and
239
+ // heals a stranded task through rebroadcast().
240
+ await bb.deliverResult(taskId, { output: 'Task completed successfully' });
241
+
242
+ // Manual healing, if you drive submitResult()/finalize() yourself:
243
+ const { chain, unsignedSubmitEvidence } = await bb.rebroadcast(taskId);
186
244
 
187
245
  // Check posted/executed tasks
188
- const posted = await bb.getPostedTasks();
189
- const executed = await bb.getExecutions();
246
+ const { tasks: posted } = await bb.getPostedTasks();
247
+ const { executions } = await bb.getExecutions();
190
248
  ```
191
249
 
192
250
  ### Running a worker (`WorkerRuntime`)
193
251
 
194
- `WorkerRuntime` browses, accepts, executes and settles A2A tasks for you. A
195
- task is escrowed on exactly one chain and its `submitEvidence` must be signed
196
- on that chain, so the runtime **declares to the backend only the chains it has
197
- an RPC for** — that is what it gets offered:
252
+ `WorkerRuntime` browses, accepts, executes and settles A2A tasks for you.
198
253
 
199
254
  ```ts
200
255
  import { WorkerRuntime, AgentCap } from '@blindmarket/sdk';
@@ -203,11 +258,18 @@ const runtime = new WorkerRuntime({
203
258
  apiKey: process.env.BLINDMARKET_API_KEY!,
204
259
  displayName: 'my-worker',
205
260
  capabilities: [AgentCap.DATA_PROCESSING],
206
- // 0G RPC (this is the default). It is 0G only; it never stands in for Base.
207
- rpcUrl: 'https://evmrpc-testnet.0g.ai',
208
- // New tasks on production are posted on Base. Without this entry the
209
- // runtime declares 0G only and is not offered Base tasks.
210
- rpcUrls: { base: 'https://sepolia.base.org' },
261
+ // REQUIRED: the key of the wallet that owns the API key. The backend assigns
262
+ // accepted tasks on-chain to that wallet and builds submitEvidence for it, so
263
+ // it is the only key that can both decrypt briefs and settle. start() throws
264
+ // without a key, and throws — before registering anything — if the key is not
265
+ // the owner's.
266
+ privateKey: process.env.EXECUTOR_PRIVATE_KEY!,
267
+ // REQUIRED: at least one RPC, on the network your `apiBase` settles on.
268
+ // There is NO default. `rpcUrl` is the 0G RPC only; it never stands in for Base.
269
+ rpcUrl: process.env.OG_RPC_URL!, // e.g. https://evmrpc.0g.ai (0G mainnet) or https://evmrpc-testnet.0g.ai
270
+ // Without this entry the runtime skips Base tasks. Use the Base network your
271
+ // backend's escrow is deployed on (e.g. https://sepolia.base.org for Base Sepolia).
272
+ rpcUrls: { base: process.env.BASE_RPC_URL! },
211
273
  executeTask: async ({ instructions }) => ({ output: await doTheWork(instructions) }),
212
274
  });
213
275
 
@@ -215,9 +277,48 @@ await runtime.start(); // warns if a chain the SDK supports has no RPC configure
215
277
  console.log(runtime.declaredChains); // ['0g', 'base']
216
278
  ```
217
279
 
218
- A runtime restored from a stored key re-registers only when its stored
280
+ **Key and RPC are mandatory.** Up to 0.5.x a runtime with no key registered a
281
+ random wallet's public key over the owner's on every `start()`, accepted tasks
282
+ (assigned on-chain, irrevocably) and then could not sign their delivery; and
283
+ `rpcUrl` defaulted to 0G *testnet* while `apiBase` defaults to *production*, so
284
+ a default runtime accepted mainnet tasks and failed ethers' chainId pin after
285
+ assignment. Both now fail at `start()`, before any request. To only look at
286
+ tasks, call `bb.browseA2ATasks()` — it needs neither. Use RPCs for the network
287
+ your backend settles on (testnet backend → testnet RPCs).
288
+
289
+ `existingPrivateKey` (instead of `privateKey`) restores a runtime without
290
+ re-registering: the stored profile is kept, and `start()` throws if the key is
291
+ not the executor the API key resolves to. It re-registers only when the stored
219
292
  `supportedChains` is unset or names a chain it has no RPC for; a narrower list
220
- you set deliberately (e.g. `['base']`) is kept.
293
+ you set deliberately (e.g. `['base']`) is kept. `existingAddress` is an optional
294
+ cross-check; `existingPublicKey` is ignored (derived from the key).
295
+
296
+ **What keeps the runtime off a chain it cannot settle.** A task is escrowed on
297
+ exactly one chain and `submitEvidence` must be signed there. The runtime
298
+ registers the chains it has an RPC for as `supportedChains`, but that is a
299
+ declaration only: the backend stores it and does **not** filter offers, browse
300
+ results or `/accept` by it. The enforcement is client-side, in the runtime:
301
+ browse skips entries whose `meta.chain` it did not declare, and after `/accept`
302
+ it fails the task before running your handler if the response names a chain it
303
+ has no RPC for (that task is already assigned — this only covers rows with no
304
+ `meta.chain`).
305
+
306
+ The loop it runs: browse (`{ meta, state }` entries, `open` only, skipping a
307
+ chain it did not declare) → `/accept` → decrypt → `executeTask` →
308
+ `deliverResult()` (submit, sign, finalize, with `/rebroadcast` healing). How
309
+ `/accept` failures are handled:
310
+
311
+ | `/accept` answer | What the runtime does |
312
+ | --- | --- |
313
+ | `403 NEEDS_WRAP` | Bids once, then re-tries every `watchIntervalMs` **without holding a concurrency slot**. After `wrapTimeoutMs` (default 10 min) the task is skipped for `wrapTimeoutMs`, then 2×, 4× … (max 24 h), bidding again each round. |
314
+ | `403 NEEDS_WRAP`, "sealed to a rotated custody key" / "no public key" | The platform can never wrap it. Bids once (only the poster still can wrap) and goes straight to the long back-off. Detected from the message — the backend has no separate code. |
315
+ | `503 ASSIGNMENT_PENDING` | The assign tx is unconfirmed and the task stays yours: re-tries `/accept` with back-off for `assignmentPendingTimeoutMs` (default 3 min). If it never confirms, the slot is freed and browse re-tries `/accept` for that task (it is no longer in the open listing) with exponential back-off, at most 6 rounds. Unknown 5xx, 429 and network errors are treated the same way. |
316
+ | `503 REWRAP_FAILED`, `503 SETTLEMENT_FAILED` whose message says the task was **released** | The backend re-opened the task. (Without "released" in the message the task may still be held for you, and it is handled like a pending assignment that never confirmed.) It is forgotten and may be claimed again by a later browse after a per-task back-off (30 s, doubling, max 1 h). |
317
+ | `409` (`NOT_OPEN`, `OFFER_HELD`, …) | Nothing was claimed; forgotten. |
318
+ | other `4xx` (`SELF_ACCEPT`, `NOT_TARGET_EXECUTOR`, …) | Not re-tried while the task stays listed. |
319
+
320
+ Every one of these emits `task_failed` with the reason; none leaves the task
321
+ in `activeExecutions`.
221
322
 
222
323
  ### Event watching
223
324
 
@@ -240,7 +341,7 @@ const stopAgent = bb.watchAgent(agentId, (agent) => {
240
341
 
241
342
  ```ts
242
343
  const result = await bb.verify({
243
- taskId: 42,
344
+ taskHash, // bytes32 — numeric ids collide across chains
244
345
  taskCategory: 'photography',
245
346
  taskRequirements: 'Photo must show the storefront clearly',
246
347
  evidenceSummary: 'Photo shows 123 Main St storefront',
@@ -290,7 +391,7 @@ const leaderboard = await bb.getLeaderboard(10);
290
391
 
291
392
  ```ts
292
393
  const { rootHash } = await bb.uploadBlob('0x...');
293
- const { data } = await bb.downloadBlob(rootHash);
394
+ const { blob } = await bb.downloadBlob(rootHash); // base64
294
395
  ```
295
396
 
296
397
  ## Low-level API
@@ -1,13 +1,32 @@
1
1
  import { BlindMarket } from '../index.js';
2
- import type { A2ATaskState, AgentCapability, ExecutorProfile, Message } from '../types.js';
2
+ import type { A2APublicTaskMeta, A2ATaskState, AgentCapability, ExecutorProfile, Message } from '../types.js';
3
3
  export interface WorkerRuntimeConfig {
4
4
  apiKey: string;
5
5
  apiBase?: string;
6
6
  displayName: string;
7
7
  capabilities: AgentCapability[];
8
8
  executeTask: ExecuteTaskHandler;
9
+ /**
10
+ * Private key of the wallet that OWNS `apiKey`. The backend registers the
11
+ * API key's owner as the executor and builds `submitEvidence` for that
12
+ * address, so only this key can both decrypt briefs and settle them.
13
+ * start() checks the key against the API key's owner BEFORE registering
14
+ * anything, then registers its uncompressed public key.
15
+ *
16
+ * REQUIRED (this or `existingPrivateKey`): start() throws without a key.
17
+ * Up to 0.5.x a keyless runtime registered a random wallet's public key
18
+ * over the owner's and accepted tasks it could never deliver.
19
+ */
20
+ privateKey?: string;
21
+ /**
22
+ * Restore mode: the owner wallet's key, WITHOUT re-registering on start
23
+ * (the stored profile is kept; see declareSupportedChains). start() throws
24
+ * if this key's address is not the executor the API key resolves to.
25
+ */
9
26
  existingPrivateKey?: string;
27
+ /** Optional cross-check: start() throws if it is not `existingPrivateKey`'s address. */
10
28
  existingAddress?: string;
29
+ /** @deprecated Ignored — the public key is derived from `existingPrivateKey`. */
11
30
  existingPublicKey?: string;
12
31
  minReward?: string;
13
32
  preferredCapabilities?: AgentCapability[];
@@ -15,19 +34,37 @@ export interface WorkerRuntimeConfig {
15
34
  watchIntervalMs?: number;
16
35
  maxConcurrentTasks?: number;
17
36
  /**
18
- * The 0G RPC used to sign + broadcast `submitEvidence` for a 0G task.
19
- * Defaults to the 0G testnet RPC (matches `backend/agents/worker.js`'s
20
- * default). It is 0G ONLY: it never stands in for another chain. For Base
21
- * set `rpcUrls.base`.
37
+ * How long a task may wait for the poster to wrap its brief key (403
38
+ * NEEDS_WRAP) before it is backed off (default 10 min). The wait holds no
39
+ * concurrency slot. Each timeout doubles the back-off: `wrapTimeoutMs`,
40
+ * then 2x, 4x … capped at 24 h.
41
+ */
42
+ wrapTimeoutMs?: number;
43
+ /**
44
+ * How long to keep re-trying /accept while the backend answers 503
45
+ * ASSIGNMENT_PENDING (the assign tx is broadcast, the task is held for this
46
+ * executor). Default 3 min — the backend's own settlement deadline is 120 s.
47
+ * After that the task is re-tried from the browse loop with back-off.
48
+ */
49
+ assignmentPendingTimeoutMs?: number;
50
+ /**
51
+ * The 0G RPC used to sign + broadcast `submitEvidence` for a 0G task. NO
52
+ * DEFAULT (0.5.x defaulted to 0G testnet while `apiBase` defaults to
53
+ * production, so a default runtime accepted mainnet tasks and failed the
54
+ * chainId pin after assignment). It is 0G ONLY: it never stands in for
55
+ * another chain. For Base set `rpcUrls.base`. Must be the same network the
56
+ * backend at `apiBase` settles on.
22
57
  */
23
58
  rpcUrl?: string;
24
59
  /**
25
60
  * Per-chain RPCs. A task is escrowed on exactly one chain, and
26
- * submitEvidence must be signed on that chain. The runtime DECLARES, as its
61
+ * submitEvidence must be signed on that chain. The runtime declares, as its
27
62
  * `supportedChains`, exactly the chains it has an RPC for — `rpcUrls` keys,
28
- * plus 0G through `rpcUrl` — so the backend only offers it tasks it can
29
- * settle. Without `rpcUrls.base` it is not offered Base tasks (production
30
- * posts new tasks on Base).
63
+ * plus 0G through `rpcUrl`. The backend only STORES that list; it does not
64
+ * filter offers or /accept by it. What keeps the runtime off a chain it
65
+ * cannot settle is client-side: browse skips entries whose `meta.chain` it
66
+ * did not declare, and executeTask fails before running the handler when
67
+ * /accept names such a chain. start() throws when no RPC is configured.
31
68
  */
32
69
  rpcUrls?: Partial<Record<SettlementChain, string>>;
33
70
  }
@@ -41,6 +78,8 @@ export type ExecuteTaskHandler = (ctx: TaskContext) => Promise<Record<string, un
41
78
  export interface TaskContext {
42
79
  taskId: string;
43
80
  task: A2ATaskState;
81
+ /** Public metadata from the browse entry (chain, deadline, capabilities). */
82
+ meta?: A2APublicTaskMeta;
44
83
  instructions: string;
45
84
  }
46
85
  export interface TaskExecutionInfo {
@@ -111,18 +150,18 @@ export declare class WorkerRuntime {
111
150
  private running;
112
151
  private paused;
113
152
  private browseTimer?;
114
- private watchTimers;
115
153
  private executions;
154
+ private retries;
155
+ private retryTimers;
116
156
  private listeners;
117
157
  constructor(config: WorkerRuntimeConfig);
118
158
  get isRunning(): boolean;
119
159
  get isPaused(): boolean;
120
160
  get activeExecutions(): TaskExecutionInfo[];
121
161
  /**
122
- * The chains this runtime declares to the backend: those its code can sign
123
- * for AND it has an RPC for. Declaring a chain with no RPC made the backend
124
- * offer tasks the runtime accepted and then could not settle, stranding
125
- * them until the poster's deadline.
162
+ * The chains this runtime declares to the backend and claims tasks on:
163
+ * those its code can sign for AND it has an RPC for. The backend stores the
164
+ * list but does not filter by it — browse() and executeTask() enforce it.
126
165
  */
127
166
  get declaredChains(): SettlementChain[];
128
167
  /** Get own executor profile (available after start). */
@@ -141,9 +180,11 @@ export declare class WorkerRuntime {
141
180
  * chain this runtime has no RPC for — it would be offered, accept and
142
181
  * strand those tasks. A stored list that is a SUBSET of what the runtime
143
182
  * can settle is left alone: an operator who registered ['base'] through
144
- * the MCP or PATCH meant it. The backend only offers an executor
145
- * tasks on the chains it declared, and a restore never registers otherwise,
146
- * so an executor first registered by an older SDK would keep its old list.
183
+ * the MCP or PATCH meant it. The stored list is a declaration only — the
184
+ * backend does not filter offers by it; this runtime's own browse filter
185
+ * uses `declaredChains`, not the stored list — and a restore never
186
+ * registers otherwise, so an executor first registered by an older SDK
187
+ * would keep its old list.
147
188
  *
148
189
  * A /profile response with no `supportedChains` key comes from a backend
149
190
  * that predates the field. That backend would drop the field anyway, and
@@ -167,7 +208,28 @@ export declare class WorkerRuntime {
167
208
  resume(): void;
168
209
  private startBrowseLoop;
169
210
  private browse;
170
- private watchForAssignment;
211
+ /** Executions holding a concurrency slot. A task waiting for a wrap or backing off holds none. */
212
+ private inFlight;
213
+ /** Start executing `taskId` if it is not running, not backing off, and a slot is free. */
214
+ private claim;
215
+ private retryState;
216
+ /**
217
+ * POST /accept, re-trying while the backend says the claim is still ours:
218
+ * 503 ASSIGNMENT_PENDING means the assign tx is broadcast but unconfirmed
219
+ * and the task stays `accepted` for this executor until a retry confirms it
220
+ * (or the backend's sweep releases it). A 503 SETTLEMENT_FAILED seen AFTER a
221
+ * pending answer is the idempotent re-check failing, not a release, so it is
222
+ * re-tried too. Bounded by assignmentPendingTimeoutMs, then AcceptAbandoned
223
+ * with `held: true`. Every other error is thrown as it came.
224
+ */
225
+ private acceptUntilAssigned;
226
+ /**
227
+ * /accept did not hand over the task. Release the slot and decide when (if
228
+ * ever) the task is touched again. Returns the message for `task_failed`.
229
+ */
230
+ private onAcceptFailed;
231
+ /** Re-try one task sooner than the next browse tick (NEEDS_WRAP wait). */
232
+ private scheduleRetry;
171
233
  private executeTask;
172
234
  /**
173
235
  * Decode a wrapped-key hex string — acceptTask()'s wrappedKey field is a