@oneshot-agent/sdk 0.55.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
@@ -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.56.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
  }
@@ -958,14 +985,14 @@ class OneShot {
958
985
  this.validate(name, 'name');
959
986
  if (options.cookies !== undefined && options.storage_state !== undefined)
960
987
  throw new errors_2.ValidationError('Provide cookies or storage_state, not both', 'options');
961
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
988
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles`, {
962
989
  method: 'POST',
963
990
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
964
991
  body: JSON.stringify({ name, ...options }),
965
992
  signal: AbortSignal.timeout(150000),
966
993
  });
967
994
  if (!response.ok) {
968
- 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'));
969
996
  }
970
997
  return response.json();
971
998
  }
@@ -994,12 +1021,12 @@ class OneShot {
994
1021
  }
995
1022
  async browserProfileSetup(profileId, action, body = {}) {
996
1023
  this.validate(profileId, 'profileId');
997
- 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}`, {
998
1025
  method: 'POST', headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders()) },
999
1026
  body: JSON.stringify(body), signal: AbortSignal.timeout(150000),
1000
1027
  });
1001
1028
  if (!response.ok)
1002
- 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'));
1003
1030
  return response.json();
1004
1031
  }
1005
1032
  /**
@@ -1014,11 +1041,11 @@ class OneShot {
1014
1041
  * ```
1015
1042
  */
1016
1043
  async listBrowserProfiles() {
1017
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles`, {
1044
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles`, {
1018
1045
  headers: await this.signedReadHeaders(),
1019
1046
  });
1020
1047
  if (!response.ok) {
1021
- 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'));
1022
1049
  }
1023
1050
  const data = await response.json();
1024
1051
  return data.profiles;
@@ -1033,12 +1060,12 @@ class OneShot {
1033
1060
  */
1034
1061
  async deleteBrowserProfile(profileId) {
1035
1062
  this.validate(profileId, 'profileId');
1036
- const response = await fetch(`${this.baseUrl}/v1/tools/browser/profiles/${profileId}`, {
1063
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/browser/profiles/${profileId}`, {
1037
1064
  method: 'DELETE',
1038
1065
  headers: await this.signedReadHeaders(),
1039
1066
  });
1040
1067
  if (!response.ok) {
1041
- 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'));
1042
1069
  }
1043
1070
  }
1044
1071
  async linkedinFree(method, path, opts) {
@@ -1046,7 +1073,7 @@ class OneShot {
1046
1073
  for (const [k, v] of Object.entries(opts.query ?? {}))
1047
1074
  if (v !== undefined)
1048
1075
  url.searchParams.set(k, v);
1049
- const response = await fetch(url.toString(), {
1076
+ const response = await this.fetchApi(url.toString(), {
1050
1077
  method,
1051
1078
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders(opts.scope ?? 'read')) },
1052
1079
  ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
@@ -1061,7 +1088,7 @@ class OneShot {
1061
1088
  catch { /* Non-JSON upstream error. */ }
1062
1089
  if (response.status === 409 && code === 'connect_required')
1063
1090
  throw new errors_1.LinkedInConnectRequiredError(body);
1064
- 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'));
1065
1092
  }
1066
1093
  return response.json();
1067
1094
  }
@@ -1248,11 +1275,11 @@ class OneShot {
1248
1275
  limit: options.limit || undefined,
1249
1276
  from: options.from || undefined,
1250
1277
  });
1251
- 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}` : ''}`, {
1252
1279
  headers: await this.signedReadHeaders()
1253
1280
  });
1254
1281
  if (!response.ok) {
1255
- 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'));
1256
1283
  }
1257
1284
  return response.json();
1258
1285
  }
@@ -1394,7 +1421,7 @@ class OneShot {
1394
1421
  const scope = (0, deadline_1.deadlineScope)(toolOptions.signal, filled.startsWith('/v1/tools/browser/profiles') ? 150000 : 30000);
1395
1422
  try {
1396
1423
  const request = (async () => {
1397
- const response = await fetch(`${this.baseUrl}${target}`, {
1424
+ const response = await this.fetchApi(`${this.baseUrl}${target}`, {
1398
1425
  method,
1399
1426
  headers: { ...(sendsBody ? { 'Content-Type': 'application/json' } : {}), ...(await this.signedReadHeaders(method === 'GET' ? 'read' : 'write')) },
1400
1427
  ...(sendsBody ? { body: JSON.stringify(body ?? {}) } : {}),
@@ -1427,7 +1454,7 @@ class OneShot {
1427
1454
  }
1428
1455
  /** Signed free request returning the `data` field of `{ success, data }`. */
1429
1456
  async signedJson(method, path, body, scope, failure) {
1430
- const response = await fetch(`${this.baseUrl}${path}`, {
1457
+ const response = await this.fetchApi(`${this.baseUrl}${path}`, {
1431
1458
  method,
1432
1459
  headers: {
1433
1460
  ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
@@ -1436,7 +1463,7 @@ class OneShot {
1436
1463
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
1437
1464
  });
1438
1465
  if (!response.ok) {
1439
- 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'));
1440
1467
  }
1441
1468
  const json = await response.json();
1442
1469
  return (json.data ?? json);
@@ -1452,14 +1479,14 @@ class OneShot {
1452
1479
  */
1453
1480
  async smsInboxGet(messageId) {
1454
1481
  this.validate(messageId, 'messageId');
1455
- const response = await fetch(`${this.baseUrl}/v1/tools/sms/inbox/${messageId}`, {
1482
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/sms/inbox/${messageId}`, {
1456
1483
  headers: await this.signedReadHeaders()
1457
1484
  });
1458
1485
  if (response.status === 404) {
1459
1486
  throw new errors_2.ToolError('SMS message not found', 404, 'Message not found');
1460
1487
  }
1461
1488
  if (!response.ok) {
1462
- 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'));
1463
1490
  }
1464
1491
  return response.json();
1465
1492
  }
@@ -1480,11 +1507,11 @@ class OneShot {
1480
1507
  unread: options.unread ? 'true' : undefined,
1481
1508
  limit: options.limit || undefined,
1482
1509
  });
