@oneshot-agent/sdk 0.54.0 → 0.56.0

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/dist/index.js CHANGED
@@ -38,6 +38,8 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.ReadOnlyWalletProvider = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
40
40
  const hunts_1 = require("./hunts");
41
+ __exportStar(require("./solve"), exports);
42
+ __exportStar(require("./predict"), exports);
41
43
  __exportStar(require("./hunts"), exports);
42
44
  const physical_mail_1 = require("./physical-mail");
43
45
  __exportStar(require("./physical-mail"), exports);
@@ -65,11 +67,36 @@ const approvals_1 = require("./approvals");
65
67
  // stay off this public surface (see tests/unit/sdk-provider-cost-removed.test.ts).
66
68
  // Import from the `@oneshot-agent/sdk/receipt` subpath instead.
67
69
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
68
- const SDK_VERSION = '0.54.0';
70
+ const SDK_VERSION = '0.56.0';
69
71
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
70
72
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
71
73
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
72
74
  const HTTP_POLL_RELAXED_MS = 5000;
75
+ function isTransientComputeReadError(error) {
76
+ if (error instanceof errors_2.ToolError) {
77
+ return error.statusCode === 429 || (error.statusCode >= 500 && error.statusCode < 600);
78
+ }
79
+ // Fetch network failures differ across browsers, Node and Bun. Do not swallow
80
+ // arbitrary TypeErrors (bad URLs/configuration), JSON errors or signing errors.
81
+ if (!(error instanceof Error))
82
+ return false;
83
+ const seen = new Set();
84
+ let current = error;
85
+ while (current !== null && typeof current === 'object' && !seen.has(current)) {
86
+ seen.add(current);
87
+ const { code, cause } = current;
88
+ if ((code !== undefined && [
89
+ 'ECONNRESET', 'ECONNREFUSED', 'ECONNABORTED', 'EPIPE', 'ETIMEDOUT',
90
+ 'ENOTFOUND', 'EAI_AGAIN', 'ENETUNREACH', 'EHOSTUNREACH',
91
+ 'ConnectionRefused', 'ConnectionClosed', 'Timeout',
92
+ 'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT',
93
+ ].includes(code)) || (current instanceof TypeError &&
94
+ /^(fetch failed|Failed to fetch|Load failed|NetworkError when attempting to fetch resource\.)$/i.test(current.message)))
95
+ return true;
96
+ current = cause;
97
+ }
98
+ return false;
99
+ }
73
100
  const BASE_URL = 'https://win.oneshotagent.com';
74
101
  const RPC_URL = 'https://mainnet.base.org';
75
102
  const CHAIN_ID = 8453;
@@ -147,6 +174,10 @@ function validateBudgetConfig(budgets) {
147
174
  }
148
175
  /** The keys executeToolRequestImpl takes out of a request as SDK options, so they never reach the API body. */
149
176
  const PAID_ROUTE_OPTION_KEYS = ['timeout', 'totalTimeoutMs', 'onRequestCreated', 'onAccepted', 'signal', 'onStatusUpdate', 'wait', 'waitForPhones', 'phoneTimeoutSec', 'idempotencyKey', 'approvalId', 'maxCost'];
