@oneshot-agent/sdk 0.55.0 → 0.57.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
@@ -39,6 +39,7 @@ 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
41
  __exportStar(require("./solve"), exports);
42
+ __exportStar(require("./predict"), exports);
42
43
  __exportStar(require("./hunts"), exports);
43
44
  const physical_mail_1 = require("./physical-mail");
44
45
  __exportStar(require("./physical-mail"), exports);
@@ -66,11 +67,36 @@ const approvals_1 = require("./approvals");
66
67
  // stay off this public surface (see tests/unit/sdk-provider-cost-removed.test.ts).
67
68
  // Import from the `@oneshot-agent/sdk/receipt` subpath instead.
68
69
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
69
- const SDK_VERSION = '0.55.0';
70
+ const SDK_VERSION = '0.57.0';
70
71
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
71
72
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
72
73
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
73
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
+ }
74
100
  const BASE_URL = 'https://win.oneshotagent.com';
75
101
  const RPC_URL = 'https://mainnet.base.org';
76
102
  const CHAIN_ID = 8453;
@@ -201,7 +227,7 @@ class OneShot {
201
227
  constructor(config, walletProvider) {
202
228
  this.hunts = new hunts_1.Hunts(this);
203
229
  this.physicalMail = new physical_mail_1.PhysicalMail(async (path, method, body, mime) => {
204
- const response = await fetch(`${this.baseUrl}/v1/tools/physical-mail${path}`, {
230
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/physical-mail${path}`, {
205
231
  method, headers: { ...await this.signedReadHeaders(method === 'GET' ? 'read' : 'write'), 'Content-Type': mime ?? 'application/json' },
206
232
  ...(body === undefined ? {} : { body: mime ? body : JSON.stringify(body) }),
207
233
  signal: AbortSignal.timeout(60000),
@@ -215,6 +241,7 @@ class OneShot {
215
241
  /** Which paid calls run, are denied, or wait for approval. Wallet sessions write it. */
216
242
  this.policy = new approvals_1.ActionPolicies((path, method, body, scope) => this.agentJson(path, method, body, scope));
217
243
  this.readProofWarned = false;
244
+ this.proofClockOffsetSec = 0;
218
245
  /** ETH mode: signed payments not yet observed as settled, keyed by reservation id. */
219
246
  this._usdcPending = new Map();
220
247
  this._usdcReservationSeq = 0;
@@ -434,35 +461,35 @@ class OneShot {
434
461
  }
435
462
  /** List the caller's domain pool with warmup and rotation metadata. */
436
463
  async listDomains() {
437
- const response = await fetch(`${this.baseUrl}/v1/tools/email/domains`, {
464
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/email/domains`, {
438
465
  headers: await this.signedReadHeaders(),
439
466
  });
440
467
  if (!response.ok) {
441
- 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'));
442
469
  }
443
470
  return response.json();
444
471
  }
445
472
  /** Take a domain out of rotation without releasing it. */
446
473
  async pauseDomain(domain) {
447
474
  this.validate(domain, 'domain');
448
- 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`, {
449
476
  method: 'POST',
450
477
  headers: await this.signedReadHeaders('write'),
451
478
  });
452
479
  if (!response.ok) {
453
- 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'));
454
481
  }
455
482
  return response.json();
456
483
  }
457
484
  /** Put a paused domain back into rotation. */
458
485
  async resumeDomain(domain) {
459
486
  this.validate(domain, 'domain');
460
- 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`, {
461
488
  method: 'POST',
462
489
  headers: await this.signedReadHeaders('write'),
463
490
  });
464
491
  if (!response.ok) {
465
- 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'));
466
493
  }
467
494
  return response.json();
468
495
  }
@@ -470,6 +497,10 @@ class OneShot {
470
497
  this.validate(options.topic, 'topic');
471
498
  return this.tool('research', { ...options });
472
499
  }
500
+ async predict(input) {
501
+ const { memo, valueTag, decisionContext, ...options } = input;
502
+ return this.tool('predict', { ...options, timeout: options.timeout ?? 240 });
503
+ }
473
504
  /**
474
505
  * Solve a routing or packing problem with constraint checks and conflicts. Quotes, pays and waits for
475
506
  * the job. The result's `solution` is typed by the request's `problem`.
@@ -484,10 +515,15 @@ class OneShot {
484
515
  const { includePhone, ...payload } = options;
485
516
  return this.tool('find/person', { ...payload, include_phone: includePhone, limit: options.limit ?? 100 });
486
517
  }
487
- async personProfile(options) {
488
- if (!options.linkedin_url && !options.email && !options.name) {
489
- 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');
490
523
  }
524
+ }
525
+ async personProfile(options) {
526
+ this.requirePersonIdentity(options);
491
527
  const { includePhone, ...payload } = options;
492
528
  return this.tool('lookup/person-profile', { ...payload, include_phone: includePhone });
493
529
  }
@@ -547,10 +583,7 @@ class OneShot {
547
583
  });
548
584
  }
549
585
  async personEmail(options) {
550
- this.validate(options.company_domain, 'company_domain');
551
- if (!options.full_name && !(options.first_name && options.last_name)) {
552
- throw new errors_2.ValidationError('Either full_name or both first_name and last_name required', 'name');
553
- }
586
+ this.requirePersonIdentity(options);
554
587
  return this.tool('lookup/person-email', { ...options });
555
588
  }
556
589
  async personEmailStatus(options) {
@@ -558,17 +591,13 @@ class OneShot {
558
591
  return this.tool('lookup/person-email-status', { ...options });
559
592
  }
560
593
  async personDossier(options) {
561
- if (!options.email && !options.social_media_url && !options.name) {
562
- throw new errors_2.ValidationError('At least one of email, social_media_url, or name is required', 'identifier');
563
- }
594
+ this.requirePersonIdentity(options);
564
595
  const { includePhone, ...payload } = options;
565
596
  // The dossier takes 2 to 5 minutes; the generic 120 second poll gives up on a normal run (#1913).
566
597
  return this.tool('lookup/person-dossier', { ...payload, include_phone: includePhone, timeout: options.timeout ?? 300 });
567
598
  }
568
599
  async personSocialAccounts(options) {
569
- if (!options.email && !options.social_media_url) {
570
- throw new errors_2.ValidationError('At least one of email or social_media_url is required', 'identifier');
571
- }
600
+ this.requirePersonIdentity(options);
572
601
  return this.tool('lookup/person-social-accounts', { ...options });
573
602
  }
574
603
  async personArticles(options) {
@@ -577,17 +606,15 @@ class OneShot {
577
606
  return this.tool('lookup/person-articles', { ...options });
578
607
  }
579
608
  async personPosts(options) {
580
- this.validate(options.social_media_url, 'social_media_url');
609
+ this.requirePersonIdentity(options);
581
610
  return this.tool('lookup/person-posts', { ...options });
582
611
  }
583
612
  async personInterests(options) {
584
- if (!options.email && !options.phone && !options.social_media_url) {
585
- throw new errors_2.ValidationError('At least one of email, phone, or social_media_url is required', 'identifier');
586
- }
613
+ this.requirePersonIdentity(options);
587
614
  return this.tool('lookup/person-interests', { ...options });
588
615
  }
589
616
  async personFollowers(options) {
590
- this.validate(options.social_media_url, 'social_media_url');
617
+ this.requirePersonIdentity(options);
591
618
  return this.tool('lookup/person-followers', { ...options });
592
619
  }
593
620
  async inboxList(options = {}) {
@@ -596,24 +623,24 @@ class OneShot {
596
623
  limit: options.limit || undefined,
597
624
  include_body: options.include_body ? 'true' : undefined,
598
625
  });
599
- const response = await fetch(`${this.baseUrl}/v1/tools/inbox${qs ? `?${qs}` : ''}`, {
626
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/inbox${qs ? `?${qs}` : ''}`, {
600
627
  headers: await this.signedReadHeaders()
601
628
  });
602
629
  if (!response.ok) {
603
- 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'));
604
631
  }
605
632
  return response.json();
606
633
  }
607
634
  async inboxGet(emailId) {
608
635
  this.validate(emailId, 'emailId');
609
- const response = await fetch(`${this.baseUrl}/v1/tools/inbox/${emailId}`, {
636
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/inbox/${emailId}`, {
610
637
  headers: await this.signedReadHeaders()
611
638
  });
612
639
  if (response.status === 404) {
613
640
  throw new errors_2.ToolError('Email not found', 404, 'Email not found');
614
641
  }
615
642
  if (!response.ok) {
616
- 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'));
617
644
  }
618
645
  return response.json();
619
646
  }
@@ -693,6 +720,8 @@ class OneShot {
693
720
  };
694
721
  if (options.caller_persona)
695
722
  payload.caller_persona = options.caller_persona;
723
+ if (options.caller_name)
724
+ payload.caller_name = options.caller_name;
696
725
  if (options.context)
697
726
  payload.context = options.context;
698
727
  if (options.max_duration_minutes)
@@ -723,7 +752,9 @@ class OneShot {
723
752
  const result = await callResp.json();
724
753
  this.log(`Call initiated: ${result.request_id}`);
725
754
  if (options.wait !== false && result.request_id) {
726
- return this.pollJob(result.request_id, options.timeout ?? 300, options.signal, options.onStatusUpdate);
755
+ // Wait for the longest the call may run, plus two minutes to dial and settle.
756
+ const timeout = options.timeout ?? (options.max_duration_minutes ?? 10) * 60 + 120;
757
+ return this.pollJob(result.request_id, timeout, options.signal, options.onStatusUpdate);
727
758
  }
728
759
  return result;
729
760
  }
@@ -958,14 +989,14 @@ class OneShot {
958
989
  this.validate(name, 'name');
959
990
  if (options.cookies !== undefined && options.storage_state !== undefined)
960
991
  throw new errors_2.ValidationError('Provide cookies or storage_state, not both', 'options');
961
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
992
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles`, {
962
993
  method: 'POST',
963
994
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
964
995
  body: JSON.stringify({ name, ...options }),
965
996
  signal: AbortSignal.timeout(150000),
966
997
  });
967
998
  if (!response.ok) {
968
- throw new errors_2.ToolError('Failed to create browser profile', response.status, await response.text());
999
+ throw new errors_2.ToolError('Failed to create browser profile', response.status, await response.text(), response.headers.get('retry-after'));
969
1000
  }
970
1001
  return response.json();
971
1002
  }
@@ -994,12 +1025,12 @@ class OneShot {
994
1025
  }
995
1026
  async browserProfileSetup(profileId, action, body = {}) {
996
1027
  this.validate(profileId, 'profileId');
997
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles/${encodeURIComponent(profileId)}/setup/${action}`, {
1028
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles/${encodeURIComponent(profileId)}/setup/${action}`, {
998
1029
  method: 'POST', headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
999
1030
  body: JSON.stringify(body), signal: AbortSignal.timeout(150000),
1000
1031
  });
1001
1032
  if (!response.ok)
1002
- throw new errors_2.ToolError('Browser profile setup failed', response.status, '');
1033
+ throw new errors_2.ToolError('Browser profile setup failed', response.status, '', response.headers.get('retry-after'));
1003
1034
  return response.json();
1004
1035
  }
1005
1036
  /**
@@ -1014,11 +1045,11 @@ class OneShot {
1014
1045
  * ```
1015
1046
  */
1016
1047
  async listBrowserProfiles() {
1017
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
1048
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles`, {
1018
1049
  headers: await this.signedReadHeaders(),
1019
1050
  });
1020
1051
  if (!response.ok) {
1021
- throw new errors_2.ToolError('Failed to list browser profiles', response.status, await response.text());
1052
+ throw new errors_2.ToolError('Failed to list browser profiles', response.status, await response.text(), response.headers.get('retry-after'));
1022
1053
  }
1023
1054
  const data = await response.json();
1024
1055
  return data.profiles;
@@ -1033,12 +1064,12 @@ class OneShot {
1033
1064
  */
1034
1065
  async deleteBrowserProfile(profileId) {
1035
1066
  this.validate(profileId, 'profileId');
1036
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles/${profileId}`, {
1067
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles/${profileId}`, {
1037
1068
  method: 'DELETE',
1038
1069
  headers: await this.signedReadHeaders(),
1039
1070
  });
1040
1071
  if (!response.ok) {
1041
- throw new errors_2.ToolError('Failed to delete browser profile', response.status, await response.text());
1072
+ throw new errors_2.ToolError('Failed to delete browser profile', response.status, await response.text(), response.headers.get('retry-after'));
1042
1073
  }
1043
1074
  }
1044
1075
  async linkedinFree(method, path, opts) {
@@ -1046,7 +1077,7 @@ class OneShot {
1046
1077
  for (const [k, v] of Object.entries(opts.query ?? {}))
1047
1078
  if (v !== undefined)
1048
1079
  url.searchParams.set(k, v);
1049
- const response = await fetch(url.toString(), {
1080
+ const response = await this.fetchApi(url.toString(), {
1050
1081
  method,
1051
1082
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders(opts.scope ?? 'read')) },
1052
1083
  ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
@@ -1061,7 +1092,7 @@ class OneShot {
1061
1092
  catch { /* Non-JSON upstream error. */ }
1062
1093
  if (response.status === 409 && code === 'connect_required')
1063
1094
  throw new errors_1.LinkedInConnectRequiredError(body);
1064
- throw new errors_2.ToolError(`LinkedIn ${opts.what} failed`, response.status, body);
1095
+ throw new errors_2.ToolError(`LinkedIn ${opts.what} failed`, response.status, body, response.headers.get('retry-after'));
1065
1096
  }
1066
1097
  return response.json();
1067
1098
  }
@@ -1248,11 +1279,11 @@ class OneShot {
1248
1279
  limit: options.limit || undefined,
1249
1280
  from: options.from || undefined,
1250
1281
  });
1251
- const response = await fetch(`${this.baseUrl}/v1/tools/sms/inbox${qs ? `?${qs}` : ''}`, {
1282
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/sms/inbox${qs ? `?${qs}` : ''}`, {
1252
1283
  headers: await this.signedReadHeaders()
1253
1284
  });
1254
1285
  if (!response.ok) {
1255
- throw new errors_2.ToolError('Failed to list SMS inbox', response.status, await response.text());
1286
+ throw new errors_2.ToolError('Failed to list SMS inbox', response.status, await response.text(), response.headers.get('retry-after'));
1256
1287
  }
1257
1288
  return response.json();
1258
1289
  }
@@ -1394,7 +1425,7 @@ class OneShot {
1394
1425
  const scope = (0, deadline_1.deadlineScope)(toolOptions.signal, filled.startsWith('/v1/tools/browser/profiles') ? 150000 : 30000);
1395
1426
  try {
1396
1427
  const request = (async () => {
1397
- const response = await fetch(`${this.baseUrl}${target}`, {
1428
+ const response = await this.fetchApi(`${this.baseUrl}${target}`, {
1398
1429
  method,
1399
1430
  headers: { ...(sendsBody ? { 'Content-Type': 'application/json' } : {}), ...(await this.signedReadHeaders(method === 'GET' ? 'read' : 'write')) },
1400
1431
  ...(sendsBody ? { body: JSON.stringify(body ?? {}) } : {}),
@@ -1427,7 +1458,7 @@ class OneShot {
1427
1458
  }
1428
1459
  /** Signed free request returning the `data` field of `{ success, data }`. */
1429
1460
  async signedJson(method, path, body, scope, failure) {
1430
- const response = await fetch(`${this.baseUrl}${path}`, {
1461
+ const response = await this.fetchApi(`${this.baseUrl}${path}`, {
1431
1462
  method,
1432
1463
  headers: {
1433
1464
  ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
@@ -1436,7 +1467,7 @@ class OneShot {
1436
1467
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
1437
1468
  });
1438
1469
  if (!response.ok) {
1439
- throw new errors_2.ToolError(failure, response.status, await response.text());
1470
+ throw new errors_2.ToolError(failure, response.status, await response.text(), response.headers.get('retry-after'));
1440
1471
  }
1441
1472
  const json = await response.json();
1442
1473
  return (json.data ?? json);
@@ -1452,14 +1483,14 @@ class OneShot {
1452
1483
  */
1453
1484
  async smsInboxGet(messageId) {
1454
1485
  this.validate(messageId, 'messageId');
1455
- const response = await fetch(`${this.baseUrl}/v1/tools/sms/inbox/${messageId}`, {
1486
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/sms/inbox/${messageId}`, {
1456
1487
  headers: await this.signedReadHeaders()
1457
1488
  });
1458
1489
  if (response.status === 404) {
1459
1490
  throw new errors_2.ToolError('SMS message not found', 404, 'Message not found');
1460
1491
  }
1461
1492
  if (!response.ok) {
1462
- throw new errors_2.ToolError('Failed to get SMS message', response.status, await response.text());
1493
+ throw new errors_2.ToolError('Failed to get SMS message', response.status, await response.text(), response.headers.get('retry-after'));
1463
1494
  }
1464
1495
  return response.json();
1465
1496
  }
@@ -1480,11 +1511,11 @@ class OneShot {
1480
1511
  unread: options.unread ? 'true' : undefined,
1481
1512
  limit: options.limit || undefined,
1482
1513
  });
1483
- const response = await fetch(`${this.baseUrl}/v1/tools/notifications${qs ? `?${qs}` : ''}`, {
1514
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/notifications${qs ? `?${qs}` : ''}`, {
1484
1515
  headers: await this.signedReadHeaders()
1485
1516
  });
1486
1517
  if (!response.ok) {
1487
- throw new errors_2.ToolError('Failed to list notifications', response.status, await response.text());
1518
+ throw new errors_2.ToolError('Failed to list notifications', response.status, await response.text(), response.headers.get('retry-after'));
1488
1519
  }
1489
1520
  return response.json();
1490
1521
  }
@@ -1498,7 +1529,7 @@ class OneShot {
1498
1529
  */
1499
1530
  async markNotificationRead(notificationId) {
1500
1531
  this.validate(notificationId, 'notificationId');
1501
- const response = await fetch(`${this.baseUrl}/v1/tools/notifications/${notificationId}/read`, {
1532
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/notifications/${notificationId}/read`, {
1502
1533
  method: 'PATCH',
1503
1534
  headers: await this.signedReadHeaders('write')
1504
1535
  });
@@ -1506,15 +1537,15 @@ class OneShot {
1506
1537
  throw new errors_2.ToolError('Notification not found', 404, 'Notification not found');
1507
1538
  }
1508
1539
  if (!response.ok) {
1509
- throw new errors_2.ToolError('Failed to mark notification as read', response.status, await response.text());
1540
+ throw new errors_2.ToolError('Failed to mark notification as read', response.status, await response.text(), response.headers.get('retry-after'));
1510
1541
  }
1511
1542
  }
1512
1543
  async getUnifiedBalance() {
1513
- const response = await fetch(`${this.baseUrl}/v1/tools/balance`, {
1544
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/balance`, {
1514
1545
  headers: await this.signedReadHeaders()
1515
1546
  });
1516
1547
  if (!response.ok) {
1517
- throw new errors_2.ToolError('Failed to fetch balance', response.status, await response.text());
1548
+ throw new errors_2.ToolError('Failed to fetch balance', response.status, await response.text(), response.headers.get('retry-after'));
1518
1549
  }
1519
1550
  return response.json();
1520
1551
  }
@@ -1604,20 +1635,89 @@ class OneShot {
1604
1635
  * console.log(status.status, status.budget?.remaining);
1605
1636
  * ```
1606
1637
  */
1607
- async getComputeGoal(goalId) {
1638
+ async getComputeGoal(goalId, options = {}) {
1608
1639
  this.validate(goalId, 'goalId');
1609
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}`, {
1610
- headers: await this.signedReadHeaders()
1640
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}`, {
1641
+ headers: await this.signedReadHeaders(),
1642
+ signal: options.signal,
1611
1643
  });
1612
1644
  if (response.status === 404) {
1613
1645
  throw new errors_2.ToolError('Goal not found', 404, 'Goal not found');
1614
1646
  }
1615
1647
  if (!response.ok) {
1616
- throw new errors_2.ToolError('Failed to get compute goal', response.status, await response.text());
1648
+ // Keep the HTTP classification even if the error body is interrupted.
1649
+ const body = await response.text().catch(() => {
1650
+ options.signal?.throwIfAborted();
1651
+ return 'Response body unavailable';
1652
+ });
1653
+ throw new errors_2.ToolError('Failed to get compute goal', response.status, body, response.headers.get('retry-after'));
1617
1654
  }
1618
1655
  const json = await response.json();
1619
1656
  return json.data;
1620
1657
  }
1658
+ /**
1659
+ * Poll a compute goal using its goal_id, not its creation request_id.
1660
+ * Returns the complete goal snapshot on completed; failed/cancelled reject
1661
+ * with JobError (goal ID, end_reason code, snapshot in partialResult).
1662
+ * Budget exhaustion ends this wait; after fundComputeGoal revives the goal,
1663
+ * call waitForComputeGoal again with the same ID to observe the new run.
1664
+ * Paused/awaiting_confirmation and recurring goals keep waiting until the
1665
+ * goal ends or the timeout (seconds, default 120) expires with JobTimeoutError.
1666
+ * Infinity disables the local deadline.
1667
+ * onStatusUpdate receives each distinct observed status before settlement.
1668
+ * Callback throws/rejections are ignored and promises are not awaited.
1669
+ * Transient reads (5xx, 429, network failures) retry on the polling cadence.
1670
+ */
1671
+ async waitForComputeGoal(goalId, options = {}) {
1672
+ this.validate(goalId, 'goalId');
1673
+ const started = Date.now();
1674
+ const scope = (0, deadline_1.deadlineScope)(options.signal, (options.timeout ?? 120) * 1000);
1675
+ const poll = async () => {
1676
+ let lastStatus;
1677
+ let polls = 0;
1678
+ while (true) {
1679
+ scope.signal.throwIfAborted();
1680
+ let goal;
1681
+ try {
1682
+ goal = await this.getComputeGoal(goalId, { signal: scope.signal });
1683
+ }
1684
+ catch (error) {
1685
+ scope.signal.throwIfAborted();
1686
+ if (!isTransientComputeReadError(error))
1687
+ throw error;
1688
+ await this.sleep(HTTP_POLL_BACKOFF_MS[Math.min(polls++, HTTP_POLL_BACKOFF_MS.length - 1)], scope.signal);
1689
+ continue;
1690
+ }
1691
+ // A late response from a transport ignoring abort must not emit progress.
1692
+ scope.signal.throwIfAborted();
1693
+ if (goal.status !== lastStatus) {
1694
+ lastStatus = goal.status;
1695
+ try {
1696
+ Promise.resolve(options.onStatusUpdate?.(goal.status, goalId)).catch(() => { });
1697
+ }
1698
+ catch { /* Observers cannot change the goal outcome. */ }
1699
+ }
1700
+ if (goal.status === 'completed')
1701
+ return goal;
1702
+ if (goal.status === 'failed' || goal.status === 'cancelled') {
1703
+ const reason = goal.end_reason ?? goal.status;
1704
+ throw new errors_2.JobError(`Compute goal ${goal.status}: ${reason}`, goalId, reason, reason, { ...goal });
1705
+ }
1706
+ await this.sleep(HTTP_POLL_BACKOFF_MS[Math.min(polls++, HTTP_POLL_BACKOFF_MS.length - 1)], scope.signal);
1707
+ }
1708
+ };
1709
+ try {
1710
+ return await (0, deadline_1.abortable)(poll(), scope.signal);
1711
+ }
1712
+ catch (error) {
1713
+ if (scope.timedOut())
1714
+ throw new errors_2.JobTimeoutError(goalId, Date.now() - started);
1715
+ throw error;
1716
+ }
1717
+ finally {
1718
+ scope.close();
1719
+ }
1720
+ }
1621
1721
  /**
1622
1722
  * List tasks under a compute goal
1623
1723
  *
@@ -1631,11 +1731,11 @@ class OneShot {
1631
1731
  */
1632
1732
  async getComputeTasks(goalId) {
1633
1733
  this.validate(goalId, 'goalId');
1634
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/tasks`, {
1734
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/tasks`, {
1635
1735
  headers: await this.signedReadHeaders()
1636
1736
  });
1637
1737
  if (!response.ok) {
1638
- throw new errors_2.ToolError('Failed to get compute tasks', response.status, await response.text());
1738
+ throw new errors_2.ToolError('Failed to get compute tasks', response.status, await response.text(), response.headers.get('retry-after'));
1639
1739
  }
1640
1740
  const json = await response.json();
1641
1741
  return json.data;
@@ -1654,11 +1754,11 @@ class OneShot {
1654
1754
  */
1655
1755
  async getComputeOutputs(goalId) {
1656
1756
  this.validate(goalId, 'goalId');
1657
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/outputs`, {
1757
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/outputs`, {
1658
1758
  headers: await this.signedReadHeaders()
1659
1759
  });
1660
1760
  if (!response.ok) {
1661
- throw new errors_2.ToolError('Failed to list compute outputs', response.status, await response.text());
1761
+ throw new errors_2.ToolError('Failed to list compute outputs', response.status, await response.text(), response.headers.get('retry-after'));
1662
1762
  }
1663
1763
  const json = await response.json();
1664
1764
  return json.data;
@@ -1669,11 +1769,11 @@ class OneShot {
1669
1769
  this.validate(key, 'key');
1670
1770
  // The key is a path: keep its slashes, escape everything else.
1671
1771
  const path = key.split('/').map(encodeURIComponent).join('/');
1672
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/outputs/${path}`, {
1772
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/outputs/${path}`, {
1673
1773
  headers: await this.signedReadHeaders()
1674
1774
  });
1675
1775
  if (!response.ok) {
1676
- throw new errors_2.ToolError('Failed to read compute output', response.status, await response.text());
1776
+ throw new errors_2.ToolError('Failed to read compute output', response.status, await response.text(), response.headers.get('retry-after'));
1677
1777
  }
1678
1778
  const json = await response.json();
1679
1779
  return json.data;
@@ -1689,14 +1789,14 @@ class OneShot {
1689
1789
  */
1690
1790
  async getComputeBudget(goalId) {
1691
1791
  this.validate(goalId, 'goalId');
1692
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/budget`, {
1792
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/budget`, {
1693
1793
  headers: await this.signedReadHeaders()
1694
1794
  });
1695
1795
  if (response.status === 404) {
1696
1796
  throw new errors_2.ToolError('Budget not found', 404, 'Budget not found for this goal');
1697
1797
  }
1698
1798
  if (!response.ok) {
1699
- throw new errors_2.ToolError('Failed to get compute budget', response.status, await response.text());
1799
+ throw new errors_2.ToolError('Failed to get compute budget', response.status, await response.text(), response.headers.get('retry-after'));
1700
1800
  }
1701
1801
  const json = await response.json();
1702
1802
  return json.data;
@@ -1712,7 +1812,7 @@ class OneShot {
1712
1812
  */
1713
1813
  async cancelComputeGoal(goalId, reason) {
1714
1814
  this.validate(goalId, 'goalId');
1715
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/cancel`, {
1815
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/cancel`, {
1716
1816
  method: 'POST',
1717
1817
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1718
1818
  body: JSON.stringify({ reason })
@@ -1726,7 +1826,7 @@ class OneShot {
1726
1826
  return { goal_id: goalId, status: pending.status ?? 'pending_settlement', message: pending.message };
1727
1827
  }
1728
1828
  if (!response.ok) {
1729
- throw new errors_2.ToolError('Failed to cancel compute goal', response.status, await response.text());
1829
+ throw new errors_2.ToolError('Failed to cancel compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1730
1830
  }
1731
1831
  const json = await response.json();
1732
1832
  return json.data;
@@ -1746,16 +1846,16 @@ class OneShot {
1746
1846
  async respondToComputeTask(goalId, input) {
1747
1847
  this.validate(goalId, 'goalId');
1748
1848
  this.validate(input.task_id, 'task_id');
1749
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1849
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1750
1850
  method: 'POST',
1751
1851
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1752
1852
  body: JSON.stringify(input)
1753
1853
  });
1754
1854
  if (response.status === 404) {
1755
- throw new errors_2.ToolError('Goal or task not found', 404, await response.text());
1855
+ throw new errors_2.ToolError('Goal or task not found', 404, await response.text(), response.headers.get('retry-after'));
1756
1856
  }
1757
1857
  if (!response.ok) {
1758
- throw new errors_2.ToolError('Failed to respond to compute task', response.status, await response.text());
1858
+ throw new errors_2.ToolError('Failed to respond to compute task', response.status, await response.text(), response.headers.get('retry-after'));
1759
1859
  }
1760
1860
  const json = await response.json();
1761
1861
  return json.data;
@@ -1775,15 +1875,15 @@ class OneShot {
1775
1875
  */
1776
1876
  async confirmComputeGoal(goalId, input) {
1777
1877
  this.validate(goalId, 'goalId');
1778
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1878
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1779
1879
  method: 'POST',
1780
1880
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1781
1881
  body: JSON.stringify({ confirm_done: input.approved, ...(input.response ? { response: input.response } : {}) }),
1782
1882
  });
1783
1883
  if (response.status === 404)
1784
- throw new errors_2.ToolError('Goal not found', 404, await response.text());
1884
+ throw new errors_2.ToolError('Goal not found', 404, await response.text(), response.headers.get('retry-after'));
1785
1885
  if (!response.ok)
1786
- throw new errors_2.ToolError('Failed to confirm compute goal', response.status, await response.text());
1886
+ throw new errors_2.ToolError('Failed to confirm compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1787
1887
  const json = await response.json();
1788
1888
  return json.data;
1789
1889
  }
@@ -1797,13 +1897,13 @@ class OneShot {
1797
1897
  */
1798
1898
  async pauseComputeGoal(goalId, reason) {
1799
1899
  this.validate(goalId, 'goalId');
1800
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/pause`, {
1900
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/pause`, {
1801
1901
  method: 'POST',
1802
1902
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1803
1903
  body: JSON.stringify({ reason })
1804
1904
  });
1805
1905
  if (!response.ok) {
1806
- throw new errors_2.ToolError('Failed to pause compute goal', response.status, await response.text());
1906
+ throw new errors_2.ToolError('Failed to pause compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1807
1907
  }
1808
1908
  const json = await response.json();
1809
1909
  return json.data;
@@ -1819,13 +1919,13 @@ class OneShot {
1819
1919
  */
1820
1920
  async resumeComputeGoal(goalId) {
1821
1921
  this.validate(goalId, 'goalId');
1822
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/resume`, {
1922
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/resume`, {
1823
1923
  method: 'POST',
1824
1924
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1825
1925
  body: JSON.stringify({})
1826
1926
  });
1827
1927
  if (!response.ok) {
1828
- throw new errors_2.ToolError('Failed to resume compute goal', response.status, await response.text());
1928
+ throw new errors_2.ToolError('Failed to resume compute goal', response.status, await response.text(), response.headers.get('retry-after'));
1829
1929
  }
1830
1930
  const json = await response.json();
1831
1931
  return json.data;
@@ -1955,11 +2055,11 @@ class OneShot {
1955
2055
  */
1956
2056
  async spendBreakdown(options) {
1957
2057
  const qs = this.buildQuery({ period: options?.period || undefined });
1958
- const response = await fetch(`${this.baseUrl}/v1/analytics/spend/breakdown${qs ? `?${qs}` : ''}`, {
2058
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/spend/breakdown${qs ? `?${qs}` : ''}`, {
1959
2059
  headers: await this.signedReadHeaders()
1960
2060
  });
1961
2061
  if (!response.ok) {
1962
- throw new errors_2.ToolError('Failed to get spend breakdown', response.status, await response.text());
2062
+ throw new errors_2.ToolError('Failed to get spend breakdown', response.status, await response.text(), response.headers.get('retry-after'));
1963
2063
  }
1964
2064
  return response.json();
1965
2065
  }
@@ -1974,11 +2074,11 @@ class OneShot {
1974
2074
  */
1975
2075
  async rocs(options) {
1976
2076
  const qs = this.buildQuery({ period: options?.period || undefined });
1977
- const response = await fetch(`${this.baseUrl}/v1/analytics/rocs${qs ? `?${qs}` : ''}`, {
2077
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/rocs${qs ? `?${qs}` : ''}`, {
1978
2078
  headers: await this.signedReadHeaders()
1979
2079
  });
1980
2080
  if (!response.ok) {
1981
- throw new errors_2.ToolError('Failed to get RoCS', response.status, await response.text());
2081
+ throw new errors_2.ToolError('Failed to get RoCS', response.status, await response.text(), response.headers.get('retry-after'));
1982
2082
  }
1983
2083
  return response.json();
1984
2084
  }
@@ -2010,11 +2110,11 @@ class OneShot {
2010
2110
  since: toIso(options?.since),
2011
2111
  until: toIso(options?.until),
2012
2112
  });
2013
- const response = await fetch(`${this.baseUrl}/v1/analytics/receipts${qs ? `?${qs}` : ''}`, {
2113
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/receipts${qs ? `?${qs}` : ''}`, {
2014
2114
  headers: await this.signedReadHeaders()
2015
2115
  });
2016
2116
  if (!response.ok) {
2017
- throw new errors_2.ToolError('Failed to list receipts', response.status, await response.text());
2117
+ throw new errors_2.ToolError('Failed to list receipts', response.status, await response.text(), response.headers.get('retry-after'));
2018
2118
  }
2019
2119
  return response.json();
2020
2120
  }
@@ -2047,19 +2147,19 @@ class OneShot {
2047
2147
  this.validate(valueTag.type, 'valueTag.type');
2048
2148
  // Cadence-level: route to the outcome ledger by correlation key.
2049
2149
  if (typeof ref === 'object' && ref.goalId && !ref.receiptId && !ref.requestId) {
2050
- const response = await fetch(`${this.baseUrl}/v1/analytics/outcomes`, {
2150
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/outcomes`, {
2051
2151
  method: 'POST',
2052
2152
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2053
2153
  body: JSON.stringify({ goal_id: ref.goalId, ...valueTag }),
2054
2154
  });
2055
2155
  if (!response.ok) {
2056
- throw new errors_2.ToolError('Failed to record outcome value', response.status, await response.text());
2156
+ throw new errors_2.ToolError('Failed to record outcome value', response.status, await response.text(), response.headers.get('retry-after'));
2057
2157
  }
2058
2158
  return;
2059
2159
  }
2060
2160
  const id = typeof ref === 'string' ? ref : (ref.receiptId ?? ref.requestId);
2061
2161
  this.validate(id, 'receiptId/requestId/goalId');
2062
- const response = await fetch(`${this.baseUrl}/v1/analytics/receipts/${id}/value`, {
2162
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/receipts/${id}/value`, {
2063
2163
  method: 'PATCH',
2064
2164
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2065
2165
  body: JSON.stringify(valueTag)
@@ -2068,7 +2168,7 @@ class OneShot {
2068
2168
  throw new errors_2.ToolError('Receipt not found', 404, 'Receipt not found or not owned by this agent');
2069
2169
  }
2070
2170
  if (!response.ok) {
2071
- throw new errors_2.ToolError('Failed to tag receipt value', response.status, await response.text());
2171
+ throw new errors_2.ToolError('Failed to tag receipt value', response.status, await response.text(), response.headers.get('retry-after'));
2072
2172
  }
2073
2173
  }
2074
2174
  /**
@@ -2090,11 +2190,11 @@ class OneShot {
2090
2190
  */
2091
2191
  async rocsByGoal(options) {
2092
2192
  const qs = this.buildQuery({ period: options?.period || undefined, goal_id: options?.goalId || undefined });
2093
- const response = await fetch(`${this.baseUrl}/v1/analytics/rocs/by-goal${qs ? `?${qs}` : ''}`, {
2193
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/rocs/by-goal${qs ? `?${qs}` : ''}`, {
2094
2194
  headers: await this.signedReadHeaders()
2095
2195
  });
2096
2196
  if (!response.ok) {
2097
- throw new errors_2.ToolError('Failed to get RoCS by goal', response.status, await response.text());
2197
+ throw new errors_2.ToolError('Failed to get RoCS by goal', response.status, await response.text(), response.headers.get('retry-after'));
2098
2198
  }
2099
2199
  return response.json();
2100
2200
  }
@@ -2135,7 +2235,7 @@ class OneShot {
2135
2235
  const headers = await this.signedReadHeaders(`staging:${method}:${path}:${digest}`);
2136
2236
  if (!headers['x-agent-proof'])
2137
2237
  throw new Error('Could not sign staging application');
2138
- const response = await fetch(`${this.baseUrl}/v1/staging${path}`, {
2238
+ const response = await this.fetchApi(`${this.baseUrl}/v1/staging${path}`, {
2139
2239
  method, headers: { ...headers, 'Content-Type': 'application/json' },
2140
2240
  signal: AbortSignal.timeout(10000),
2141
2241
  ...(body ? { body: JSON.stringify(body) } : {}),
@@ -2170,7 +2270,7 @@ class OneShot {
2170
2270
  return base;
2171
2271
  try {
2172
2272
  const agent = this.provider.address;
2173
- const issuedAt = Math.floor(Date.now() / 1000);
2273
+ const issuedAt = Math.floor(Date.now() / 1000) + (domainName === 'OneShot Agent Auth' ? this.proofClockOffsetSec : 0);
2174
2274
  const nonce = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
2175
2275
  const signature = await this.provider.signTypedData(
2176
2276
  // `domainName` lets a different origin (e.g. Soul Hunt, which keeps
@@ -2205,6 +2305,45 @@ class OneShot {
2205
2305
  return base;
2206
2306
  }
2207
2307
  }
2308
+ /** Retry a clock-window refusal once, before the API has dispatched the request.
2309
+ * Only this API's proof clock changes; payments and other services keep their own clocks.
2310
+ * Callers supply replayable JSON bodies and retain their original abort/deadline signal.
2311
+ */
2312
+ async fetchApi(url, init) {
2313
+ const response = await fetch(url, init);
2314
+ const headers = new Headers(init.headers);
2315
+ const proof = headers.get('x-agent-proof');
2316
+ if (response.status !== 401 || !proof || this._accessToken)
2317
+ return response;
2318
+ const origin = new URL(this.baseUrl).origin;
2319
+ if (new URL(url).origin !== origin || (response.url && new URL(response.url).origin !== origin))
2320
+ return response;
2321
+ let scope;
2322
+ let serverTime;
2323
+ try {
2324
+ const body = await response.clone().json();
2325
+ if (body?.error !== 'proof_required' ||
2326
+ !['proof expired', 'proof issued in the future'].some(reason => body.message === `This endpoint requires a signed agent proof (x-agent-proof): ${reason}`) ||
2327
+ typeof body.server_time !== 'number' || !Number.isSafeInteger(body.server_time) || body.server_time <= 0)
2328
+ return response;
2329
+ const sent = JSON.parse(typeof Buffer !== 'undefined' ? Buffer.from(proof, 'base64').toString('utf-8') : atob(proof));
2330
+ if (typeof sent.scope !== 'string')
2331
+ return response;
2332
+ scope = sent.scope;
2333
+ serverTime = body.server_time;
2334
+ }
2335
+ catch {
2336
+ return response;
2337
+ }
2338
+ init.signal?.throwIfAborted();
2339
+ this.proofClockOffsetSec = serverTime - Math.floor(Date.now() / 1000);
2340
+ const refreshed = await this.signedReadHeaders(scope);
2341
+ init.signal?.throwIfAborted();
2342
+ if (!refreshed['x-agent-proof'])
2343
+ return response;
2344
+ headers.set('x-agent-proof', refreshed['x-agent-proof']);
2345
+ return fetch(url, { ...init, headers });
2346
+ }
2208
2347
  /** Build a query string, skipping null/undefined values. Pass falsy-but-valid
2209
2348
  * values (0, '') as undefined at the call site to match prior `if (x)` guards. */
2210
2349
  buildQuery(params) {
@@ -2248,7 +2387,7 @@ class OneShot {
2248
2387
  body.alert_email = this._alertEmail;
2249
2388
  let response;
2250
2389
  try {
2251
- response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
2390
+ response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/budgets`, {
2252
2391
  method: 'PUT',
2253
2392
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2254
2393
  body: JSON.stringify(body),
@@ -2259,7 +2398,7 @@ class OneShot {
2259
2398
  }
2260
2399
  if (!response.ok) {
2261
2400
  const text = await response.text();
2262
- throw new errors_2.BudgetSyncError(`Could not sync spend budget (${response.status}): ${text}`, response.status, text);
2401
+ throw new errors_2.BudgetSyncError(`Could not sync spend budget (${response.status}): ${text}`, response.status, text, response.headers.get('retry-after'));
2263
2402
  }
2264
2403
  this.log('Budget synced');
2265
2404
  })();
@@ -2301,7 +2440,7 @@ class OneShot {
2301
2440
  */
2302
2441
  /** Signed-proof JSON request to a free agent route; returns `data`. Errors map through failFromResponse. */
2303
2442
  async agentJson(path, method, body, scope = 'read') {
2304
- const response = await fetch(`${this.baseUrl}${path}`, {
2443
+ const response = await this.fetchApi(`${this.baseUrl}${path}`, {
2305
2444
  method,
2306
2445
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders(scope)) },
2307
2446
  ...(body === undefined || method === 'GET' ? {} : { body: JSON.stringify(body) }),
@@ -2313,11 +2452,11 @@ class OneShot {
2313
2452
  return (json && typeof json === 'object' && 'data' in json ? json.data : json);
2314
2453
  }
2315
2454
  async budgets() {
2316
- const response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
2455
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/budgets`, {
2317
2456
  headers: await this.signedReadHeaders(),
2318
2457
  });
2319
2458
  if (!response.ok) {
2320
- throw new errors_2.ToolError('Failed to fetch budgets', response.status, await response.text());
2459
+ throw new errors_2.ToolError('Failed to fetch budgets', response.status, await response.text(), response.headers.get('retry-after'));
2321
2460
  }
2322
2461
  const body = await response.json();
2323
2462
  return (body.data ?? body);
@@ -2340,24 +2479,24 @@ class OneShot {
2340
2479
  */
2341
2480
  async createAccessToken(opts = {}) {
2342
2481
  this.assertWalletSession('mint access tokens');
2343
- const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens`, {
2482
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens`, {
2344
2483
  method: 'POST',
2345
2484
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2346
2485
  body: JSON.stringify(opts.name !== undefined ? { name: opts.name } : {}),
2347
2486
  });
2348
2487
  if (!response.ok)
2349
- throw new errors_2.ToolError('Failed to create access token', response.status, await response.text());
2488
+ throw new errors_2.ToolError('Failed to create access token', response.status, await response.text(), response.headers.get('retry-after'));
2350
2489
  const body = await response.json();
2351
2490
  return body.data;
2352
2491
  }
2353
2492
  /** List this agent's access tokens (prefix, label, last use, spend today — never the token). */
2354
2493
  async listAccessTokens(opts = {}) {
2355
2494
  this.assertWalletSession('list access tokens');
2356
- const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
2495
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens${opts.activeOnly ? '?active=true' : ''}`, {
2357
2496
  headers: await this.signedReadHeaders(),
2358
2497
  });
2359
2498
  if (!response.ok)
2360
- throw new errors_2.ToolError('Failed to list access tokens', response.status, await response.text());
2499
+ throw new errors_2.ToolError('Failed to list access tokens', response.status, await response.text(), response.headers.get('retry-after'));
2361
2500
  const body = await response.json();
2362
2501
  return body.data.tokens;
2363
2502
  }
@@ -2365,12 +2504,12 @@ class OneShot {
2365
2504
  async revokeAccessToken(id) {
2366
2505
  this.assertWalletSession('revoke access tokens');
2367
2506
  this.validate(id, 'id');
2368
- const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens/${encodeURIComponent(id)}`, {
2507
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens/${encodeURIComponent(id)}`, {
2369
2508
  method: 'DELETE',
2370
2509
  headers: await this.signedReadHeaders('write'),
2371
2510
  });
2372
2511
  if (!response.ok)
2373
- throw new errors_2.ToolError('Failed to revoke access token', response.status, await response.text());
2512
+ throw new errors_2.ToolError('Failed to revoke access token', response.status, await response.text(), response.headers.get('retry-after'));
2374
2513
  const body = await response.json();
2375
2514
  return body.data;
2376
2515
  }
@@ -2429,7 +2568,7 @@ class OneShot {
2429
2568
  const budget = response.status === 403 ? this.parseBudgetRejection(text) : undefined;
2430
2569
  if (budget)
2431
2570
  throw budget;
2432
- throw new errors_2.ToolError(message, response.status, text);
2571
+ throw new errors_2.ToolError(message, response.status, text, response.headers.get('retry-after'));
2433
2572
  }
2434
2573
  /**
2435
2574
  * Map a 402 `insufficient_credits` body (access-token sessions) onto a typed
@@ -2493,12 +2632,12 @@ class OneShot {
2493
2632
  throw new errors_2.ToolError(`Could not resolve the access token's agent (network): ${err}`, 0, String(err));
2494
2633
  }
2495
2634
  if (!response.ok) {
2496
- throw new errors_2.ToolError('Could not resolve the access token\'s agent', response.status, await response.text());
2635
+ throw new errors_2.ToolError('Could not resolve the access token\'s agent', response.status, await response.text(), response.headers.get('retry-after'));
2497
2636
  }
2498
2637
  const json = await response.json();
2499
2638
  const address = json.data?.wallet_address;
2500
2639
  if (!address)
2501
- throw new errors_2.ToolError('GET /v1/agents/me returned no wallet_address', response.status, JSON.stringify(json));
2640
+ throw new errors_2.ToolError('GET /v1/agents/me returned no wallet_address', response.status, JSON.stringify(json), response.headers.get('retry-after'));
2502
2641
  return address;
2503
2642
  }
2504
2643
  /**
@@ -2601,7 +2740,7 @@ class OneShot {
2601
2740
  const headers = signed ? await this.signedReadHeaders() : undefined;
2602
2741
  if (scope.signal.aborted)
2603
2742
  throw new errors_2.OneShotError('Read deadline exceeded');
2604
- const response = await fetch(`${this.baseUrl}${path}`, { headers, signal: scope.signal });
2743
+ const response = await this.fetchApi(`${this.baseUrl}${path}`, { headers, signal: scope.signal });
2605
2744
  if (!response.ok && !(allowDegraded && response.status === 503))
2606
2745
  await this.failFromResponse('Reliability read failed', response);
2607
2746
  return await response.json();
@@ -2620,7 +2759,7 @@ class OneShot {
2620
2759
  }
2621
2760
  async executeToolRequest(endpoint, options, quoteId) {
2622
2761
  // LinkedIn writes act through a human's account and cannot be recalled: always keyed.
2623
- const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|solve|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2762
+ const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2624
2763
  const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
2625
2764
  if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
2626
2765
  throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
@@ -2663,7 +2802,7 @@ class OneShot {
2663
2802
  this.log('Memo truncated to 1000 chars');
2664
2803
  }
2665
2804
  }
2666
- else if (!endpoint.endsWith('/v1/tools/solve') && !endpoint.includes('/inbox') && !endpoint.includes('/notifications') && !endpoint.includes('/balance')) {
2805
+ else if (!endpoint.endsWith('/v1/tools/solve') && !endpoint.endsWith('/v1/tools/predict') && !endpoint.includes('/inbox') && !endpoint.includes('/notifications') && !endpoint.includes('/balance')) {
2667
2806
  this.log('No memo provided — consider adding a reason for audit trail');
2668
2807
  }
2669
2808
  if (payload.decisionContext !== undefined) {
@@ -2692,7 +2831,7 @@ class OneShot {
2692
2831
  context.phase = 'submission';
2693
2832
  let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
2694
2833
  // Handle 402 Payment Required
2695
- if (response.status === 402 && /\/v1\/tools\/(solve|find\/person|lookup\/person-(profile|dossier))$/.test(endpoint)) {
2834
+ if (response.status === 402 && /\/v1\/tools\/(solve|predict|find\/person|lookup\/person-(profile|dossier))$/.test(endpoint)) {
2696
2835
  ({ execResp: response } = await this.runQuoteToPay({
2697
2836
  endpoint, payload, signal, maxCost, quoteResponse: response, extraHeaders,
2698
2837
  expectMsg: 'Expected a tool quote', totalOf: quote => quote.total,
@@ -2813,6 +2952,15 @@ class OneShot {
2813
2952
  * `wait: false`, or to resume waiting after a client restart. Delivery is the
2814
2953
  * same as for blocking calls: WebSocket push when the server offers it, HTTP
2815
2954
  * polling of `GET /v1/requests/:id` as the source of truth.
2955
+ * For compute goals, use `waitForComputeGoal(goal_id)` instead: their creation
2956
+ * request_id does not track progress. `onStatusUpdate` observes distinct job
2957
+ * statuses, including the terminal status before settlement.
2958
+ * With waitForPhones, completed is deferred until the phone wait returns.
2959
+ * Callback errors are ignored; returned promises are not awaited.
2960
+ *
2961
+ * `signal` ends the local wait only (in-flight fetch aborted, poll timer
2962
+ * cleared, rejects with `OneShotError('Operation cancelled')`); the job
2963
+ * itself keeps running and can be waited for again.
2816
2964
  */
2817
2965
  async waitForResult(requestId, options = {}) {
2818
2966
  this.validate(requestId, 'requestId');
@@ -2825,6 +2973,9 @@ class OneShot {
2825
2973
  // swallows frames) costs nothing beyond the poll cadence. Previously the
2826
2974
  // client waited 60% of its timeout on the socket before polling at all —
2827
2975
  // in prod that was ~72s of dead air on every call while the push never came.
2976
+ // An already-aborted signal ends the wait before any proof is signed or sent.
2977
+ if (signal?.aborted)
2978
+ throw new errors_2.OneShotError('Operation cancelled');
2828
2979
  const inner = new AbortController();
2829
2980
  const onOuterAbort = () => inner.abort();
2830
2981
  if (signal?.aborted)
@@ -2832,12 +2983,24 @@ class OneShot {
2832
2983
  else
2833
2984
  signal?.addEventListener('abort', onOuterAbort, { once: true });
2834
2985
  const wait = { pushConfirmed: false, lastStatus: undefined, via: undefined };
2835
- // Two sources now report status; only surface changes to the caller.
2986
+ const notify = (status) => {
2987
+ // Progress observers must not alter job outcomes or trigger HTTP retries.
2988
+ try {
2989
+ Promise.resolve(onStatusUpdate?.(status, requestId)).catch(() => { });
2990
+ }
2991
+ catch { /* Ignore synchronous observer errors too. */ }
2992
+ };
2993
+ // Latch terminal transport status immediately, but defer completion for
2994
+ // callers waiting on phones so the losing branch cannot emit stale states.
2836
2995
  const emit = (status) => {
2996
+ if (wait.lastStatus === 'completed' || wait.lastStatus === 'failed')
2997
+ return;
2837
2998
  if (status === wait.lastStatus)
2838
2999
  return;
2839
3000
  wait.lastStatus = status;
2840
- onStatusUpdate?.(status, requestId);
3001
+ if (status === 'completed' && phoneOpts?.waitForPhones)
3002
+ return;
3003
+ notify(status);
2841
3004
  };
2842
3005
  const startedAt = Date.now();
2843
3006
  const wsBranch = this.waitViaWebSocket(requestId, inner.signal, emit, wait).catch((err) => {
@@ -2873,8 +3036,10 @@ class OneShot {
2873
3036
  // hit a transient error before any successful refresh, we return what we
2874
3037
  // already have (with phones_pending=true still set so consumers know).
2875
3038
  if (phoneOpts?.waitForPhones && this._isPhonesPending(result)) {
2876
- return this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
3039
+ result = await this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
2877
3040
  }
3041
+ if (phoneOpts?.waitForPhones)
3042
+ notify('completed');
2878
3043
  return result;
2879
3044
  }
2880
3045
  /**
@@ -2918,7 +3083,7 @@ class OneShot {
2918
3083
  if (signal?.aborted)
2919
3084
  throw new errors_2.OneShotError('Operation cancelled');
2920
3085
  try {
2921
- const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
3086
+ const resp = await this.fetchApi(`${this.baseUrl}/v1/requests/${requestId}`, {
2922
3087
  headers: await this.signedReadHeaders(),
2923
3088
  signal,
2924
3089
  });
@@ -2927,7 +3092,7 @@ class OneShot {
2927
3092
  // whatever we last had so consumers don't lose the sync result.
2928
3093
  if (lastResult !== undefined)
2929
3094
  return lastResult;
2930
- throw new errors_2.ToolError('Failed to check job status', resp.status, await resp.text());
3095
+ throw new errors_2.ToolError('Failed to check job status', resp.status, await resp.text(), resp.headers.get('retry-after'));
2931
3096
  }
2932
3097
  const job = await resp.json();
2933
3098
  lastResult = (job.result ?? job);
@@ -2936,6 +3101,8 @@ class OneShot {
2936
3101
  }
2937
3102
  }
2938
3103
  catch (err) {
3104
+ if (signal?.aborted)
3105
+ throw new errors_2.OneShotError('Operation cancelled');
2939
3106
  if (err instanceof errors_2.OneShotError)
2940
3107
  throw err;
2941
3108
  if (lastResult !== undefined)
@@ -3031,6 +3198,7 @@ class OneShot {
3031
3198
  wait.pushConfirmed = true;
3032
3199
  this.log('WebSocket push confirmed — relaxing HTTP polling');
3033
3200
  }
3201
+ emit(String(msg.status));
3034
3202
  if (msg.status === 'completed') {
3035
3203
  this.log('Job completed (WebSocket)');
3036
3204
  settle(() => {
@@ -3050,9 +3218,6 @@ class OneShot {
3050
3218
  reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code, msg.result));
3051
3219
  });
3052
3220
  }
3053
- else {
3054
- emit(String(msg.status));
3055
- }
3056
3221
  }
3057
3222
  catch {
3058
3223
  // Ignore malformed messages
@@ -3088,6 +3253,9 @@ class OneShot {
3088
3253
  catch (error) {
3089
3254
  if (scope.timedOut())
3090
3255
  throw new errors_2.JobTimeoutError(requestId, Date.now() - started);
3256
+ // An abort mid-fetch surfaces as the transport's AbortError; report it as the cancellation it is.
3257
+ if (signal?.aborted && !(error instanceof errors_2.OneShotError))
3258
+ throw new errors_2.OneShotError('Operation cancelled');
3091
3259
  throw error;
3092
3260
  }
3093
3261
  finally {
@@ -3104,7 +3272,7 @@ class OneShot {
3104
3272
  if (signal?.aborted)
3105
3273
  throw new errors_2.OneShotError('Operation cancelled');
3106
3274
  try {
3107
- const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
3275
+ const resp = await this.fetchApi(`${this.baseUrl}/v1/requests/${requestId}`, {
3108
3276
  headers: await this.signedReadHeaders(),
3109
3277
  signal
3110
3278
  });
@@ -3115,9 +3283,10 @@ class OneShot {
3115
3283
  if (resp.status >= 500 || resp.status === 429) {
3116
3284
  throw new Error(`Poll returned ${resp.status}: ${body.slice(0, 200)}`);
3117
3285
  }
3118
- throw new errors_2.ToolError('Failed to check job status', resp.status, body);
3286
+ throw new errors_2.ToolError('Failed to check job status', resp.status, body, resp.headers.get('retry-after'));
3119
3287
  }
3120
3288
  const job = await resp.json();
3289
+ emit?.(String(job.status));
3121
3290
  if (job.status === 'completed') {
3122
3291
  this.log('Job completed');
3123
3292
  if (wait)
@@ -3140,7 +3309,6 @@ class OneShot {
3140
3309
  wait.via = 'http';
3141
3310
  throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code, job.result);
3142
3311
  }
3143
- emit?.(String(job.status));
3144
3312
  retries = 0;
3145
3313
  const interval = wait?.pushConfirmed
3146
3314
  ? HTTP_POLL_RELAXED_MS
@@ -3223,7 +3391,7 @@ class OneShot {
3223
3391
  // alone (401 proof_required).
3224
3392
  const unsignedStub = payment?.payload?.signature === '0x';
3225
3393
  const askedForProof = !!extraHeaders?.['x-agent-proof'];
3226
- const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve)$/.test(endpoint)
3394
+ const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict)$/.test(endpoint)
3227
3395
  ? 'submit'
3228
3396
  : askedForProof ? 'read' : 'write';
3229
3397
  const headers = {
@@ -3251,7 +3419,7 @@ class OneShot {
3251
3419
  transportDeadline?.signal.addEventListener('abort', () => transportDeadline.close(), { once: true });
3252
3420
  if (fetchSignal?.aborted)
3253
3421
  throw new errors_2.OneShotError('Operation cancelled');
3254
- const work = fetch(`${this.baseUrl}${endpoint}`, {
3422
+ const work = this.fetchApi(`${this.baseUrl}${endpoint}`, {
3255
3423
  method: 'POST', headers, body: JSON.stringify(data), signal: fetchSignal,
3256
3424
  });
3257
3425
  try {
@@ -3408,7 +3576,7 @@ class OneShot {
3408
3576
  }
3409
3577
  catch (err) {
3410
3578
  // Keyed writes may already be queued server-side after a transport failure: keep the reservation.
3411
- if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3579
+ 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))
3412
3580
  this.releaseUsdcReservation(signed.reservation);
3413
3581
  throw err;
3414
3582
  }