1483
- const response = await fetch(`${this.baseUrl}/v1/tools/notifications${qs ? `?${qs}` : ''}`, {
1510
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/notifications${qs ? `?${qs}` : ''}`, {
1484
1511
  headers: await this.signedReadHeaders()
1485
1512
  });
1486
1513
  if (!response.ok) {
1487
- 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'));
1488
1515
  }
1489
1516
  return response.json();
1490
1517
  }
@@ -1498,7 +1525,7 @@ class OneShot {
1498
1525
  */
1499
1526
  async markNotificationRead(notificationId) {
1500
1527
  this.validate(notificationId, 'notificationId');
1501
- const response = await fetch(`${this.baseUrl}/v1/tools/notifications/${notificationId}/read`, {
1528
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/notifications/${notificationId}/read`, {
1502
1529
  method: 'PATCH',
1503
1530
  headers: await this.signedReadHeaders('write')
1504
1531
  });
@@ -1506,15 +1533,15 @@ class OneShot {
1506
1533
  throw new errors_2.ToolError('Notification not found', 404, 'Notification not found');
1507
1534
  }
1508
1535
  if (!response.ok) {
1509
- 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'));
1510
1537
  }
1511
1538
  }
1512
1539
  async getUnifiedBalance() {
1513
- const response = await fetch(`${this.baseUrl}/v1/tools/balance`, {
1540
+ const response = await this.fetchApi(`${this.baseUrl}/v1/tools/balance`, {
1514
1541
  headers: await this.signedReadHeaders()
1515
1542
  });
1516
1543
  if (!response.ok) {
1517
- 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'));
1518
1545
  }
1519
1546
  return response.json();
1520
1547
  }
@@ -1604,20 +1631,89 @@ class OneShot {
1604
1631
  * console.log(status.status, status.budget?.remaining);
1605
1632
  * ```
1606
1633
  */
1607
- async getComputeGoal(goalId) {
1634
+ async getComputeGoal(goalId, options = {}) {
1608
1635
  this.validate(goalId, 'goalId');
1609
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}`, {
1610
- headers: await this.signedReadHeaders()
1636
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}`, {
1637
+ headers: await this.signedReadHeaders(),
1638
+ signal: options.signal,
1611
1639
  });
1612
1640
  if (response.status === 404) {
1613
1641
  throw new errors_2.ToolError('Goal not found', 404, 'Goal not found');
1614
1642
  }
1615
1643
  if (!response.ok) {
1616
- 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'));
1617
1650
  }
1618
1651
  const json = await response.json();
1619
1652
  return json.data;
1620
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
+ }
1621
1717
  /**
1622
1718
  * List tasks under a compute goal
1623
1719
  *
@@ -1631,11 +1727,11 @@ class OneShot {
1631
1727
  */
1632
1728
  async getComputeTasks(goalId) {
1633
1729
  this.validate(goalId, 'goalId');
1634
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/tasks`, {
1730
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/tasks`, {
1635
1731
  headers: await this.signedReadHeaders()
1636
1732
  });
1637
1733
  if (!response.ok) {
1638
- 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'));
1639
1735
  }
1640
1736
  const json = await response.json();
1641
1737
  return json.data;
@@ -1654,11 +1750,11 @@ class OneShot {
1654
1750
  */
1655
1751
  async getComputeOutputs(goalId) {
1656
1752
  this.validate(goalId, 'goalId');
1657
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/outputs`, {
1753
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/outputs`, {
1658
1754
  headers: await this.signedReadHeaders()
1659
1755
  });
1660
1756
  if (!response.ok) {
1661
- 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'));
1662
1758
  }
1663
1759
  const json = await response.json();
1664
1760
  return json.data;
@@ -1669,11 +1765,11 @@ class OneShot {
1669
1765
  this.validate(key, 'key');
1670
1766
  // The key is a path: keep its slashes, escape everything else.
1671
1767
  const path = key.split('/').map(encodeURIComponent).join('/');
1672
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/outputs/${path}`, {
1768
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/outputs/${path}`, {
1673
1769
  headers: await this.signedReadHeaders()
1674
1770
  });
1675
1771
  if (!response.ok) {
1676
- 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'));
1677
1773
  }
1678
1774
  const json = await response.json();
1679
1775
  return json.data;
@@ -1689,14 +1785,14 @@ class OneShot {
1689
1785
  */
1690
1786
  async getComputeBudget(goalId) {
1691
1787
  this.validate(goalId, 'goalId');
1692
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/budget`, {
1788
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/budget`, {
1693
1789
  headers: await this.signedReadHeaders()
1694
1790
  });
1695
1791
  if (response.status === 404) {
1696
1792
  throw new errors_2.ToolError('Budget not found', 404, 'Budget not found for this goal');
1697
1793
  }
1698
1794
  if (!response.ok) {
1699
- 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'));
1700
1796
  }
1701
1797
  const json = await response.json();
1702
1798
  return json.data;
@@ -1712,7 +1808,7 @@ class OneShot {
1712
1808
  */
1713
1809
  async cancelComputeGoal(goalId, reason) {
1714
1810
  this.validate(goalId, 'goalId');
1715
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/cancel`, {
1811
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/cancel`, {
1716
1812
  method: 'POST',
1717
1813
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1718
1814
  body: JSON.stringify({ reason })
@@ -1726,7 +1822,7 @@ class OneShot {
1726
1822
  return { goal_id: goalId, status: pending.status ?? 'pending_settlement', message: pending.message };
1727
1823
  }
1728
1824
  if (!response.ok) {
1729
- 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'));
1730
1826
  }
1731
1827
  const json = await response.json();
1732
1828
  return json.data;
@@ -1746,16 +1842,16 @@ class OneShot {
1746
1842
  async respondToComputeTask(goalId, input) {
1747
1843
  this.validate(goalId, 'goalId');
1748
1844
  this.validate(input.task_id, 'task_id');
1749
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1845
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1750
1846
  method: 'POST',
1751
1847
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1752
1848
  body: JSON.stringify(input)
1753
1849
  });
1754
1850
  if (response.status === 404) {
1755
- 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'));
1756
1852
  }
1757
1853
  if (!response.ok) {
1758
- 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'));
1759
1855
  }
1760
1856
  const json = await response.json();
1761
1857
  return json.data;
@@ -1775,15 +1871,15 @@ class OneShot {
1775
1871
  */
1776
1872
  async confirmComputeGoal(goalId, input) {
1777
1873
  this.validate(goalId, 'goalId');
1778
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1874
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
1779
1875
  method: 'POST',
1780
1876
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1781
1877
  body: JSON.stringify({ confirm_done: input.approved, ...(input.response ? { response: input.response } : {}) }),
1782
1878
  });
1783
1879
  if (response.status === 404)
1784
- 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'));
1785
1881
  if (!response.ok)
1786
- 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'));
1787
1883
  const json = await response.json();
1788
1884
  return json.data;
1789
1885
  }
@@ -1797,13 +1893,13 @@ class OneShot {
1797
1893
  */
1798
1894
  async pauseComputeGoal(goalId, reason) {
1799
1895
  this.validate(goalId, 'goalId');
1800
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/pause`, {
1896
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/pause`, {
1801
1897
  method: 'POST',
1802
1898
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1803
1899
  body: JSON.stringify({ reason })
1804
1900
  });
1805
1901
  if (!response.ok) {
1806
- 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'));
1807
1903
  }
1808
1904
  const json = await response.json();
1809
1905
  return json.data;
@@ -1819,13 +1915,13 @@ class OneShot {
1819
1915
  */
1820
1916
  async resumeComputeGoal(goalId) {
1821
1917
  this.validate(goalId, 'goalId');
1822
- const response = await fetch(`${this.baseUrl}/v1/compute/${goalId}/resume`, {
1918
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/resume`, {
1823
1919
  method: 'POST',
1824
1920
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
1825
1921
  body: JSON.stringify({})
1826
1922
  });
1827
1923
  if (!response.ok) {
1828
- 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'));
1829
1925
  }
1830
1926
  const json = await response.json();
1831
1927
  return json.data;
@@ -1955,11 +2051,11 @@ class OneShot {
1955
2051
  */
1956
2052
  async spendBreakdown(options) {
1957
2053
  const qs = this.buildQuery({ period: options?.period || undefined });
1958
- 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}` : ''}`, {
1959
2055
  headers: await this.signedReadHeaders()
1960
2056
  });
1961
2057
  if (!response.ok) {
1962
- 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'));
1963
2059
  }
1964
2060
  return response.json();
1965
2061
  }
@@ -1974,11 +2070,11 @@ class OneShot {
1974
2070
  */
1975
2071
  async rocs(options) {
1976
2072
  const qs = this.buildQuery({ period: options?.period || undefined });
1977
- const response = await fetch(`${this.baseUrl}/v1/analytics/rocs${qs ? `?${qs}` : ''}`, {
2073
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/rocs${qs ? `?${qs}` : ''}`, {
1978
2074
  headers: await this.signedReadHeaders()
1979
2075
  });
1980
2076
  if (!response.ok) {
1981
- 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'));
1982
2078
  }
1983
2079
  return response.json();
1984
2080
  }
@@ -2010,11 +2106,11 @@ class OneShot {
2010
2106
  since: toIso(options?.since),
2011
2107
  until: toIso(options?.until),
2012
2108
  });
2013
- const response = await fetch(`${this.baseUrl}/v1/analytics/receipts${qs ? `?${qs}` : ''}`, {
2109
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/receipts${qs ? `?${qs}` : ''}`, {
2014
2110
  headers: await this.signedReadHeaders()
2015
2111
  });
2016
2112
  if (!response.ok) {
2017
- 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'));
2018
2114
  }
2019
2115
  return response.json();
2020
2116
  }
@@ -2047,19 +2143,19 @@ class OneShot {
2047
2143
  this.validate(valueTag.type, 'valueTag.type');
2048
2144
  // Cadence-level: route to the outcome ledger by correlation key.
2049
2145
  if (typeof ref === 'object' && ref.goalId && !ref.receiptId && !ref.requestId) {
2050
- const response = await fetch(`${this.baseUrl}/v1/analytics/outcomes`, {
2146
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/outcomes`, {
2051
2147
  method: 'POST',
2052
2148
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2053
2149
  body: JSON.stringify({ goal_id: ref.goalId, ...valueTag }),
2054
2150
  });
2055
2151
  if (!response.ok) {
2056
- 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'));
2057
2153
  }
2058
2154
  return;
2059
2155
  }
2060
2156
  const id = typeof ref === 'string' ? ref : (ref.receiptId ?? ref.requestId);
2061
2157
  this.validate(id, 'receiptId/requestId/goalId');
2062
- const response = await fetch(`${this.baseUrl}/v1/analytics/receipts/${id}/value`, {
2158
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/receipts/${id}/value`, {
2063
2159
  method: 'PATCH',
2064
2160
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2065
2161
  body: JSON.stringify(valueTag)
@@ -2068,7 +2164,7 @@ class OneShot {
2068
2164
  throw new errors_2.ToolError('Receipt not found', 404, 'Receipt not found or not owned by this agent');
2069
2165
  }
2070
2166
  if (!response.ok) {
2071
- 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'));
2072
2168
  }
2073
2169
  }
2074
2170
  /**
@@ -2090,11 +2186,11 @@ class OneShot {
2090
2186
  */
2091
2187
  async rocsByGoal(options) {
2092
2188
  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}` : ''}`, {
2189
+ const response = await this.fetchApi(`${this.baseUrl}/v1/analytics/rocs/by-goal${qs ? `?${qs}` : ''}`, {
2094
2190
  headers: await this.signedReadHeaders()
2095
2191
  });
2096
2192
  if (!response.ok) {
2097
- 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'));
2098
2194
  }
2099
2195
  return response.json();
2100
2196
  }
@@ -2135,7 +2231,7 @@ class OneShot {
2135
2231
  const headers = await this.signedReadHeaders(`staging:${method}:${path}:${digest}`);
2136
2232
  if (!headers['x-agent-proof'])
2137
2233
  throw new Error('Could not sign staging application');
2138
- const response = await fetch(`${this.baseUrl}/v1/staging${path}`, {
2234
+ const response = await this.fetchApi(`${this.baseUrl}/v1/staging${path}`, {
2139
2235
  method, headers: { ...headers, 'Content-Type': 'application/json' },
2140
2236
  signal: AbortSignal.timeout(10000),
2141
2237
  ...(body ? { body: JSON.stringify(body) } : {}),
@@ -2170,7 +2266,7 @@ class OneShot {
2170
2266
  return base;
2171
2267
  try {
2172
2268
  const agent = this.provider.address;
2173
- const issuedAt = Math.floor(Date.now() / 1000);
2269
+ const issuedAt = Math.floor(Date.now() / 1000) + (domainName === 'OneShot Agent Auth' ? this.proofClockOffsetSec : 0);
2174
2270
  const nonce = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
2175
2271
  const signature = await this.provider.signTypedData(
2176
2272
  // `domainName` lets a different origin (e.g. Soul Hunt, which keeps
@@ -2205,6 +2301,45 @@ class OneShot {
2205
2301
  return base;
2206
2302
  }
2207
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
+ }
2208
2343
  /** Build a query string, skipping null/undefined values. Pass falsy-but-valid
2209
2344
  * values (0, '') as undefined at the call site to match prior `if (x)` guards. */
2210
2345
  buildQuery(params) {
@@ -2248,7 +2383,7 @@ class OneShot {
2248
2383
  body.alert_email = this._alertEmail;
2249
2384
  let response;
2250
2385
  try {
2251
- response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
2386
+ response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/budgets`, {
2252
2387
  method: 'PUT',
2253
2388
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2254
2389
  body: JSON.stringify(body),
@@ -2259,7 +2394,7 @@ class OneShot {
2259
2394
  }
2260
2395
  if (!response.ok) {
2261
2396
  const text = await response.text();
2262
- 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'));
2263
2398
  }
2264
2399
  this.log('Budget synced');
2265
2400
  })();
@@ -2301,7 +2436,7 @@ class OneShot {
2301
2436
  */
2302
2437
  /** Signed-proof JSON request to a free agent route; returns `data`. Errors map through failFromResponse. */
2303
2438
  async agentJson(path, method, body, scope = 'read') {
2304
- const response = await fetch(`${this.baseUrl}${path}`, {
2439
+ const response = await this.fetchApi(`${this.baseUrl}${path}`, {
2305
2440
  method,
2306
2441
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders(scope)) },
2307
2442
  ...(body === undefined || method === 'GET' ? {} : { body: JSON.stringify(body) }),
@@ -2313,11 +2448,11 @@ class OneShot {
2313
2448
  return (json && typeof json === 'object' && 'data' in json ? json.data : json);
2314
2449
  }
2315
2450
  async budgets() {
2316
- const response = await fetch(`${this.baseUrl}/v1/agents/me/budgets`, {
2451
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/budgets`, {
2317
2452
  headers: await this.signedReadHeaders(),
2318
2453
  });
2319
2454
  if (!response.ok) {
2320
- 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'));
2321
2456
  }
2322
2457
  const body = await response.json();
2323
2458
  return (body.data ?? body);
@@ -2340,24 +2475,24 @@ class OneShot {
2340
2475
  */
2341
2476
  async createAccessToken(opts = {}) {
2342
2477
  this.assertWalletSession('mint access tokens');
2343
- const response = await fetch(`${this.baseUrl}/v1/agents/me/access-tokens`, {
2478
+ const response = await this.fetchApi(`${this.baseUrl}/v1/agents/me/access-tokens`, {
2344
2479
  method: 'POST',
2345
2480
  headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2346
2481
  body: JSON.stringify(opts.name !== undefined ? { name: opts.name } : {}),
2347
2482
  });
2348
2483
  if (!response.ok)
2349
- 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'));
2350
2485
  const body = await response.json();
2351
2486
  return body.data;
2352
2487
  }
2353
2488
  /** List this agent's access tokens (prefix, label, last use, spend today — never the token). */
2354
2489
  async listAccessTokens(opts = {}) {
2355
2490
  this.assertWalletSession('list access tokens');
2356
- 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' : ''}`, {
2357
2492
  headers: await this.signedReadHeaders(),
2358
2493
  });
2359
2494
  if (!response.ok)
2360
- 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'));
2361
2496
  const body = await response.json();
2362
2497
  return body.data.tokens;
2363
2498
  }
@@ -2365,12 +2500,12 @@ class OneShot {
2365
2500
  async revokeAccessToken(id) {
2366
2501
  this.assertWalletSession('revoke access tokens');
2367
2502
  this.validate(id, 'id');
2368
- 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)}`, {
2369
2504
  method: 'DELETE',
2370
2505
  headers: await this.signedReadHeaders('write'),
2371
2506
  });
2372
2507
  if (!response.ok)
2373
- 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'));
2374
2509
  const body = await response.json();
2375
2510
  return body.data;
2376
2511
  }
@@ -2429,7 +2564,7 @@ class OneShot {
2429
2564
  const budget = response.status === 403 ? this.parseBudgetRejection(text) : undefined;
2430
2565
  if (budget)
2431
2566
  throw budget;
2432
- throw new errors_2.ToolError(message, response.status, text);
2567
+ throw new errors_2.ToolError(message, response.status, text, response.headers.get('retry-after'));
2433
2568
  }
2434
2569
  /**
2435
2570
  * Map a 402 `insufficient_credits` body (access-token sessions) onto a typed
@@ -2493,12 +2628,12 @@ class OneShot {
2493
2628
  throw new errors_2.ToolError(`Could not resolve the access token's agent (network): ${err}`, 0, String(err));
2494
2629
  }
2495
2630
  if (!response.ok) {
2496
- 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'));
2497
2632
  }
2498
2633
  const json = await response.json();
2499
2634
  const address = json.data?.wallet_address;
2500
2635
  if (!address)
2501
- 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'));
2502
2637
  return address;
2503
2638
  }
2504
2639
  /**
@@ -2601,7 +2736,7 @@ class OneShot {
2601
2736
  const headers = signed ? await this.signedReadHeaders() : undefined;
2602
2737
  if (scope.signal.aborted)
2603
2738
  throw new errors_2.OneShotError('Read deadline exceeded');
2604
- const response = await fetch(`${this.baseUrl}${path}`, { headers, signal: scope.signal });
2739
+ const response = await this.fetchApi(`${this.baseUrl}${path}`, { headers, signal: scope.signal });
2605
2740
  if (!response.ok && !(allowDegraded && response.status === 503))
2606
2741
  await this.failFromResponse('Reliability read failed', response);
2607
2742
  return await response.json();
@@ -2620,7 +2755,7 @@ class OneShot {
2620
2755
  }
2621
2756
  async executeToolRequest(endpoint, options, quoteId) {
2622
2757
  // 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);
2758
+ const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2624
2759
  const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
2625
2760
  if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
2626
2761
  throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
@@ -2663,7 +2798,7 @@ class OneShot {
2663
2798
  this.log('Memo truncated to 1000 chars');
2664
2799
  }
2665
2800
  }
2666
- else if (!endpoint.endsWith('/v1/tools/solve') && !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')) {
2667
2802
  this.log('No memo provided — consider adding a reason for audit trail');
2668
2803
  }
2669
2804
  if (payload.decisionContext !== undefined) {
@@ -2692,7 +2827,7 @@ class OneShot {
2692
2827
  context.phase = 'submission';
2693
2828
  let response = await this.makeRequest(endpoint, payload, undefined, quoteId, signal, undefined, extraHeaders);
2694
2829
  // Handle 402 Payment Required
2695
- if (response.status === 402 && /\/v1\/tools\/(solve|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)) {
2696
2831
  ({ execResp: response } = await this.runQuoteToPay({
2697
2832
  endpoint, payload, signal, maxCost, quoteResponse: response, extraHeaders,
2698
2833
  expectMsg: 'Expected a tool quote', totalOf: quote => quote.total,
@@ -2813,6 +2948,15 @@ class OneShot {
2813
2948
  * `wait: false`, or to resume waiting after a client restart. Delivery is the
2814
2949
  * same as for blocking calls: WebSocket push when the server offers it, HTTP
2815
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.
2816
2960
  */
2817
2961
  async waitForResult(requestId, options = {}) {
2818
2962
  this.validate(requestId, 'requestId');
@@ -2825,6 +2969,9 @@ class OneShot {
2825
2969
  // swallows frames) costs nothing beyond the poll cadence. Previously the
2826
2970
  // client waited 60% of its timeout on the socket before polling at all —
2827
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');
2828
2975
  const inner = new AbortController();
2829
2976
  const onOuterAbort = () => inner.abort();
2830
2977
  if (signal?.aborted)
@@ -2832,12 +2979,24 @@ class OneShot {
2832
2979
  else
2833
2980
  signal?.addEventListener('abort', onOuterAbort, { once: true });
2834
2981
  const wait = { pushConfirmed: false, lastStatus: undefined, via: undefined };
2835
- // 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.
2836
2991
  const emit = (status) => {
2992
+ if (wait.lastStatus === 'completed' || wait.lastStatus === 'failed')
2993
+ return;
2837
2994
  if (status === wait.lastStatus)
2838
2995
  return;
2839
2996
  wait.lastStatus = status;
2840
- onStatusUpdate?.(status, requestId);
2997
+ if (status === 'completed' && phoneOpts?.waitForPhones)
2998
+ return;
2999
+ notify(status);
2841
3000
  };
2842
3001
  const startedAt = Date.now();
2843
3002
  const wsBranch = this.waitViaWebSocket(requestId, inner.signal, emit, wait).catch((err) => {
@@ -2873,8 +3032,10 @@ class OneShot {
2873
3032
  // hit a transient error before any successful refresh, we return what we
2874
3033
  // already have (with phones_pending=true still set so consumers know).
2875
3034
  if (phoneOpts?.waitForPhones && this._isPhonesPending(result)) {
2876
- return this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
3035
+ result = await this._pollForPhones(requestId, phoneOpts.phoneTimeoutSec ?? 360, signal, result);
2877
3036
  }
3037
+ if (phoneOpts?.waitForPhones)
3038
+ notify('completed');
2878
3039
  return result;
2879
3040
  }
2880
3041
  /**
@@ -2918,7 +3079,7 @@ class OneShot {
2918
3079
  if (signal?.aborted)
2919
3080
  throw new errors_2.OneShotError('Operation cancelled');
2920
3081
  try {
2921
- const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
3082
+ const resp = await this.fetchApi(`${this.baseUrl}/v1/requests/${requestId}`, {
2922
3083
  headers: await this.signedReadHeaders(),
2923
3084
  signal,
2924
3085
  });
@@ -2927,7 +3088,7 @@ class OneShot {
2927
3088
  // whatever we last had so consumers don't lose the sync result.
2928
3089
  if (lastResult !== undefined)
2929
3090
  return lastResult;
2930
- 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'));
2931
3092
  }
2932
3093
  const job = await resp.json();
2933
3094
  lastResult = (job.result ?? job);
@@ -2936,6 +3097,8 @@ class OneShot {
2936
3097
  }
2937
3098
  }
2938
3099
  catch (err) {
3100
+ if (signal?.aborted)
3101
+ throw new errors_2.OneShotError('Operation cancelled');
2939
3102
  if (err instanceof errors_2.OneShotError)
2940
3103
  throw err;
2941
3104
  if (lastResult !== undefined)
@@ -3031,6 +3194,7 @@ class OneShot {
3031
3194
  wait.pushConfirmed = true;
3032
3195
  this.log('WebSocket push confirmed — relaxing HTTP polling');
3033
3196
  }
3197
+ emit(String(msg.status));
3034
3198
  if (msg.status === 'completed') {
3035
3199
  this.log('Job completed (WebSocket)');
3036
3200
  settle(() => {
@@ -3050,9 +3214,6 @@ class OneShot {
3050
3214
  reject(new errors_2.JobError(`Job failed: ${msg.error ?? 'Unknown'}`, requestId, String(msg.error ?? 'Unknown'), msg.error_code, msg.result));
3051
3215
  });
3052
3216
  }
3053
- else {
3054
- emit(String(msg.status));
3055
- }
3056
3217
  }
3057
3218
  catch {
3058
3219
  // Ignore malformed messages
@@ -3088,6 +3249,9 @@ class OneShot {
3088
3249
  catch (error) {
3089
3250
  if (scope.timedOut())
3090
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');
3091
3255
  throw error;
3092
3256
  }
3093
3257
  finally {
@@ -3104,7 +3268,7 @@ class OneShot {
3104
3268
  if (signal?.aborted)
3105
3269
  throw new errors_2.OneShotError('Operation cancelled');
3106
3270
  try {
3107
- const resp = await fetch(`${this.baseUrl}/v1/requests/${requestId}`, {
3271
+ const resp = await this.fetchApi(`${this.baseUrl}/v1/requests/${requestId}`, {
3108
3272
  headers: await this.signedReadHeaders(),
3109
3273
  signal
3110
3274
  });
@@ -3115,9 +3279,10 @@ class OneShot {
3115
3279
  if (resp.status >= 500 || resp.status === 429) {
3116
3280
  throw new Error(`Poll returned ${resp.status}: ${body.slice(0, 200)}`);
3117
3281
  }
3118
- 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'));
3119
3283
  }
3120
3284
  const job = await resp.json();
3285
+ emit?.(String(job.status));
3121
3286
  if (job.status === 'completed') {
3122
3287
  this.log('Job completed');
3123
3288
  if (wait)
@@ -3140,7 +3305,6 @@ class OneShot {
3140
3305
  wait.via = 'http';
3141
3306
  throw new errors_2.JobError(`Job failed: ${job.error ?? 'Unknown'}`, requestId, String(job.error ?? 'Unknown'), job.error_code, job.result);
3142
3307
  }
3143
- emit?.(String(job.status));
3144
3308
  retries = 0;
3145
3309
  const interval = wait?.pushConfirmed
3146
3310
  ? HTTP_POLL_RELAXED_MS
@@ -3223,7 +3387,7 @@ class OneShot {
3223
3387
  // alone (401 proof_required).
3224
3388
  const unsignedStub = payment?.payload?.signature === '0x';
3225
3389
  const askedForProof = !!extraHeaders?.['x-agent-proof'];
3226
- const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve)$/.test(endpoint)
3390
+ const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict)$/.test(endpoint)
3227
3391
  ? 'submit'
3228
3392
  : askedForProof ? 'read' : 'write';
3229
3393
  const headers = {
@@ -3251,7 +3415,7 @@ class OneShot {
3251
3415
  transportDeadline?.signal.addEventListener('abort', () => transportDeadline.close(), { once: true });
3252
3416
  if (fetchSignal?.aborted)
3253
3417
  throw new errors_2.OneShotError('Operation cancelled');
3254
- const work = fetch(`${this.baseUrl}${endpoint}`, {
3418
+ const work = this.fetchApi(`${this.baseUrl}${endpoint}`, {
3255
3419
  method: 'POST', headers, body: JSON.stringify(data), signal: fetchSignal,
3256
3420
  });
3257
3421
  try {
@@ -3408,7 +3572,7 @@ class OneShot {
3408
3572
  }
3409
3573
  catch (err) {
3410
3574
  // 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))
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))
3412
3576
  this.releaseUsdcReservation(signed.reservation);
3413
3577
  throw err;
3414
3578
  }