@oneshot-agent/sdk 0.61.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.OneShot = exports.executeSwap = exports.getSwapQuote = exports.ReadOnlyWalletProvider = exports.CdpWalletProvider = exports.EthersWalletProvider = void 0;
40
40
  const untrusted_content_1 = require("./untrusted-content");
41
+ const tool_timeouts_generated_1 = require("./tool-timeouts.generated");
41
42
  const hunts_1 = require("./hunts");
42
43
  __exportStar(require("./solve"), exports);
43
44
  __exportStar(require("./predict"), exports);
@@ -64,7 +65,7 @@ __exportStar(require("./errors"), exports);
64
65
  const approvals_1 = require("./approvals");
65
66
  // Keep receipt on its subpath: node:crypto breaks browser bundles, and providerCost is not public.
66
67
  // Keep in sync with package.json `version`. Guarded by version.test.ts.
67
- const SDK_VERSION = '0.61.0';
68
+ const SDK_VERSION = '0.63.0';
68
69
  /** HTTP poll cadence while push is unconfirmed: fast first checks, settling at 2s. */
69
70
  const HTTP_POLL_BACKOFF_MS = [300, 600, 1000, 2000];
70
71
  /** HTTP poll cadence once the WebSocket has delivered for this request. */
@@ -319,6 +320,39 @@ class OneShot {
319
320
  async tool(toolName, options) {
320
321
  return this.executeToolRequest(`/v1/tools/${toolName}`, options);
321
322
  }
323
+ /**
324
+ * Price account setup without a recipient or message; this does not buy or send. Choose the mode
325
+ * (`relay` by default, or `mailbox`) and the addresses (default `['agent']`). On a verified domain
326
+ * the agent already has, this prices only the named addresses it does not have yet.
327
+ */
328
+ async quoteDomain(domain, options = {}) {
329
+ this.validate(domain, 'domain');
330
+ const { mailbox_mode, addresses, ...rest } = options;
331
+ if (mailbox_mode !== undefined && mailbox_mode !== 'relay' && mailbox_mode !== 'mailbox') {
332
+ throw new errors_2.ValidationError("mailbox_mode must be 'relay' or 'mailbox'", 'mailbox_mode');
333
+ }
334
+ if (addresses !== undefined && (!Array.isArray(addresses) || addresses.length < 1 || addresses.length > 10
335
+ || addresses.some((a) => typeof a !== 'string' || !a.trim()))) {
336
+ throw new errors_2.ValidationError('addresses must name 1 to 10 local parts, such as ["sales"]', 'addresses');
337
+ }
338
+ const quote = await this.executeToolRequest('/v1/domains/quote', {
339
+ ...rest, domain,
340
+ ...(mailbox_mode ? { mailbox_mode } : {}),
341
+ ...(addresses ? { addresses: addresses.map((a) => a.trim().toLowerCase()) } : {}),
342
+ });
343
+ if (quote.total_cost !== undefined)
344
+ this.assertWithinMaxCost(String(quote.total_cost), options.maxCost);
345
+ return quote;
346
+ }
347
+ /**
348
+ * Owner/API purchase of the exact setup quote. The mode and addresses are the quote's; the result
349
+ * names them. Registration, adding addresses and email are separate actions.
350
+ */
351
+ async buyDomain(domain, quoteId, options = {}) {
352
+ this.validate(domain, 'domain');
353
+ this.validate(quoteId?.trim(), 'quoteId');
354
+ return this.executeToolRequest('/v1/domains/buy', { ...options, domain }, quoteId.trim());
355
+ }
322
356
  async email(options) {
323
357
  // to/subject are server-derived when replying to an inbound email.
324
358
  if (!options.reply_to_email_id) {
@@ -331,59 +365,117 @@ class OneShot {
331
365
  if (options.from_mailbox && !options.from_domain) {
332
366
  throw new errors_2.ValidationError('from_mailbox was pinned without from_domain. Pass from_domain (a domain you own) as well, or omit both to rotate across your warmed domains.', 'from_domain');
333
367
  }
334
- const useRotation = !options.from_domain && !options.from_mailbox;
335
- const fromAddress = useRotation
336
- ? undefined
337
- : `${options.from_mailbox ?? 'agent'}@${options.from_domain}`;
338
368
  // Reuse the confirmed quote so the charge matches the amount shown; true alone confirms no amount.
339
369
  const confirmedQuote = typeof options.confirmNewDomain === 'string' && options.confirmNewDomain.trim()
340
370
  ? options.confirmNewDomain.trim() : undefined;
341
371
  if (confirmedQuote) {
342
- this.log(`Email: sending against confirmed quote ${confirmedQuote}`);
343
- return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress), confirmedQuote);
372
+ this.validate(options.from_domain, 'from_domain');
373
+ const setup = await this.buyDomain(options.from_domain, confirmedQuote, {
374
+ maxCost: options.maxCost, signal: options.signal, wait: false,
375
+ });
376
+ return { status: 'setup_pending', setup, request_id: setup.request_id, receipt_id: setup.receipt_id,
377
+ message: 'Setup purchased; no email was sent. Wait for it to finish, then call email without confirmNewDomain.' };
378
+ }
379
+ let fromAddress;
380
+ if (options.from_domain) {
381
+ const domainName = options.from_domain.trim().toLowerCase().replace(/\.$/, '');
382
+ // An unavailable ownership read leaves the email quote to decide whether setup is needed.
383
+ const owned = await this.listDomains().catch(() => undefined);
384
+ const entry = owned?.domains.find(d => d.domain.toLowerCase() === domainName);
385
+ // A new name reaches the setup quote without an email quote.
386
+ if (owned && !entry)
387
+ return this.emailDomainSetup(domainName, options);
388
+ fromAddress = options.from_mailbox
389
+ ? `${options.from_mailbox}@${options.from_domain}`
390
+ : entry?.default_from ?? `agent@${options.from_domain}`;
391
+ }
392
+ let quote;
393
+ try {
394
+ quote = await this.tool('email/quote', {
395
+ ...(fromAddress ? { from_address: fromAddress } : {}),
396
+ ...(options.to !== undefined ? { to_address: options.to } : {}),
397
+ ...(options.subject !== undefined ? { subject: options.subject } : {}),
398
+ ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
399
+ body: options.body,
400
+ maxCost: options.maxCost,
401
+ });
402
+ }
403
+ catch (error) {
404
+ const setup = await this.emailSetupFromRefusal(error, options);
405
+ if (setup)
406
+ return setup;
407
+ throw error;
344
408
  }
345
- // Mailbox provisioning and domain registration are distinct fees; only a new domain needs confirmation.
346
- const quote = await this.tool('email/quote', {
347
- ...(fromAddress ? { from_address: fromAddress } : {}),
348
- ...(options.to !== undefined ? { to_address: options.to } : {}),
349
- ...(options.subject !== undefined ? { subject: options.subject } : {}),
350
- ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
351
- ...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
352
- body: options.body,
353
- maxCost: options.maxCost,
354
- });
355
409
  this.log(`Email quote: $${quote.total_cost}`);
356
410
  // Enforce the cap locally even if the server did not check the header.
357
411
  this.assertWithinMaxCost(quote.total_cost, options.maxCost);
358
- // A new domain must be explicitly authorized against its exact quote before any charge or send.
359
- if (quote.is_new) {
360
- const domainName = quote.domain ?? options.from_domain ?? '';
361
- const breakdown = quote.registration_fee !== undefined && quote.service_fee !== undefined
362
- ? ` (registration $${quote.registration_fee} + service fee $${quote.service_fee}` +
363
- `${quote.mailbox_provisioning_fee ? ` + mailbox fee $${quote.mailbox_provisioning_fee}` : ''})`
364
- : '';
365
- return {
366
- status: 'confirmation_required',
367
- cost: parseFloat(quote.total_cost),
368
- quote_id: quote.quote_id,
369
- domain: { domain: domainName, status: 'quote_only', was_provisioned: false },
370
- message: `Registering ${domainName} costs $${quote.total_cost} total${breakdown}. ` +
371
- `Nothing has been charged yet. Show the user this exact amount; once they agree, call this again with the same ` +
372
- `arguments and confirm_new_domain: '${quote.quote_id}' (SDK: confirmNewDomain) to register the domain, ` +
373
- `charge exactly $${quote.total_cost}, and send this email. true only re-quotes; a wrong or expired quote id is refused without charge.`,
374
- };
375
- }
376
412
  return this.executeToolRequest('/v1/tools/email/send', this.emailSendPayload(options, fromAddress ?? quote.from_address), quote.quote_id);
377
413
  }
378
- /** The /email/send body for `email()`: the same whether it follows a fresh quote or a confirmed one. */
414
+ /**
415
+ * A send never sets up a domain or an address: the email quote refuses with `no_sending_domain` or
416
+ * `address_not_set_up` and names the setup. Route that to the setup quote, so the caller reviews its
417
+ * price, rather than surfacing a bare error. Any other error is the caller's.
418
+ */
419
+ async emailSetupFromRefusal(error, options) {
420
+ if (!(error instanceof errors_2.ToolError) || error.statusCode !== 400)
421
+ return undefined;
422
+ let body;
423
+ try {
424
+ body = JSON.parse(error.responseBody);
425
+ }
426
+ catch {
427
+ return undefined;
428
+ }
429
+ const requirements = Array.isArray(body.requirements) ? body.requirements : undefined;
430
+ if (body.error === 'address_not_set_up' && body.domain && body.from_address) {
431
+ const local = body.from_address.split('@')[0];
432
+ try {
433
+ return await this.emailDomainSetup(body.domain, options, [local]);
434
+ }
435
+ catch (quoteError) {
436
+ // The address is already being added by an earlier purchase.
437
+ if (quoteError instanceof errors_2.ToolError && quoteError.statusCode === 409 && /addresses_already_set_up/.test(quoteError.responseBody)) {
438
+ return { status: 'setup_pending', error: 'address_not_set_up', requirements,
439
+ message: `${body.from_address} is being set up; no email was sent. Send again once it is active in listDomains().` };
440
+ }
441
+ throw quoteError;
442
+ }
443
+ }
444
+ if (body.error !== 'no_sending_domain')
445
+ return undefined;
446
+ const domain = body.domain ?? options.from_domain?.trim().toLowerCase();
447
+ if (body.reason === 'not_owned' && domain)
448
+ return this.emailDomainSetup(domain, options);
449
+ const pending = body.reason === 'not_verified';
450
+ return {
451
+ status: pending ? 'setup_pending' : 'setup_required', error: 'no_sending_domain', requirements,
452
+ message: body.message ?? 'This agent has no verified sending domain. Buy one with quoteDomain and buyDomain; no email was sent.',
453
+ };
454
+ }
455
+ async emailDomainSetup(domainName, options, addresses) {
456
+ const named = addresses ?? (options.from_mailbox ? [options.from_mailbox] : undefined);
457
+ const setup = await this.quoteDomain(domainName, { maxCost: options.maxCost, signal: options.signal, ...(named ? { addresses: named } : {}) });
458
+ const addingAddresses = setup.setup === 'domain_addresses';
459
+ return {
460
+ status: setup.purchasable ? 'confirmation_required' : 'setup_unavailable',
461
+ cost: setup.total_cost,
462
+ quote_id: setup.quote_id,
463
+ setup,
464
+ domain: { domain: domainName, status: 'quote_only', was_provisioned: false },
465
+ message: !setup.purchasable
466
+ ? 'This name cannot be purchased. Choose another name for account setup.'
467
+ : addingAddresses
468
+ ? 'This address is not on the domain yet. No purchase or email send has occurred. The owner or API caller must review setup.total_cost, then buy this exact quote at POST /v1/domains/buy (SDK: confirmNewDomain). Send separately once the address is active.'
469
+ : 'No setup purchase or email send has occurred. The owner or API caller must review setup.total_cost and setup.billing, then buy this exact quote at POST /v1/domains/buy (SDK: confirmNewDomain). No email is included; send separately after verification.',
470
+ };
471
+ }
472
+ /** Keep setup-only fields out of the paid send body. */
379
473
  emailSendPayload(options, resolvedFromAddress) {
380
474
  const payload = {
381
475
  ...(resolvedFromAddress ? { from_address: resolvedFromAddress } : {}),
382
476
  ...(options.to !== undefined ? { to_address: options.to } : {}),
383
477
  ...(options.subject !== undefined ? { subject: options.subject } : {}),
384
478
  ...(options.reply_to_email_id ? { reply_to_email_id: options.reply_to_email_id } : {}),
385
- // Preserve mailbox provisioning when the new domain does not yet have a stored mode.
386
- ...(options.mailbox_mode ? { mailbox_mode: options.mailbox_mode } : {}),
387
479
  body: options.body,
388
480
  signal: options.signal,
389
481
  onStatusUpdate: options.onStatusUpdate,
@@ -1139,8 +1231,8 @@ class OneShot {
1139
1231
  * already has with them, or a new one. Use it once an invitation is accepted,
1140
1232
  * or for a connection the account has never messaged. Needs the `reply`
1141
1233
  * grant; shares the reply daily cap and pacing. A failed send carries an
1142
- * error code: `not_connected` (invite them first) and `rate_limited` mean
1143
- * "defer", not "give up".
1234
+ * error code: `recipient_not_connected` means invite them first, not give
1235
+ * up.
1144
1236
  */
1145
1237
  async linkedinMessage(options) {
1146
1238
  this.validate(options.accountId, 'accountId');
@@ -1488,7 +1580,7 @@ class OneShot {
1488
1580
  }
1489
1581
  /**
1490
1582
  * The agent's connections: GitHub and business systems, each with `id`, `system`, `account_label` and
1491
- * `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement) takes the `id` of an active
1583
+ * `status`. Free. A connected-system read (ledger, CRM, shipping, tax, procurement, meetings, field service) takes the `id` of an active
1492
1584
  * one as `connection_id`; pick it by `system` and `account_label`, then call the read with the generic
1493
1585
  * {@link OneShot.route}.
1494
1586
  *
@@ -1533,6 +1625,11 @@ class OneShot {
1533
1625
  * Uses the quote-then-pay flow: first call gets a 402 with a budget estimate,
1534
1626
  * second call (with payment) creates the goal.
1535
1627
  *
1628
+ * Name the unit of work, how work starts and what each unit needs in the objective, or pass
1629
+ * `scope`. The quote carries `scope`, `price` (per unit expected and ceiling, overhead, total or
1630
+ * units covered, unpriced, unmeasured) and the `done_check` the goal is held to; `onQuote` reads
1631
+ * them before paying and may throw to stop. A `scope` you pass is priced again in code.
1632
+ *
1536
1633
  * @example
1537
1634
  * ```typescript
1538
1635
  * const goal = await agent.compute({
@@ -1567,6 +1664,8 @@ class OneShot {
1567
1664
  payload.approvals = options.approvals;
1568
1665
  if (options.done_check)
1569
1666
  payload.done_check = options.done_check;
1667
+ if (options.scope)
1668
+ payload.scope = options.scope;
1570
1669
  if (options.egress_allowlist)
1571
1670
  payload.egress_allowlist = options.egress_allowlist;
1572
1671
  // Compute returns the goal directly on 202 - no job polling, unlike the
@@ -1578,7 +1677,10 @@ class OneShot {
1578
1677
  maxCost: options.maxCost,
1579
1678
  expectMsg: 'Expected 402 for compute quote',
1580
1679
  totalOf: (ctx) => ctx.total_budget,
1581
- onQuote: (ctx) => this.log(`Compute quote: $${ctx.total_budget} — ${ctx.objective_summary}`),
1680
+ onQuote: (ctx) => {
1681
+ this.log(`Compute quote: $${ctx.total_budget} — ${ctx.objective_summary}`);
1682
+ options.onQuote?.(ctx);
1683
+ },
1582
1684
  on400: async (resp) => {
1583
1685
  const errorData = await resp.json();
1584
1686
  if (errorData.error === 'content_blocked') {
@@ -1625,9 +1727,9 @@ class OneShot {
1625
1727
  * Poll a compute goal using its goal_id, not its creation request_id.
1626
1728
  * Returns the complete goal snapshot on completed; failed/cancelled reject
1627
1729
  * with JobError (goal ID, end_reason code, snapshot in partialResult).
1628
- * Budget exhaustion ends this wait; after fundComputeGoal revives the goal,
1730
+ * Budget exhaustion waits in awaiting_budget for funding; after seven days it ends. After fundComputeGoal revives an ended goal,
1629
1731
  * call waitForComputeGoal again with the same ID to observe the new run.
1630
- * Paused/awaiting_confirmation/awaiting_connection and recurring goals keep waiting until the
1732
+ * Paused/awaiting_confirmation/awaiting_connection/awaiting_budget/awaiting_tool/awaiting_setup and recurring goals keep waiting until the
1631
1733
  * goal ends or the timeout (seconds, default 120) expires with JobTimeoutError.
1632
1734
  * Infinity disables the local deadline.
1633
1735
  * onStatusUpdate receives each distinct observed status before settlement.
@@ -1685,7 +1787,8 @@ class OneShot {
1685
1787
  }
1686
1788
  }
1687
1789
  /**
1688
- * List tasks under a compute goal
1790
+ * List tasks under a compute goal, newest first: one page of `limit` tasks (default 100, at most 500).
1791
+ * Use `getComputeTasksPage` to read further pages.
1689
1792
  *
1690
1793
  * @example
1691
1794
  * ```typescript
@@ -1695,16 +1798,40 @@ class OneShot {
1695
1798
  * }
1696
1799
  * ```
1697
1800
  */
1698
- async getComputeTasks(goalId) {
1801
+ async getComputeTasks(goalId, options = {}) {
1802
+ return (await this.getComputeTasksPage(goalId, options)).tasks;
1803
+ }
1804
+ /**
1805
+ * One page of a compute goal's tasks, newest first, with the cursor of the next page (null on the last).
1806
+ *
1807
+ * @example
1808
+ * ```typescript
1809
+ * let cursor: string | undefined;
1810
+ * do {
1811
+ * const page = await agent.getComputeTasksPage('goal_01HX...', { cursor });
1812
+ * for (const t of page.tasks) console.log(t.id, t.status);
1813
+ * cursor = page.next_cursor ?? undefined;
1814
+ * } while (cursor);
1815
+ * ```
1816
+ */
1817
+ async getComputeTasksPage(goalId, options = {}) {
1699
1818
  this.validate(goalId, 'goalId');
1700
- const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/tasks`, {
1819
+ const query = new URLSearchParams();
1820
+ if (options.status)
1821
+ query.set('status', options.status);
1822
+ if (options.limit !== undefined)
1823
+ query.set('limit', String(options.limit));
1824
+ if (options.cursor)
1825
+ query.set('cursor', options.cursor);
1826
+ const qs = query.toString();
1827
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/tasks${qs ? `?${qs}` : ''}`, {
1701
1828
  headers: await this.signedReadHeaders()
1702
1829
  });
1703
1830
  if (!response.ok) {
1704
1831
  throw new errors_2.ToolError('Failed to get compute tasks', response.status, await response.text(), response.headers.get('retry-after'));
1705
1832
  }
1706
1833
  const json = await response.json();
1707
- return json.data;
1834
+ return { tasks: json.data, next_cursor: json.page?.next_cursor ?? null };
1708
1835
  }
1709
1836
  /**
1710
1837
  * List what a compute goal produced: the files its sandbox tasks left and
@@ -1878,7 +2005,31 @@ class OneShot {
1878
2005
  return json.data;
1879
2006
  }
1880
2007
  /**
1881
- * Pause a recurring compute goal
2008
+ * Continue a goal held in `awaiting_tool` without the tool it requested
2009
+ * (#3173). The goal returns to active and resumes with `message` as what
2010
+ * its owner said, or a default note that it continues without the tool.
2011
+ *
2012
+ * @example
2013
+ * ```typescript
2014
+ * await agent.continueComputeGoalWithoutTool(goalId, { message: 'Use the public filings instead.' });
2015
+ * ```
2016
+ */
2017
+ async continueComputeGoalWithoutTool(goalId, input = {}) {
2018
+ this.validate(goalId, 'goalId');
2019
+ const response = await this.fetchApi(`${this.baseUrl}/v1/compute/${goalId}/respond`, {
2020
+ method: 'POST',
2021
+ headers: { 'Content-Type': 'application/json', ...(await this.signedReadHeaders('write')) },
2022
+ body: JSON.stringify({ continue_without_tool: true, ...(input.message ? { response: input.message } : {}) }),
2023
+ });
2024
+ if (response.status === 404)
2025
+ throw new errors_2.ToolError('Goal not found', 404, await response.text(), response.headers.get('retry-after'));
2026
+ if (!response.ok)
2027
+ throw new errors_2.ToolError('Failed to continue compute goal without the tool', response.status, await response.text(), response.headers.get('retry-after'));
2028
+ const json = await response.json();
2029
+ return json.data;
2030
+ }
2031
+ /**
2032
+ * Pause a compute goal (one-off goals keep their work and continue where they were on resume)
1882
2033
  *
1883
2034
  * @example
1884
2035
  * ```typescript
@@ -1899,7 +2050,7 @@ class OneShot {
1899
2050
  return json.data;
1900
2051
  }
1901
2052
  /**
1902
- * Resume a paused recurring compute goal
2053
+ * Resume a paused compute goal
1903
2054
  *
1904
2055
  * @example
1905
2056
  * ```typescript
@@ -1922,7 +2073,7 @@ class OneShot {
1922
2073
  }
1923
2074
  /**
1924
2075
  * Top up budget for a recurring compute goal, or for a goal (one-off or recurring) that
1925
- * ended because it ran out of budget (`end_reason: 'budget_exhausted'`).
2076
+ * is awaiting budget or ended because it ran out of budget (`end_reason: 'budget_exhausted'`).
1926
2077
  *
1927
2078
  * Uses the quote-then-pay flow: the first call gets a 402 with the top-up price,
1928
2079
  * the second call (with payment) credits the budget. The amount IS the price.
@@ -2505,6 +2656,9 @@ class OneShot {
2505
2656
  const credits = response.status === 402 ? this.parseInsufficientCredits(text) : undefined;
2506
2657
  if (credits)
2507
2658
  throw credits;
2659
+ const ambiguous = response.status === 500 ? this.parseSettlementAmbiguous(text) : undefined;
2660
+ if (ambiguous)
2661
+ throw ambiguous;
2508
2662
  const rejection = [402, 500].includes(response.status) ? this.parsePaymentRejection(text) : undefined;
2509
2663
  if (rejection)
2510
2664
  throw rejection;
@@ -2613,6 +2767,20 @@ class OneShot {
2613
2767
  return undefined;
2614
2768
  }
2615
2769
  }
2770
+ /** A 500 `settlement_ambiguous`: the payment may be spent, so the caller must not sign another for this call. */
2771
+ parseSettlementAmbiguous(text) {
2772
+ let body;
2773
+ try {
2774
+ body = JSON.parse(text);
2775
+ }
2776
+ catch {
2777
+ return undefined;
2778
+ }
2779
+ if (body?.error !== 'settlement_ambiguous')
2780
+ return undefined;
2781
+ const settlementId = typeof body.settlement_id === 'string' ? body.settlement_id : undefined;
2782
+ return new errors_1.SettlementAmbiguousError(`Payment may have gone through. Don't pay again; if it landed and the call did not run, it is credited to your account.${settlementId ? ` [settlement ${settlementId}]` : ''}`, settlementId);
2783
+ }
2616
2784
  parsePaymentRejection(text) {
2617
2785
  let data;
2618
2786
  try {
@@ -2691,7 +2859,7 @@ class OneShot {
2691
2859
  }
2692
2860
  async executeToolRequest(endpoint, options, quoteId) {
2693
2861
  // LinkedIn writes act through a human's account and cannot be recalled: always keyed.
2694
- const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2862
+ const reliable = /(?:^|\/)(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|lookup\/company-fund-holdings|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint);
2695
2863
  const key = options.idempotencyKey ?? (reliable ? ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(16)) : undefined);
2696
2864
  if (options.totalTimeoutMs !== undefined && (!Number.isFinite(options.totalTimeoutMs) || options.totalTimeoutMs <= 0)) {
2697
2865
  throw new errors_2.ValidationError('totalTimeoutMs must be positive', 'totalTimeoutMs');
@@ -2804,7 +2972,7 @@ class OneShot {
2804
2972
  if (duplicate?.error === 'duplicate_request' && typeof duplicate.details?.request_id === 'string') {
2805
2973
  response = Response.json({ request_id: duplicate.details.request_id, receipt_id: duplicate.details.receipt_id,
2806
2974
  status: 'processing', tool: 'linkedin', linkedin: { action_request_id: duplicate.details.action_request_id,
2807
- invitation_id: duplicate.details.invitation_id } }, { status: 202 });
2975
+ invitation_id: duplicate.details.invitation_id, scheduled_for: null } }, { status: 202 });
2808
2976
  }
2809
2977
  }
2810
2978
  if (!response.ok) {
@@ -2820,7 +2988,7 @@ class OneShot {
2820
2988
  if (!wait) {
2821
2989
  return { ...result, idempotencyKey };
2822
2990
  }
2823
- const completed = await this.pollJob(result.request_id, timeout, signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined, endpoint);
2991
+ const completed = await this.pollJob(result.request_id, timeout ?? tool_timeouts_generated_1.TOOL_TIMEOUTS[endpoint], signal, onStatusUpdate, waitForPhones ? { waitForPhones, phoneTimeoutSec } : undefined, endpoint);
2824
2992
  if (completed && typeof completed === 'object' && !Array.isArray(completed)) {
2825
2993
  return { ...completed, request_id: context.requestId, receipt_id: completed.receipt_id ?? context.receiptId, idempotencyKey };
2826
2994
  }
@@ -3260,7 +3428,7 @@ class OneShot {
3260
3428
  // Identity Verification requires wallet proof on the payment leg too, to recover keyed submissions.
3261
3429
  const provenPayment = /(?:^|\/)identity\/start$/.test(endpoint);
3262
3430
  const askedForProof = !!extraHeaders?.['x-agent-proof'];
3263
- const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|physical-mail\/send|solve|predict)$/.test(endpoint)
3431
+ const proofScope = /(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|lookup\/company-fund-holdings|physical-mail\/send|solve|predict)$/.test(endpoint)
3264
3432
  ? 'submit'
3265
3433
  : askedForProof ? 'read' : 'write';
3266
3434
  const headers = {
@@ -3431,14 +3599,22 @@ class OneShot {
3431
3599
  }
3432
3600
  catch (err) {
3433
3601
  // Keyed writes may already be queued server-side after a transport failure: keep the reservation.
3434
- 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))
3602
+ if (!/(lookup\/person-(profile|email|email-status)|lookup\/place-permits|lookup\/company-private-raises|lookup\/company-fund-holdings|physical-mail\/send|solve|predict|linkedin\/(reply|message|profile-view|react|invite(?:\/withdraw)?))$/.test(endpoint))
3435
3603
  this.releaseUsdcReservation(signed.reservation);
3436
3604
  throw err;
3437
3605
  }
3438
- if (!resp.ok && !(extraHeaders?.['Idempotency-Key'] && resp.status >= 500))
3606
+ // A settlement_ambiguous payment may be spent: keep its reservation until it expires.
3607
+ if (!resp.ok && !(extraHeaders?.['Idempotency-Key'] && resp.status >= 500) && !(await this.isSettlementAmbiguous(resp))) {
3439
3608
  this.releaseUsdcReservation(signed.reservation);
3609
+ }
3440
3610
  return resp;
3441
3611
  }
3612
+ async isSettlementAmbiguous(resp) {
3613
+ if (resp.status !== 500)
3614
+ return false;
3615
+ const text = await resp.clone().text().catch(() => '');
3616
+ return this.parseSettlementAmbiguous(text) !== undefined;
3617
+ }
3442
3618
  /** Parse the PAYMENT-REQUIRED header from a 402 response into the accepted requirements and Bazaar metadata. */
3443
3619
  parsePaymentRequired(header) {
3444
3620
  if (header) {