177
+ /** ETH mode is unsupported on CDP wallets (#2153): the swap path needs a confirmed receipt CDP never returns. */
178
+ function cdpEthModeError() {
179
+ return new errors_2.ValidationError("ETH currency mode is not supported with CDP wallets; fund the CDP wallet with USDC or use currency: 'USDC' (or a privateKey / custom walletProvider for ETH mode)", 'currency');
180
+ }
150
181
  class OneShot {
151
182
  /**
152
183
  * Async factory — required for CDP wallets (account creation is async).
@@ -174,6 +205,9 @@ class OneShot {
174
205
  return new OneShot(config, config.walletProvider);
175
206
  }
176
207
  if (config.cdp) {
208
+ // Reject before the CDP SDK is imported or an account is created (#2153).
209
+ if ((config.currency ?? 'USDC') === 'ETH')
210
+ throw cdpEthModeError();
177
211
  const { CdpWalletProvider } = await Promise.resolve().then(() => __importStar(require('./providers/cdp')));
178
212
  const cdpOpts = typeof config.cdp === 'object' ? config.cdp : undefined;
179
213
  const walletProvider = await CdpWalletProvider.create(cdpOpts);
@@ -193,7 +227,7 @@ class OneShot {
193
227
  constructor(config, walletProvider) {
194
228
  this.hunts = new hunts_1.Hunts(this);
195
229
  this.physicalMail = new physical_mail_1.PhysicalMail(async (path, method, body, mime) => {
196
- const response = await fetch(`${this.baseUrl}/v1/tools/physical-mail${path}`, {
230
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/physical-mail${path}`, {
197
231
  method, headers: { ...await this.signedReadHeaders(method === 'GET' ? 'read' : 'write'), 'Content-Type': mime ?? 'application/json' },
198
232
  ...(body === undefined ? {} : { body: mime ? body : JSON.stringify(body) }),
199
233
  signal: AbortSignal.timeout(60000),
@@ -207,6 +241,7 @@ class OneShot {
207
241
  /** Which paid calls run, are denied, or wait for approval. Wallet sessions write it. */
208
242
  this.policy = new approvals_1.ActionPolicies((path, method, body, scope) => this.agentJson(path, method, body, scope));
209
243
  this.readProofWarned = false;
244
+ this.proofClockOffsetSec = 0;
210
245
  /** ETH mode: signed payments not yet observed as settled, keyed by reservation id. */
211
246
  this._usdcPending = new Map();
212
247
  this._usdcReservationSeq = 0;
@@ -262,6 +297,11 @@ class OneShot {
262
297
  else {
263
298
  throw new errors_2.ValidationError('Provide privateKey or use OneShot.create() for CDP/custom wallets', 'config');
264
299
  }
300
+ // CDP wallets cannot run ETH mode even though CdpWalletProvider exposes
301
+ // sendTransaction: its wait() never observes a receipt, so the swap path
302
+ // cannot confirm the ETH→USDC trade (#2153).
303
+ if (config.cdp && this._currency === 'ETH')
304
+ throw cdpEthModeError();
265
305
  if (this._currency === 'ETH' && !this.provider.sendTransaction) {
266
306
  throw new errors_2.ValidationError('ETH currency mode requires a wallet provider that supports sendTransaction', 'currency');
267
307
  }
@@ -421,35 +461,35 @@ class OneShot {
421
461
  }
422
462
  /** List the caller's domain pool with warmup and rotation metadata. */
423
463
  async listDomains() {
424
- const response = await fetch(`${this.baseUrl}/v1/tools/email/domains`, {
464
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/email/domains`, {
425
465
  headers: await this.signedReadHeaders(),
426
466
  });
427
467
  if (!response.ok) {
428
- throw new errors_2.ToolError('Failed to list domains', response.status, await response.text());
468
+ throw new errors_2.ToolError('Failed to list domains', response.status, await response.text(), response.headers.get('retry-after'));
429
469
  }
430
470
  return response.json();
431
471
  }
432
472
  /** Take a domain out of rotation without releasing it. */
433
473
  async pauseDomain(domain) {
434
474
  this.validate(domain, 'domain');
435
- const response = await fetch(`${this.baseUrl}/v1/tools/email/domains/${encodeURIComponent(domain)}/pause`, {
475
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/email/domains/${encodeURIComponent(domain)}/pause`, {
436
476
  method: 'POST',
437
477
  headers: await this.signedReadHeaders('write'),
438
478
  });
439
479
  if (!response.ok) {
440
- throw new errors_2.ToolError('Failed to pause domain', response.status, await response.text());
480
+ throw new errors_2.ToolError('Failed to pause domain', response.status, await response.text(), response.headers.get('retry-after'));
441
481
  }
442
482
  return response.json();
443
483
  }
444
484
  /** Put a paused domain back into rotation. */
445
485
  async resumeDomain(domain) {
446
486
  this.validate(domain, 'domain');
447
- const response = await fetch(`${this.baseUrl}/v1/tools/email/domains/${encodeURIComponent(domain)}/resume`, {
487
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/email/domains/${encodeURIComponent(domain)}/resume`, {
448
488
  method: 'POST',
449
489
  headers: await this.signedReadHeaders('write'),
450
490
  });
451
491
  if (!response.ok) {
452
- throw new errors_2.ToolError('Failed to resume domain', response.status, await response.text());
492
+ throw new errors_2.ToolError('Failed to resume domain', response.status, await response.text(), response.headers.get('retry-after'));
453
493
  }
454
494
  return response.json();
455
495
  }
@@ -457,14 +497,33 @@ class OneShot {
457
497
  this.validate(options.topic, 'topic');
458
498
  return this.tool('research', { ...options });
459
499
  }
500
+ async predict(input) {
501
+ const { memo, valueTag, decisionContext, ...options } = input;
502
+ return this.tool('predict', { ...options, timeout: options.timeout ?? 240 });
503
+ }
504
+ /**
505
+ * Solve a routing or packing problem with constraint checks and conflicts. Quotes, pays and waits for
506
+ * the job. The result's `solution` is typed by the request's `problem`.
507
+ */
508
+ async solve(input) {
509
+ // Shared option objects and JavaScript callers may still supply receipt metadata.
510
+ // Solve's strict request schema does not accept it, on either quote or paid requests.
511
+ const { memo, valueTag, decisionContext, ...options } = input;
512
+ return this.tool('solve', { ...options, timeout: options.timeout ?? 420 });
513
+ }
460
514
  async personFind(options) {
461
515
  const { includePhone, ...payload } = options;
462
516
  return this.tool('find/person', { ...payload, include_phone: includePhone, limit: options.limit ?? 100 });
463
517
  }
464
- async personProfile(options) {
465
- if (!options.linkedin_url && !options.email && !options.name) {
466
- throw new errors_2.ValidationError('At least one of linkedin_url, email, or name is required', 'identifier');
518
+ /** At least one identity (#1899): name, email, phone or a profile URL under any of its names. */
519
+ requirePersonIdentity(options) {
520
+ const hasName = options.name || options.full_name || (options.first_name && options.last_name);
521
+ if (!hasName && !options.email && !options.phone && !options.profile_url && !options.linkedin_url && !options.social_media_url) {
522
+ throw new errors_2.ValidationError('At least one of name, email, phone, or profile_url is required', 'identifier');
467
523
  }
524
+ }
525
+ async personProfile(options) {
526
+ this.requirePersonIdentity(options);
468
527
  const { includePhone, ...payload } = options;
469
528
  return this.tool('lookup/person-profile', { ...payload, include_phone: includePhone });
470
529
  }
@@ -524,10 +583,7 @@ class OneShot {
524
583
  });
525
584
  }
526
585
  async personEmail(options) {
527
- this.validate(options.company_domain, 'company_domain');
528
- if (!options.full_name && !(options.first_name && options.last_name)) {
529
- throw new errors_2.ValidationError('Either full_name or both first_name and last_name required', 'name');
530
- }
586
+ this.requirePersonIdentity(options);
531
587
  return this.tool('lookup/person-email', { ...options });
532
588
  }
533
589
  async personEmailStatus(options) {
@@ -535,17 +591,13 @@ class OneShot {
535
591
  return this.tool('lookup/person-email-status', { ...options });
536
592
  }
537
593
  async personDossier(options) {
538
- if (!options.email && !options.social_media_url && !options.name) {
539
- throw new errors_2.ValidationError('At least one of email, social_media_url, or name is required', 'identifier');
540
- }
594
+ this.requirePersonIdentity(options);
541
595
  const { includePhone, ...payload } = options;
542
596
  // The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run (#1913).
543
597
  return this.tool('lookup/person-dossier', { ...payload, include_phone: includePhone, timeout: options.timeout ?? 300 });
544
598
  }
545
599
  async personSocialAccounts(options) {
546
- if (!options.email && !options.social_media_url) {
547
- throw new errors_2.ValidationError('At least one of email or social_media_url is required', 'identifier');
548
- }
600
+ this.requirePersonIdentity(options);
549
601
  return this.tool('lookup/person-social-accounts', { ...options });
550
602
  }
551
603
  async personArticles(options) {
@@ -554,17 +606,15 @@ class OneShot {
554
606
  return this.tool('lookup/person-articles', { ...options });
555
607
  }
556
608
  async personPosts(options) {
557
- this.validate(options.social_media_url, 'social_media_url');
609
+ this.requirePersonIdentity(options);
558
610
  return this.tool('lookup/person-posts', { ...options });
559
611
  }
560
612
  async personInterests(options) {
561
- if (!options.email && !options.phone && !options.social_media_url) {
562
- throw new errors_2.ValidationError('At least one of email, phone, or social_media_url is required', 'identifier');
563
- }
613
+ this.requirePersonIdentity(options);
564
614
  return this.tool('lookup/person-interests', { ...options });
565
615
  }
566
616
  async personFollowers(options) {
567
- this.validate(options.social_media_url, 'social_media_url');
617
+ this.requirePersonIdentity(options);
568
618
  return this.tool('lookup/person-followers', { ...options });
569
619
  }
570
620
  async inboxList(options = {}) {
@@ -573,24 +623,24 @@ class OneShot {
573
623
  limit: options.limit || undefined,
574
624
  include_body: options.include_body ? 'true' : undefined,
575
625
  });
576
- const response = await fetch(`${this.baseUrl}/v1/tools/inbox${qs ? `?${qs}` : ''}`, {
626
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/inbox${qs ? `?${qs}` : ''}`, {
577
627
  headers: await this.signedReadHeaders()
578
628
  });
579
629
  if (!response.ok) {
580
- throw new errors_2.ToolError('Failed to list inbox', response.status, await response.text());
630
+ throw new errors_2.ToolError('Failed to list inbox', response.status, await response.text(), response.headers.get('retry-after'));
581
631
  }
582
632
  return response.json();
583
633
  }
584
634
  async inboxGet(emailId) {
585
635
  this.validate(emailId, 'emailId');
586
- const response = await fetch(`${this.baseUrl}/v1/tools/inbox/${emailId}`, {
636
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/inbox/${emailId}`, {
587
637
  headers: await this.signedReadHeaders()
588
638
  });
589
639
  if (response.status === 404) {
590
640
  throw new errors_2.ToolError('Email not found', 404, 'Email not found');
591
641
  }
592
642
  if (!response.ok) {
593
- throw new errors_2.ToolError('Failed to get email', response.status, await response.text());
643
+ throw new errors_2.ToolError('Failed to get email', response.status, await response.text(), response.headers.get('retry-after'));
594
644
  }
595
645
  return response.json();
596
646
  }
@@ -935,14 +985,14 @@ class OneShot {
935
985
  this.validate(name, 'name');
936
986
  if (options.cookies !== undefined && options.storage_state !== undefined)
937
987
  throw new errors_2.ValidationError('Provide cookies or storage_state, not both', 'options');
938
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
988
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles`, {
939
989
  method: 'POST',
940
990
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
941
991
  body: JSON.stringify({ name, ...options }),
942
992
  signal: AbortSignal.timeout(150000),
943
993
  });
944
994
  if (!response.ok) {
945
- throw new errors_2.ToolError('Failed to create browser profile', response.status, await response.text());
995
+ throw new errors_2.ToolError('Failed to create browser profile', response.status, await response.text(), response.headers.get('retry-after'));
946
996
  }
947
997
  return response.json();
948
998
  }
@@ -971,12 +1021,12 @@ class OneShot {
971
1021
  }
972
1022
  async browserProfileSetup(profileId, action, body = {}) {
973
1023
  this.validate(profileId, 'profileId');
974
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles/${encodeURIComponent(profileId)}/setup/${action}`, {
1024
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles/${encodeURIComponent(profileId)}/setup/${action}`, {
975
1025
  method: 'POST', headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
976
1026
  body: JSON.stringify(body), signal: AbortSignal.timeout(150000),
977
1027
  });
978
1028
  if (!response.ok)
979
- throw new errors_2.ToolError('Browser profile setup failed', response.status, '');
1029
+ throw new errors_2.ToolError('Browser profile setup failed', response.status, '', response.headers.get('retry-after'));
980
1030
  return response.json();
981
1031
  }
982
1032
  /**
@@ -991,11 +1041,11 @@ class OneShot {
991
1041
  * ```
992
1042
  */
993
1043
  async listBrowserProfiles() {
994
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
1044
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles`, {
995
1045
  headers: await this.signedReadHeaders(),
996
1046
  });
997
1047
  if (!response.ok) {
998
- throw new errors_2.ToolError('Failed to list browser profiles', response.status, await response.text());
1048
+ throw new errors_2.ToolError('Failed to list browser profiles', response.status, await response.text(), response.headers.get('retry-after'));
999
1049
  }
1000
1050
  const data = await response.json();
1001
1051
  return data.profiles;
@@ -1010,12 +1060,12 @@ class OneShot {
1010
1060
  */
1011
1061
  async deleteBrowserProfile(profileId) {
1012
1062
  this.validate(profileId, 'profileId');
1013
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles/${profileId}`, {
1063
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles/${profileId}`, {
1014
1064
  method: 'DELETE',
1015
1065
  headers: await this.signedReadHeaders(),
1016
1066
  });
1017
1067
  if (!response.ok) {
1018
- throw new errors_2.ToolError('Failed to delete browser profile', response.status, await response.text());
1068
+ throw new errors_2.ToolError('Failed to delete browser profile', response.status, await response.text(), response.headers.get('retry-after'));
1019
1069
  }
1020
1070
  }
1021
1071
  async linkedinFree(method, path, opts) {
@@ -1023,7 +1073,7 @@ class OneShot {
1023
1073
  for (const [k, v] of Object.entries(opts.query ?? {}))
1024
1074
  if (v !== undefined)
1025
1075
  url.searchParams.set(k, v);
1026
- const response = await fetch(url.toString(), {
1076
+ const response = await this.fetchApi(url.toString(), {
1027
1077
  method,
1028
1078
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders(opts.scope ?? 'read')) },
1029
1079
  ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
@@ -1038,7 +1088,7 @@ class OneShot {
1038
1088
  catch { /* Non-JSON upstream error. */ }
1039
1089
  if (response.status === 409 && code === 'connect_required')
1040
1090
  throw new errors_1.LinkedInConnectRequiredError(body);
1041
- throw new errors_2.ToolError(`LinkedIn ${opts.what} failed`, response.status, body);
1091
+ throw new errors_2.ToolError(`LinkedIn ${opts.what} failed`, response.status, body, response.headers.get('retry-after'));
1042
1092
  }
1043
1093
  return response.json();
1044
1094
  }
@@ -1225,11 +1275,11 @@ class OneShot {
1225
1275
  limit: options.limit || undefined,
1226
1276
  from: options.from || undefined,
1227
1277
  });
1228
- const response = await fetch(`${this.baseUrl}/v1/tools/sms/inbox${qs ? `?${qs}` : ''}`, {
1278
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/sms/inbox${qs ? `?${qs}` : ''}`, {
1229
1279
  headers: await this.signedReadHeaders()
1230
1280
  });
1231
1281
  if (!response.ok) {
1232
- throw new errors_2.ToolError('Failed to list SMS inbox', response.status, await response.text());
1282
+ throw new errors_2.ToolError('Failed to list SMS inbox', response.status, await response.text(), response.headers.get('retry-after'));
1233
1283
  }
1234
1284
  return response.json();
1235
1285
  }
@@ -1371,7 +1421,7 @@ class OneShot {
1371
1421
  const scope = (0, deadline_1.deadlineScope)(toolOptions.signal, filled.startsWith('/v1/tools/browser/profiles') ? 150000 : 30000);
1372
1422
  try {
1373
1423
  const request = (async () => {
1374
- const response = await fetch(`${this.baseUrl}${target}`, {
1424
+ const response = await this.fetchApi(`${this.baseUrl}${target}`, {
1375
1425
  method,
1376
1426
  headers: { ...(sendsBody ? { 'Content-Type': 'application/json' } : {}), ...(await this.signedReadHeaders(method === 'GET' ? 'read' : 'write')) },
1377
1427
  ...(sendsBody ? { body: JSON.stringify(body ?? {}) } : {}),
@@ -1404,7 +1454,7 @@ class OneShot {
1404
1454
  }
1405
1455
  /** Signed free request returning the `data` field of `{ success, data }`. */
1406
1456
  async signedJson(method, path, body, scope, failure) {
1407
- const response = await fetch(`${this.baseUrl}${path}`, {
1457
+ const response = await this.fetchApi(`${this.baseUrl}${path}`, {
1408
1458
  method,
1409
1459
  headers: {
1410
1460
  ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
@@ -1413,7 +1463,7 @@ class OneShot {
1413
1463
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
1414
1464
  });
1415
1465
  if (!response.ok) {
1416
- throw new errors_2.ToolError(failure, response.status, await response.text());
1466
+ throw new errors_2.ToolError(failure, response.status, await response.text(), response.headers.get('retry-after'));
1417
1467
  }
1418
1468
  const json = await response.json();
1419
1469
  return (json.data ?? json);
@@ -1429,14 +1479,14 @@ class OneShot {
1429
1479
  */
1430
1480
  async smsInboxGet(messageId) {
1431
1481
  this.validate(messageId, 'messageId');
1432
- const response = await fetch(`${this.baseUrl}/v1/tools/sms/inbox/${messageId}`, {
1482
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/sms/inbox/${messageId}`, {
1433
1483
  headers: await this.signedReadHeaders()
1434
1484
  });
1435
1485
  if (response.status === 404) {
1436
1486
  throw new errors_2.ToolError('SMS message not found', 404, 'Message not found');
1437
1487
  }
1438
1488
  if (!response.ok) {
1439
- throw new errors_2.ToolError('Failed to get SMS message', response.status, await response.text());
1489
+ throw new errors_2.ToolError('Failed to get SMS message', response.status, await response.text(), response.headers.get('retry-after'));
1440
1490
  }
1441
1491
  return response.json();
1442
1492
  }
@@ -1457,11 +1507,11 @@ class OneShot {
1457
1507
  unread: options.unread ? 'true' : undefined,
1458
1508
  limit: options.limit || undefined,
1459
1509
  });
1460
- const response = await fetch(`${this.baseUrl}/v1/tools/notifications${qs ? `?${qs}` : ''}`, {
1510
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/notifications${qs ? `?${qs}` : ''}`, {
1461
1511
  headers: await this.signedReadHeaders()
1462
1512
  });
1463
1513
  if (!response.ok) {
1464
- throw new errors_2.ToolError('Failed to list notifications', response.status, await response.text());
1514
+ throw new errors_2.ToolError('Failed to list notifications', response.status, await response.text(), response.headers.get('retry-after'));
1465
1515
  }
1466
1516
  return response.json();
1467
1517
  }
@@ -1475,7 +1525,7 @@ class OneShot {
1475
1525
  */
1476
1526
  async markNotificationRead(notificationId) {
1477
1527
  this.validate(notificationId, 'notificationId');
1478
- const response = await fetch(`${this.baseUrl}/v1/tools/notifications/${notificationId}/read`, {
1528
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/notifications/${notificationId}/read`, {
1479
1529
  method: 'PATCH',
1480
1530
  headers: await this.signedReadHeaders('write')
1481
1531
  });
@@ -1483,15 +1533,15 @@ class OneShot {
1483
1533
  throw new errors_2.ToolError('Notification not found', 404, 'Notification not found');
1484
1534
  }
1485
1535
  if (!response.ok) {
1486
- throw new errors_2.ToolError('Failed to mark notification as read', response.status, await response.text());
1536
+ throw new errors_2.ToolError('Failed to mark notification as read', response.status, await response.text(), response.headers.get('retry-after'));
1487
1537
  }
1488
1538
  }
1489
1539
  async getUnifiedBalance() {
1490
- const response = await fetch(`${this.baseUrl}/v1/tools/balance`, {
1540
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/balance`, {
1491
1541
  headers: await this.signedReadHeaders()
1492
1542
  });
1493
1543
  if (!response.ok) {
1494
- throw new errors_2.ToolError('Failed to fetch balance', response.status, await response.text());
1544
+ throw new errors_2.ToolError('Failed to fetch balance', response.status, await response.text(), response.headers.get('retry-after'));
1495
1545
  }
1496
1546
  return response.json();
1497
1547
  }
@@ -1581,20 +1631,89 @@ class OneShot {
1581
1631
  * console.log(status.status, status.budget?.remaining);
1582
1632
  * ```
1583
1633
  */
1584
- async getComputeGoal(goalId) {
1634
+ async getComputeGoal(goalId, options = {}) {
1585
1635
  this.validate(goalId, 'goalId');
1586
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}`, {
1587
- headers: await this.signedReadHeaders()
1636
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}`, {
1637
+ headers: await this.signedReadHeaders(),
1638
+ signal: options.signal,
1588
1639
  });
1589
1640
  if (response.status === 404) {
1590
1641
  throw new errors_2.ToolError('Goal not found', 404, 'Goal not found');
1591
1642
  }
1592
1643
  if (!response.ok) {
1593
- throw new errors_2.ToolError('Failed to get compute goal', response.status, await response.text());
1644
+ // Keep the HTTP classification even if the error body is interrupted.
1645
+ const body = await response.text().catch(() => {
1646
+ options.signal?.throwIfAborted();
1647
+ return 'Response body unavailable';
1648
+ });
1649
+ throw new errors_2.ToolError('Failed to get compute goal', response.status, body, response.headers.get('retry-after'));
1594
1650
  }
1595
1651
  const json = await response.json();
1596
1652
  return json.data;
1597
1653
  }
1654
+ /**
1655
+ * Poll a compute goal using its goal_id, not its creation request_id.
1656
+ * Returns the complete goal snapshot on completed; failed/cancelled reject
1657
+ * with JobError (goal ID, end_reason code, snapshot in partialResult).
1658
+ * Budget exhaustion ends this wait; after fundComputeGoal revives the goal,
1659
+ * call waitForComputeGoal again with the same ID to observe the new run.
1660
+ * Paused/awaiting_confirmation and recurring goals keep waiting until the
1661
+ * goal ends or the timeout (seconds, default 120) expires with JobTimeoutError.
1662
+ * Infinity disables the local deadline.
1663
+ * onStatusUpdate receives each distinct observed status before settlement.
1664
+ * Callback throws/rejections are ignored and promises are not awaited.
1665
+ * Transient reads (5xx, 429, network failures) retry on the polling cadence.
1666
+ */
1667
+ async waitForComputeGoal(goalId, options = {}) {
1668
+ this.validate(goalId, 'goalId');
1669
+ const started = Date.now();
1670
+ const scope = (0, deadline_1.deadlineScope)(options.signal, (options.timeout ?? 120) * 1000);
1671
+ const poll = async () => {
1672
+ let lastStatus;
1673
+ let polls = 0;
1674
+ while (true) {
1675
+ scope.signal.throwIfAborted();
1676
+ let goal;
1677
+ try {
1678
+ goal = await this.getComputeGoal(goalId, { signal: scope.signal });
1679
+ }
1680
+ catch (error) {
1681
+ scope.signal.throwIfAborted();
1682
+ if (!isTransientComputeReadError(error))
1683
+ throw error;
1684
+ await this.sleep(HTTP_POLL_BACKOFF_MS[Math.min(polls++, HTTP_POLL_BACKOFF_MS.length - 1)], scope.signal);
1685
+ continue;
1686
+ }
1687
+ // A late response from a transport ignoring abort must not emit progress.
1688
+ scope.signal.throwIfAborted();
1689
+ if (goal.status !== lastStatus) {
1690
+ lastStatus = goal.status;
1691
+ try {
1692
+ Promise.resolve(options.onStatusUpdate?.(goal.status, goalId)).catch(() => { });
1693
+ }
1694
+ catch { /* Observers cannot change the goal outcome. */ }
1695
+ }
1696
+ if (goal.status === 'completed')
1697
+ return goal;
1698
+ if (goal.status === 'failed' || goal.status === 'cancelled') {
1699
+ const reason = goal.end_reason ?? goal.status;
1700
+ throw new errors_2.JobError(`Compute goal ${goal.status}: ${reason}`, goalId, reason, reason, { ...goal });
1701
+ }
1702
+ await this.sleep(HTTP_POLL_BACKOFF_MS[Math.min(polls++, HTTP_POLL_BACKOFF_MS.length - 1)], scope.signal);
1703
+ }
1704
+ };
1705
+ try {
1706
+ return await (0, deadline_1.abortable)(poll(), scope.signal);
1707
+ }
1708
+ catch (error) {
1709
+ if (scope.timedOut())
1710
+ throw new errors_2.JobTimeoutError(goalId, Date.now() - started);
1711
+ throw error;
1712
+ }
1713
+ finally {
1714
+ scope.close();
1715
+ }
1716
+ }
1598
1717
  /**
1599
1718
  * List tasks under a compute goal
1600
1719
  *
@@ -1608,11 +1727,11 @@ class OneShot {
1608
1727
  */
1609
1728
  async getComputeTasks(goalId) {
1610
1729
  this.validate(goalId, 'goalId');
1611
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/tasks`, {
1730
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/tasks`, {
1612
1731
  headers: await this.signedReadHeaders()
1613
1732
  });
1614
1733
  if (!response.ok) {
1615
- throw new errors_2.ToolError('Failed to get compute tasks', response.status, await response.text());
1734
+ throw new errors_2.ToolError('Failed to get compute tasks', response.status, await response.text(), response.headers.get('retry-after'));
1616
1735
  }
1617
1736
  const json = await response.json();
1618
1737
  return json.data;
@@ -1631,11 +1750,11 @@ class OneShot {
1631
1750
  */
1632
1751
  async getComputeOutputs(goalId) {
1633
1752
  this.validate(goalId, 'goalId');
1634
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/outputs`, {
1753
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/outputs`, {
1635
1754
  headers: await this.signedReadHeaders()
1636
1755
  });
1637
1756
  if (!response.ok) {
1638
- throw new errors_2.ToolError('Failed to list compute outputs', response.status, await response.text());
1757
+ throw new errors_2.ToolError('Failed to list compute outputs', response.status, await response.text(), response.headers.get('retry-after'));
1639
1758
  }
1640
1759
  const json = await response.json();
1641
1760
  return json.data;
@@ -1646,11 +1765,11 @@ class OneShot {
1646
1765
  this.validate(key, 'key');
1647
1766
  // The key is a path: keep its slashes, escape everything else.
1648
1767
  const path = key.split('/').map(encodeURIComponent).join('/');
1649
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/outputs/${path}`, {
1768
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/outputs/${path}`, {
1650
1769
  headers: await this.signedReadHeaders()
1651
1770
  });
1652
1771
  if (!response.ok) {
1653
- throw new errors_2.ToolError('Failed to read compute output', response.status, await response.text());
1772
+ throw new errors_2.ToolError('Failed to read compute output', response.status, await response.text(), response.headers.get('retry-after'));
1654
1773
  }
1655
1774
  const json = await response.json();
1656
1775
  return json.data;
@@ -1666,14 +1785,14 @@ class OneShot {
1666
1785
  */
1667
1786
  async getComputeBudget(goalId) {
1668
1787
  this.validate(goalId, 'goalId');
1669
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/budget`, {
1788
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/budget`, {
1670
1789
  headers: await this.signedReadHeaders()
1671
1790
  });
1672
1791
  if (response.status === 404) {
1673
1792
  throw new errors_2.ToolError('Budget not found', 404, 'Budget not found for this goal');
1674
1793
  }
1675
1794
  if (!response.ok) {
1676
- throw new errors_2.ToolError('Failed to get compute budget', response.status, await response.text());
1795
+ throw new errors_2.ToolError('Failed to get compute budget', response.status, await response.text(), response.headers.get('retry-after'));
1677
1796
  }
1678
1797
  const json = await response.json();
1679
1798
  return json.data;
@@ -1689,7 +1808,7 @@ class OneShot {
1689
1808
  */
1690
1809
  async cancelComputeGoal(goalId, reason) {
1691
1810
  this.validate(goalId, 'goalId');
1692
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/cancel`, {
1811
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/cancel`, {
1693
1812
  method: 'POST',
1694
1813
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1695
1814
  body: JSON.stringify({ reason })
@@ -1703,7 +1822,7 @@ class OneShot {
1703
1822
  return { goal_id: goalId, status: pending.status ?? 'pending_settlement', message: pending.message };
1704
1823
  }
1705
1824
  if (!response.ok) {
1706
- throw new errors_2.ToolError('Failed to cancel compute goal', response.status, await response.text());
1825
+ throw new errors_2.ToolError('Failed to cancel compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1707
1826
  }
1708
1827
  const json = await response.json();
1709
1828
  return json.data;
@@ -1723,16 +1842,16 @@ class OneShot {
1723
1842
  async respondToComputeTask(goalId, input) {
1724
1843
  this.validate(goalId, 'goalId');
1725
1844
  this.validate(input.task_id, 'task_id');
1726
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1845
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1727
1846
  method: 'POST',
1728
1847
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1729
1848
  body: JSON.stringify(input)
1730
1849
  });
1731
1850
  if (response.status === 404) {
1732
- throw new errors_2.ToolError('Goal or task not found', 404, await response.text());
1851
+ throw new errors_2.ToolError('Goal or task not found', 404, await response.text(), response.headers.get('retry-after'));
1733
1852
  }
1734
1853
  if (!response.ok) {
1735
- throw new errors_2.ToolError('Failed to respond to compute task', response.status, await response.text());
1854
+ throw new errors_2.ToolError('Failed to respond to compute task', response.status, await response.text(), response.headers.get('retry-after'));
1736
1855
  }
1737
1856
  const json = await response.json();
1738
1857
  return json.data;
@@ -1752,15 +1871,15 @@ class OneShot {
1752
1871
  */
1753
1872
  async confirmComputeGoal(goalId, input) {
1754
1873
  this.validate(goalId, 'goalId');
1755
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1874
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1756
1875
  method: 'POST',
1757
1876
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1758
1877
  body: JSON.stringify({ confirm_done: input.approved, ...(input.response ? { response: input.response } : {}) }),
1759
1878
  });
1760
1879
  if (response.status === 404)
1761
- throw new errors_2.ToolError('Goal not found', 404, await response.text());
1880
+ throw new errors_2.ToolError('Goal not found', 404, await response.text(), response.headers.get('retry-after'));
1762
1881
  if (!response.ok)
1763
- throw new errors_2.ToolError('Failed to confirm compute goal', response.status, await response.text());
1882
+ throw new errors_2.ToolError('Failed to confirm compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1764
1883
  const json = await response.json();
1765
1884
  return json.data;
1766
1885
  }
@@ -1774,13 +1893,13 @@ class OneShot {
1774
1893
  */
1775
1894
  async pauseComputeGoal(goalId, reason) {
1776
1895
  this.validate(goalId, 'goalId');
1777
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/pause`, {
1896
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/pause`, {
1778
1897
  method: 'POST',
1779
1898
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1780
1899
  body: JSON.stringify({ reason })
1781
1900
  });
1782
1901
  if (!response.ok) {
1783
- throw new errors_2.ToolError('Failed to pause compute goal', response.status, await response.text());
1902
+ throw new errors_2.ToolError('Failed to pause compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1784
1903
  }
1785
1904
  const json = await response.json();
1786
1905
  return json.data;
@@ -1796,13 +1915,13 @@ class OneShot {
1796
1915
  */
1797
1916
  async resumeComputeGoal(goalId) {
1798
1917
  this.validate(goalId, 'goalId');
1799
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/resume`, {
1918
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/resume`, {
1800
1919
  method: 'POST',
1801
1920
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1802
1921
  body: JSON.stringify({})
1803
1922
  });
1804
1923
  if (!response.ok) {
1805
- throw new errors_2.ToolError('Failed to resume compute goal', response.status, await response.text());
1924
+ throw new errors_2.ToolError('Failed to resume compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1806
1925
  }
1807
1926
  const json = await response.json();
1808
1927
  return json.data;
@@ -1932,11 +2051,11 @@ class OneShot {
1932
2051
  */
1933
2052
  async spendBreakdown(options) {
1934
2053
  const qs = this.buildQuery({ period: options?.period || undefined });
1935
- const response = await fetch(`${this.baseUrl}/v1/analytics/spend/breakdown${qs ? `?${qs}` : ''}`, {
2054
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/spend/breakdown${qs ? `?${qs}` : ''}`, {
1936
2055
  headers: await this.signedReadHeaders()
1937
2056
  });
1938
2057
  if (!response.ok) {
1939
- throw new errors_2.ToolError('Failed to get spend breakdown', response.status, await response.text());
2058
+ throw new errors_2.ToolError('Failed to get spend breakdown', response.status, await response.text(), response.headers.get('retry-after'));
1940
2059
  }
1941
2060
  return response.json();
1942
2061
  }
@@ -1951,11 +2070,11 @@ class OneShot {
1951
2070
  */
1952
2071
  async rocs(options) {
1953
2072
  const qs = this.buildQuery({ period: options?.period || undefined });
1954
- const response = await fetch(`${this.baseUrl}/v1/analytics/rocs${qs ? `?${qs}` : ''}`, {
2073
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/rocs${qs ? `?${qs}` : ''}`, {
1955
2074
  headers: await this.signedReadHeaders()
1956
2075
  });
1957
2076
  if (!response.ok) {
1958
- throw new errors_2.ToolError('Failed to get RoCS', response.status, await response.text());
2077
+ throw new errors_2.ToolError('Failed to get RoCS', response.status, await response.text(), response.headers.get('retry-after'));
1959
2078
  }
1960
2079
  return response.json();
1961
2080
  }
@@ -1987,11 +2106,11 @@ class OneShot {
1987
2106
  since: toIso(options?.since),
1988
2107
  until: toIso(options?.until),
1989
2108
  });
1990
- const response = await fetch(`${this.baseUrl}/v1/analytics/receipts${qs ? `?${qs}` : ''}`, {
2109
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/receipts${qs ? `?${qs}` : ''}`, {
1991
2110
  headers: await this.signedReadHeaders()
1992
2111
  });
1993
2112
  if (!response.ok) {
1994
- throw new errors_2.ToolError('Failed to list receipts', response.status, await response.text());
2113
+ throw new errors_2.ToolError('Failed to list receipts', response.status, await response.text(), response.headers.get('retry-after'));
1995
2114
  }
1996
2115
  return response.json();
1997
2116
  }
@@ -2024,19 +2143,19 @@ class OneShot {
2024
2143
  this.validate(valueTag.type, 'valueTag.type');
2025
2144
  // Cadence-level: route to the outcome ledger by correlation key.
2026
2145
  if (typeof ref === 'object' && ref.goalId && !ref.receiptId && !ref.requestId) {
2027
- const response = await fetch(`${this.baseUrl}/v1/analytics/outcomes`, {
2146
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/outcomes`, {
2028
2147
  method: 'POST',
2029
2148
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2030
2149
  body: JSON.stringify({ goal_id: ref.goalId, ...valueTag }),
2031
2150
  });
2032
2151
  if (!response.ok) {
2033
- throw new errors_2.ToolError('Failed to record outcome value', response.status, await response.text());
2152
+ throw new errors_2.ToolError('Failed to record outcome value', response.status, await response.text(), response.headers.get('retry-after'));
2034
2153
  }
2035
2154
  return;
2036
2155
  }
2037
2156
  const id = typeof ref === 'string' ? ref : (ref.receiptId ?? ref.requestId);
2038
2157
  this.validate(id, 'receiptId/requestId/goalId');
2039
- const response = await fetch(`${this.baseUrl}/v1/analytics/receipts/${id}/value`, {
2158
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/receipts/${id}/value`, {
2040
2159
  method: 'PATCH',
2041
2160
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2042
2161
  body: JSON.stringify(valueTag)
@@ -2045,7 +2164,7 @@ class OneShot {
2045
2164
  throw new errors_2.ToolError('Receipt not found', 404, 'Receipt not found or not owned by this agent');
2046
2165
  }
2047
2166
  if (!response.ok) {
2048
- throw new errors_2.ToolError('Failed to tag receipt value', response.status, await response.text());
2167
+ throw new errors_2.ToolError('Failed to tag receipt value', response.status, await response.text(), response.headers.get('retry-after'));
2049
2168
  }
2050
2169
  }
2051
2170
  /**
@@ -2067,11 +2186,11 @@ class OneShot {
2067
2186
  */
2068
2187
  async rocsByGoal(options) {
2069
2188
  const qs = this.buildQuery({ period: options?.period || undefined, goal_id: options?.goalId || undefined });
2070
- const response = await fetch(`${this.baseUrl}/v1/analytics/rocs/by-goal${qs ? `?${qs}` : ''}`, {
2189
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/rocs/by-goal${qs ? `?${qs}` : ''}`, {
2071
2190
  headers: await this.signedReadHeaders()
2072
2191
  });
2073
2192
  if (!response.ok) {
2074
- throw new errors_2.ToolError('Failed to get RoCS by goal', response.status, await response.text());
2193
+ throw new errors_2.ToolError('Failed to get RoCS by goal', response.status, await response.text(), response.headers.get('retry-after'));
2075
2194
  }
2076
2195
  return response.json();
2077
2196
  }
@@ -2112,7 +2231,7 @@ class OneShot {
2112
2231
  const headers = await this.signedReadHeaders(`staging:${method}:${path}:${digest}`);
2113
2232
  if (!headers['x-agent-proof'])
2114
2233
  throw new Error('Could not sign staging application');
2115
- const response = await fetch(`${this.baseUrl}/v1/staging${path}`, {
2234
+ const response = await this.fetchApi(`${this.baseUrl}/v1/staging${path}`, {
2116
2235
  method, headers: { ...headers, 'Content-Type': 'application/json' },
2117
2236
  signal: AbortSignal.timeout(10000),
2118
2237
  ...(body ? { body: JSON.stringify(body) } : {}),
@@ -2147,7 +2266,7 @@ class OneShot {
2147
2266
  return base;
2148
2267
  try {
2149
2268
  const agent = this.provider.address;
2150
- const issuedAt = Math.floor(Date.now() / 1000);
2269
+ const issuedAt = Math.floor(Date.now() / 1000) + (domainName === 'OneShot Agent Auth' ? this.proofClockOffsetSec : 0);
2151
2270
  const nonce = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
2152
2271
  const signature = await this.provider.signTypedData(
2153
2272
  // `domainName` lets a different origin (e.g. Soul Hunt, which keeps
@@ -2182,6 +2301,45 @@ class OneShot {
2182
2301
  return base;
2183
2302
  }
2184
2303
  }
2304
+ /** Retry a clock-window refusal once, before the API has dispatched the request.
2305
+ * Only this API's proof clock changes; payments and other services keep their own clocks.
2306
+ * Callers supply replayable JSON bodies and retain their original abort/deadline signal.
2307
+ */
2308
+ async fetchApi(url, init) {
2309
+ const response = await fetch(url, init);
2310
+ const headers = new Headers(init.headers);
2311
+ const proof = headers.get('x-agent-proof');
2312
+ if (response.status !== 401 || !proof || this._accessToken)
2313
+ return response;
2314
+ const origin = new URL(this.baseUrl).origin;
2315
+ if (new URL(url).origin !== origin || (response.url && new URL(response.url).origin !== origin))
2316
+ return response;
2317
+ let scope;
2318
+ let serverTime;
2319
+ try {
2320
+ const body = await response.clone().json();
2321
+ if (body?.error !== 'proof_required' ||
2322
+ !['proof expired', 'proof issued in the future'].some(reason => body.message === `This endpoint requires a signed agent proof (x-agent-proof): ${reason}`) ||
2323
+ typeof body.server_time !== 'number' || !Number.isSafeInteger(body.server_time) || body.server_time <= 0)
2324
+ return response;
2325
+ const sent = JSON.parse(typeof Buffer !== 'undefined' ? Buffer.from(proof, 'base64').toString('utf-8') : atob(proof));
2326
+ if (typeof sent.scope !== 'string')
2327
+ return response;
2328
+ scope = sent.scope;
2329
+ serverTime = body.server_time;
2330
+ }
2331
+ catch {
2332
+ return response;
2333
+ }
2334
+ init.signal?.throwIfAborted();
2335
+ this.proofClockOffsetSec = serverTime - Math.floor(Date.now() / 1000);
2336
+ const refreshed = await this.signedReadHeaders(scope);
2337
+ init.signal?.throwIfAborted();
2338
+ if (!refreshed['x-agent-proof'])
2339
+ return response;
2340
+ headers.set('x-agent-proof', refreshed['x-agent-proof']);
2341
+ return fetch(url, { ...init, headers });
2342
+ }
2185
2343
  /** Build a query string, skipping null/undefined values. Pass falsy-but-valid
2186
2344
  * values (0, '') as undefined at the call site to match prior `if (x)` guards. */
2187
2345
  buildQuery(params) {
@@ -2225,7 +2383,7 @@ class OneShot {
2225
2383
  body.alert_email = this._alertEmail;
2226
2384
  let response;
2227
2385
  try {
2228
- response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
2386
+ response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/budgets`, {
2229
2387
  method: 'PUT',
2230
2388
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2231
2389
  body: JSON.stringify(body),
@@ -2236,7 +2394,7 @@ class OneShot {
2236
2394
  }
2237
2395
  if (!response.ok) {
2238
2396
  const text = await response.text();
2239
- throw new errors_2.BudgetSyncError(`Could not sync spend budget (${response.status}): ${text}`, response.status, text);
2397
+ throw new errors_2.BudgetSyncError(`Could not sync spend budget (${response.status}): ${text}`, response.status, text, response.headers.get('retry-after'));
2240
2398
  }
2241
2399
  this.log('Budget synced');
2242
2400
  })();
@@ -2278,7 +2436,7 @@ class OneShot {
2278
2436
  */
2279
2437
  /** Signed-proof JSON request to a free agent route; returns `data`. Errors map through failFromResponse. */
2280
2438
  async agentJson(path, method, body, scope = 'read') {
2281
- const response = await fetch(`${this.baseUrl}${path}`, {
2439
+ const response = await this.fetchApi(`${this.baseUrl}${path}`, {
2282
2440
  method,
2283
2441
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders(scope)) },
2284
2442
  ...(body === undefined || method === 'GET' ? {} : { body: JSON.stringify(body) }),
@@ -2290,11 +2448,11 @@ class OneShot {
2290
2448
  return (json && typeof json === 'object' && 'data' in json ? json.data : json);
2291
2449
  }
2292
2450
  async budgets() {
2293
- const response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
2451
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/budgets`, {
2294
2452
  headers: await this.signedReadHeaders(),
2295
2453
  });
2296
2454
  if (!response.ok) {
2297
- throw new errors_2.ToolError('Failed to fetch budgets', response.status, await response.text());
2455
+ throw new errors_2.ToolError('Failed to fetch budgets', response.status, await response.text(), response.headers.get('retry-after'));
2298
2456
  }
2299
2457
  const body = await response.json();
2300
2458
  return (body.data ?? body);
@@ -2317,24 +2475,24 @@ class OneShot {
2317
2475
  */
2318
2476
  async createAccessToken(opts = {}) {
2319
2477
  this.assertWalletSession('mint access tokens');
2320
- const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens`, {
2478
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens`, {
2321
2479
  method: 'POST',
2322
2480
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2323
2481
  body: JSON.stringify(opts.name !== undefined ? { name: opts.name } : {}),
2324
2482
  });
2325
2483
  if (!response.ok)
2326
- throw new errors_2.ToolError('Failed to create access token', response.status, await response.text());
2484
+ throw new errors_2.ToolError('Failed to create access token', response.status, await response.text(), response.headers.get('retry-after'));
2327
2485
  const body = await response.json();
2328
2486
  return body.data;
2329
2487
  }
2330
2488
  /** List this agent's access tokens (prefix, label, last use, spend today — never the token). */
2331
2489
  async listAccessTokens(opts = {}) {
2332
2490
  this.assertWalletSession('list access tokens');
2333
- const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
2491
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
2334
2492
  headers: await this.signedReadHeaders(),
2335
2493
  });
2336
2494
  if (!response.ok)
2337
- throw new errors_2.ToolError('Failed to list access tokens', response.status, await response.text());
2495
+ throw new errors_2.ToolError('Failed to list access tokens', response.status, await response.text(), response.headers.get('retry-after'));
2338
2496
  const body = await response.json();
2339
2497
  return body.data.tokens;
2340
2498
  }
@@ -2342,12 +2500,12 @@ class OneShot {
2342
2500
  async revokeAccessToken(id) {
2343
2501
  this.assertWalletSession('revoke access tokens');
2344
2502
  this.validate(id, 'id');
2345
- const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens/${encodeURIComponent(id)}`, {
2503
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens/${encodeURIComponent(id)}`, {
2346
2504
  method: 'DELETE',
2347
2505
  headers: await this.signedReadHeaders('write'),
2348
2506
  });
2349
2507
  if (!response.ok)
2350
- throw new errors_2.ToolError('Failed to revoke access token', response.status, await response.text());
2508
+ throw new errors_2.ToolError('Failed to revoke access token', response.status, await response.text(), response.headers.get('retry-after'));
2351
2509
  const body = await response.json();
2352
2510
  return body.data;
2353
2511
  }
@@ -2406,7 +2564,7 @@ class OneShot {
2406
2564
  const budget = response.status === 403 ? this.parseBudgetRejection(text) : undefined;
2407
2565
  if (budget)
2408
2566
  throw budget;
2409
- throw new errors_2.ToolError(message, response.status, text);
2567
+ throw new errors_2.ToolError(message, response.status, text, response.headers.get('retry-after'));
2410
2568
  }
2411
2569
  /**
2412
2570
  * Map a 402 `insufficient_credits` body (access-token sessions) onto a typed
@@ -2470,12 +2628,12 @@ class OneShot {
2470
2628
  throw new errors_2.ToolError(`Could not resolve the access token's agent (network): ${err}`, 0, String(err));
2471
2629
  }
2472
2630
  if (!response.ok) {
2473
- throw new errors_2.ToolError('Could not resolve the access token\'s agent', response.status, await response.text());
2631
+ throw new errors_2.ToolError('Could not resolve the access token\'s agent', response.status, await response.text(), response.headers.get('retry-after'));
2474
2632
  }
2475
2633
  const json = await response.json();
2476
2634
  const address = json.data?.wallet_address;
2477
2635
  if (!address)
2478
- throw new errors_2.ToolError('GET /v1/agents/me returned no wallet_address', response.status, JSON.stringify(json));
2636
+ throw new errors_2.ToolError('GET /v1/agents/me returned no wallet_address', response.status, JSON.stringify(json), response.headers.get('retry-after'));
2479
2637
  return address;
2480
2638
  }
2481
2639
  /**
@@ -2578,7 +2736,7 @@ class OneShot {
2578
2736
  const headers = signed ? await this.signedReadHeaders() : undefined;
2579
2737
  if (scope.signal.aborted)
2580
2738
  throw new errors_2.OneShotError('Read deadline exceeded');
2581
- const response = await fetch(`${this.baseUrl}${path}`, { headers, signal: scope.signal });
2739
+ const response = await this.fetchApi(`${this.baseUrl}${path}`, { headers, signal: scope.signal });
2582
2740
  if (!response.ok && !(allowDegraded && response.status === 503))
2583
2741
  await this.failFromResponse('Reliability read failed', response);
2584
2742
  return await response.json();
@@ -2597,7 +2755,7 @@ class OneShot {
2597
2755
  }
2598
2756
  async executeToolRequest(endpoint, options, quoteId) {
2599
2757
  // LinkedIn writes act through a human's account and cannot be recalled: always keyed.
2600
- const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2758
+ const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2601
2759
  const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
2602
2760
  if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
2603
2761
  throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
@@ -2640,7 +2798,7 @@ class OneShot {
2640
2798
  this.log('Memo truncated to 1000 chars');
2641
2799
  }
2642
2800
  }
2643
- else if (!endpoint.includes('/inbox') && !endpoint.includes('/notifications') && !endpoint.includes('/balance')) {
2801
+ else if (!endpoint.endsWith('/v1/tools/solve') && !endpoint.endsWith('/v1/tools/predict') && !endpoint.includes('/inbox') && !endpoint.includes('/notifications') && !endpoint.includes('/balance')) {
2644
2802
  this.log('No memo provided — consider adding a reason for audit trail');
2645
2803
  }
2646
2804
  if (payload.decisionContext !== undefined) {
@@ -2669,10 +2827,10 @@ class OneShot {
2669
2827
  context.phase = 'submission';
2670
2828
  let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
2671
2829
  // Handle 402 Payment Required
2672
- if (response.status === 402 && /\/v1\/tools\/(find\/person|lookup\/person-(profile|dossier))$/.test(endpoint)) {
2830
+ if (response.status === 402 && /\/v1\/tools\/(solve|predict|find\/person|lookup\/person-(profile|dossier))$/.test(endpoint)) {
2673
2831
  ({ execResp: response } = await this.runQuoteToPay({
2674
2832
  endpoint, payload, signal, maxCost, quoteResponse: response, extraHeaders,
2675
- expectMsg: 'Expected a person quote', totalOf: quote => quote.total,
2833
+ expectMsg: 'Expected a tool quote', totalOf: quote => quote.total,
2676
2834
  onQuote: () => { context.phase = 'payment'; },
2677
2835
  }));
2678
2836
  context.phase = 'submission';
@@ -2790,6 +2948,15 @@ class OneShot {
2790
2948
  * `wait: false`, or to resume waiting after a client restart. Delivery is the
2791
2949
  * same as for blocking calls: WebSocket push when the server offers it, HTTP
2792
2950
  * polling of `GET /v1/requests/:id` as the source of truth.
2951
+ * For compute goals, use `waitForComputeGoal(goal_id)` instead: their creation
2952
+ * request_id does not track progress. `onStatusUpdate` observes distinct job
2953
+ * statuses, including the terminal status before settlement.
2954
+ * With waitForPhones, completed is deferred until the phone wait returns.
2955
+ * Callback errors are ignored; returned promises are not awaited.
2956
+ *
2957
+ * `signal` ends the local wait only (in-flight fetch aborted, poll timer
2958
+ * cleared, rejects with `OneShotError('Operation cancelled')`); the job
2959
+ * itself keeps running and can be waited for again.
2793
2960
  */
2794
2961
  async waitForResult(requestId, options = {}) {
2795
2962
  this.validate(requestId, 'requestId');
@@ -2802,6 +2969,9 @@ class OneShot {
2802
2969
  // swallows frames) costs nothing beyond the poll cadence. Previously the
2803
2970
  // client waited 60% of its timeout on the socket before polling at all —
2804
2971
  // in prod that was ~72s of dead air on every call while the push never came.
2972
+ // An already-aborted signal ends the wait before any proof is signed or sent.
2973
+ if (signal?.aborted)
2974
+ throw new errors_2.OneShotError('Operation cancelled');
2805
2975
  const inner = new AbortController();
2806
2976
  const onOuterAbort = () => inner.abort();
2807
2977
  if (signal?.aborted)
@@ -2809,12 +2979,24 @@ class OneShot {
2809
2979
  else
2810
2980
  signal?.addEventListener('abort', onOuterAbort, { once: true });
2811
2981
  const wait = { pushConfirmed: false, lastStatus: undefined, via: undefined };
2812
- // Two sources now report status; only surface changes to the caller.
2982
+ const notify = (status) => {
2983
+ // Progress observers must not alter job outcomes or trigger HTTP retries.
2984
+ try {
2985
+ Promise.resolve(onStatusUpdate?.(status, requestId)).catch(() => { });
2986
+ }
2987
+ catch { /* Ignore synchronous observer errors too. */ }
2988
+ };
2989
+ // Latch terminal transport status immediately, but defer completion for
2990
+ // callers waiting on phones so the losing branch cannot emit stale states.
2813
2991
  const emit = (status) => {
2992
+ if (wait.lastStatus === 'completed' || wait.lastStatus === 'failed')
2993
+ return;
2814
2994
  if (status === wait.lastStatus)
2815
2995
  return;
2816
2996
  wait.lastStatus = status;
2817
- onStatusUpdate?.(status, requestId);
2997
+ if (status === 'completed' && phoneOpts?.waitForPhones)
2998
+ return;
2999
+ notify(status);
2818
3000
  };
2819
3001
  const startedAt = Date.now();
2820
3002
  const wsBranch = this.waitViaWebSocket(requestId, inner.signal, emit, wait).catch((err) => {
@@ -2850,8 +3032,10 @@ class OneShot {
2850
3032
  // hit a transient error before any successful refresh, we return what we
2851
3033
  // already have (with phones_pending=true still set so consumers know).
2852
3034
  if (phoneOpts?.waitForPhones && this._isPhonesPending(result)) {
2853
- return this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
3035
+ result = await this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
2854
3036
  }
3037
+ if (phoneOpts?.waitForPhones)
3038
+ notify('completed');
2855
3039
  return result;
2856
3040
  }
2857
3041
  /**
@@ -2895,7 +3079,7 @@ class OneShot {
2895
3079
  if (signal?.aborted)
2896
3080
  throw new errors_2.OneShotError('Operation cancelled');
2897
3081
  try {
2898
- const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
3082
+ const resp = await this.fetchApi(`${this.baseUrl}/v1/requests/${requestId}`, {
2899
3083
  headers: await this.signedReadHeaders(),
2900
3084
  signal,
2901
3085
  });
@@ -2904,7 +3088,7 @@ class OneShot {
2904
3088
  // whatever we last had so consumers don't lose the sync result.
2905
3089
  if (lastResult !== undefined)
2906
3090
  return lastResult;
2907
- throw new errors_2.ToolError('Failed to check job status', resp.status, await resp.text());
3091
+ throw new errors_2.ToolError('Failed to check job status', resp.status, await resp.text(), resp.headers.get('retry-after'));
2908
3092
  }
2909
3093
  const job = await resp.json();
2910
3094
  lastResult = (job.result ?? job);
@@ -2913,6 +3097,8 @@ class OneShot {
2913
3097
  }
2914
3098
  }
2915
3099
  catch (err) {
3100
+ if (signal?.aborted)
3101
+ throw new errors_2.OneShotError('Operation cancelled');
2916
3102
  if (err instanceof errors_2.OneShotError)
2917
3103
  throw err;
2918
3104
  if (lastResult !== undefined)
@@ -3008,6 +3194,7 @@ class OneShot {
3008
3194
  wait.pushConfirmed = true;
3009
3195
  this.log('WebSocket push confirmed — relaxing HTTP polling');
3010
3196
  }
3197
+ emit(String(msg.status));
3011
3198
  if (msg.status === 'completed') {
3012
3199
  this.log('Job completed (WebSocket)');
3013
3200
  settle(() => {
@@ -3027,9 +3214,6 @@ class OneShot {
3027
3214
  reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code, msg.result));
3028
3215
  });
3029
3216
  }
3030
- else {
3031
- emit(String(msg.status));
3032
- }
3033
3217
  }
3034
3218
  catch {
3035
3219
  // Ignore malformed messages
@@ -3065,6 +3249,9 @@ class OneShot {
3065
3249
  catch (error) {
3066
3250
  if (scope.timedOut())
3067
3251
  throw new errors_2.JobTimeoutError(requestId, Date.now() - started);
3252
+ // An abort mid-fetch surfaces as the transport's AbortError; report it as the cancellation it is.
3253
+ if (signal?.aborted && !(error instanceof errors_2.OneShotError))
3254
+ throw new errors_2.OneShotError('Operation cancelled');
3068
3255
  throw error;
3069
3256
  }
3070
3257
  finally {
@@ -3081,7 +3268,7 @@ class OneShot {
3081
3268
  if (signal?.aborted)
3082
3269
  throw new errors_2.OneShotError('Operation cancelled');
3083
3270
  try {
3084
- const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
3271
+ const resp = await this.fetchApi(`${this.baseUrl}/v1/requests/${requestId}`, {
3085
3272
  headers: await this.signedReadHeaders(),
3086
3273
  signal
3087
3274
  });
@@ -3092,9 +3279,10 @@ class OneShot {
3092
3279
  if (resp.status >= 500 || resp.status === 429) {
3093
3280
  throw new Error(`Poll returned ${resp.status}: ${body.slice(0, 200)}`);
3094
3281
  }
3095
- throw new errors_2.ToolError('Failed to check job status', resp.status, body);
3282
+ throw new errors_2.ToolError('Failed to check job status', resp.status, body, resp.headers.get('retry-after'));
3096
3283
  }
3097
3284
  const job = await resp.json();
3285
+ emit?.(String(job.status));
3098
3286
  if (job.status === 'completed') {
3099
3287
  this.log('Job completed');
3100
3288
  if (wait)
@@ -3117,7 +3305,6 @@ class OneShot {
3117
3305
  wait.via = 'http';
3118
3306
  throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code, job.result);
3119
3307
  }
3120
- emit?.(String(job.status));
3121
3308
  retries = 0;
3122
3309
  const interval = wait?.pushConfirmed
3123
3310
  ? HTTP_POLL_RELAXED_MS
@@ -3200,7 +3387,7 @@ class OneShot {
3200
3387
  // alone (401 proof_required).
3201
3388
  const unsignedStub = payment?.payload?.signature === '0x';
3202
3389
  const askedForProof = !!extraHeaders?.['x-agent-proof'];
3203
- const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send)$/.test(endpoint)
3390
+ const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict)$/.test(endpoint)
3204
3391
  ? 'submit'
3205
3392
  : askedForProof ? 'read' : 'write';
3206
3393
  const headers = {
@@ -3228,7 +3415,7 @@ class OneShot {
3228
3415
  transportDeadline?.signal.addEventListener('abort', () => transportDeadline.close(), { once: true });
3229
3416
  if (fetchSignal?.aborted)
3230
3417
  throw new errors_2.OneShotError('Operation cancelled');
3231
- const work = fetch(`${this.baseUrl}${endpoint}`, {
3418
+ const work = this.fetchApi(`${this.baseUrl}${endpoint}`, {
3232
3419
  method: 'POST', headers, body: JSON.stringify(data), signal: fetchSignal,
3233
3420
  });
3234
3421
  try {
@@ -3385,7 +3572,7 @@ class OneShot {
3385
3572
  }
3386
3573
  catch (err) {
3387
3574
  // Keyed writes may already be queued server-side after a transport failure: keep the reservation.
3388
- if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3575
+ if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3389
3576
  this.releaseUsdcReservation(signed.reservation);
3390
3577
  throw err;
3391
3578
  }