@thirdfy/agent-cli 0.2.73 → 0.2.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,26 @@
2
2
 
3
3
  All notable changes to `@thirdfy/agent-cli` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
- ## [Unreleased]
5
+ ## [0.2.76] - 2026-09-23
6
+
7
+ ### Fixed
8
+
9
+ - Managed writes (perps orders, TP/SL updates, closes) use a 90s HTTP timeout instead of the 30s default. They sign, broadcast, and wait for a receipt in one API call. Reads keep the base timeout; onboarding, earn, and bridge actions keep 180s. Override: `THIRDFY_CLI_WRITE_TIMEOUT_MS` (or `THIRDFY_API_WRITE_TIMEOUT_MS`).
10
+ - `requestJson` resends a timed-out request only when it is a GET, a validation-only preview (`validationOnly` or `executionLane: validation_only`), or marked `writeAction: false` (read rail, build-tx, telemetry, governance preflight, execute-intent preflight). Every other POST (venue fund, delegation, onboarding) returns `WRITE_OUTCOME_UNKNOWN` instead of a second send. Read/write classification uses catalog metadata when the resolved action is available.
11
+ - Avantis market open/close (`place_avantis_perps_order`, `close_avantis_position`) use the 180s long timeout; the API streams, then polls, for up to about 180s.
12
+ - A write whose request times out is not resent. The retry could double a trade that already executed. The CLI throws `WRITE_OUTCOME_UNKNOWN` (`nextAction: reread_state_before_retry`). Preflight (`validationOnly`) and reads still retry once. Pin: `test/actionTimeouts.test.cjs`. Pairs with `thirdfy-mcp` **0.0.125**.
13
+
14
+ ## [0.2.75] - 2026-09-21
15
+
16
+ ### Fixed
17
+
18
+ - World ID complete commands no longer attach the tracking option bundle, which duplicated `--network` and `--proof` and crashed CLI startup. Deposit begin registers `--runtime-id`, `--amount-usd`, and `--destination-address`. Chain id comes from the chains bundle. Pairs with `thirdfy-mcp` **0.0.124**.
19
+
20
+ ## [0.2.74] - 2026-09-21
21
+
22
+ ### Added
23
+
24
+ - World ID deposit proof commands: `delegation world-id-begin`, `delegation world-id-complete`, `delegation world-id-deposit-begin`, `delegation world-id-deposit-complete`. Pins: `test/delegation-world-id.test.cjs`. Pairs with `thirdfy-mcp` **0.0.124** and Thirdfy API deposit receipt routes.
6
25
 
7
26
  ## [0.2.73] - 2026-09-21
8
27
 
package/README.md CHANGED
@@ -40,10 +40,10 @@ Run without global install:
40
40
  npx @thirdfy/agent-cli --help
41
41
  ```
42
42
 
43
- ## What's new in v0.2.73
43
+ ## What's new in v0.2.76
44
44
 
45
- - Hyperliquid Circle CCTP ids `quote_hyperliquid_cctp_deposit` and `deposit_hyperliquid_cctp`. Managed execute signs those on `params.sourceChainId` with no default chain.
46
- - Provider hints follow API `setupHints` and `get_hyperliquid_funding_status` `nextAction` instead of hardcoded Arbitrum / Li.Fi.
45
+ - Writes (orders, TP/SL updates, closes) get a 90s HTTP timeout instead of 30s. Override with `THIRDFY_CLI_WRITE_TIMEOUT_MS`.
46
+ - A write whose request times out is no longer resent. The CLI reports `WRITE_OUTCOME_UNKNOWN`: re-read positions before retrying.
47
47
  - See [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases for older versions.
48
48
 
49
49
  ## Quick start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.2.73",
3
+ "version": "0.2.76",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/help.mjs CHANGED
@@ -30,6 +30,10 @@ Core commands:
30
30
  thirdfy-agent delegation balance [--auth-token <token> | --owner-session-token <token>] --agent-key <key> [--chain-id <id>] [--json]
31
31
  thirdfy-agent delegation revoke [--auth-token <token> | --owner-session-token <token>] --agent-key <key> [--reason <text>] [--chain-id <id>] [--json]
32
32
  thirdfy-agent delegation redeem --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid|agent_wallet] [--estimated-amount-usd <usd>] [--json]
33
+ thirdfy-agent delegation world-id-begin [--auth-token <token> | --owner-session-token <token>] --agent-key <key> [--chain-id <id>] [--json]
34
+ thirdfy-agent delegation world-id-complete [--auth-token <token> | --owner-session-token <token>] --agent-key <key> --proof <json> [--chain-id <id>] [--json]
35
+ thirdfy-agent delegation world-id-deposit-begin [--auth-token <token> | --owner-session-token <token>] --org-id <uuid> --runtime-id <uuid> [--amount-usd <usd>] [--destination-address <addr>] [--deposit-signal <id>] [--chain-id <id>] [--json]
36
+ thirdfy-agent delegation world-id-deposit-complete [--auth-token <token> | --owner-session-token <token>] --org-id <uuid> --proof <json> [--chain-id <id>] [--json]
33
37
  thirdfy-agent managed wallet init [--auth-token <token> | --owner-session-token <token>] [--chain-id <id>] [--json]
34
38
  thirdfy-agent managed wallet grant [--auth-token <token> | --owner-session-token <token>] --agent-key <key> --token-address <addr> --max-usd-per-day <usd> [--chain-id <id>] [--period-duration <sec>] [--expiry-seconds <sec>] [--json]
35
39
  thirdfy-agent managed setup [--auth-token <token> | --owner-session-token <token>] --agent-key <key> [--run-mode thirdfy|hybrid|agent_wallet] [--token-address <addr>] [--max-usd-per-day <usd>] [--chain-id <id>] [--json]
@@ -412,6 +412,34 @@ export const CLI_MANIFEST = [
412
412
  options: ['auth', 'delegation'],
413
413
  description: 'Revoke delegation',
414
414
  },
415
+ {
416
+ path: ['delegation', 'world-id-begin'],
417
+ handler: 'commandDelegationWorldIdBegin',
418
+ tier: 'online',
419
+ options: ['auth', 'delegation', 'chains'],
420
+ description: 'Begin World ID proof for an agent delegation',
421
+ },
422
+ {
423
+ path: ['delegation', 'world-id-complete'],
424
+ handler: 'commandDelegationWorldIdComplete',
425
+ tier: 'online',
426
+ options: ['auth', 'delegation', 'chains'],
427
+ description: 'Complete World ID proof for an agent delegation',
428
+ },
429
+ {
430
+ path: ['delegation', 'world-id-deposit-begin'],
431
+ handler: 'commandDelegationWorldIdDepositBegin',
432
+ tier: 'online',
433
+ options: ['auth', 'delegation', 'chains'],
434
+ description: 'Begin World ID deposit proof for an organization',
435
+ },
436
+ {
437
+ path: ['delegation', 'world-id-deposit-complete'],
438
+ handler: 'commandDelegationWorldIdDepositComplete',
439
+ tier: 'online',
440
+ options: ['auth', 'delegation', 'chains'],
441
+ description: 'Complete World ID deposit proof for an organization',
442
+ },
415
443
  {
416
444
  path: ['delegation', 'redeem'],
417
445
  handler: 'commandDelegationRedeem',
@@ -20,5 +20,11 @@ export function attachDelegationOptions(cmd) {
20
20
  .option('--verify', 'verify on-chain')
21
21
  .option('--include-onchain', 'include on-chain data')
22
22
  .option('--onchain', 'alias for include on-chain')
23
- .option('--reason <text>', 'revoke reason');
23
+ .option('--reason <text>', 'revoke reason')
24
+ .option('--org-id <id>', 'EarnOrg organization id')
25
+ .option('--runtime-id <id>', 'EarnOrg runtime id')
26
+ .option('--amount-usd <usd>', 'deposit amount in USD')
27
+ .option('--destination-address <addr>', 'agent destination address')
28
+ .option('--deposit-signal <signal>', 'deposit proof signal (deposit:...)')
29
+ .option('--proof <json>', 'World ID proof JSON');
24
30
  }
@@ -108,7 +108,8 @@ export function createCondorCommands({ commandRun, printEnvelope }) {
108
108
  ...params,
109
109
  },
110
110
  };
111
- const data = await apiPost(ctx, '/api/v1/agent/governance/preflight', body, {
111
+ // Preflight only evaluates policy, so a timed-out request is safe to resend.
112
+ const data = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/governance/preflight', body, {
112
113
  'x-agent-api-key': agentApiKey,
113
114
  });
114
115
  printEnvelope({
@@ -326,6 +326,101 @@ async function commandDelegationBalance(ctx, flags, capabilities) {
326
326
  }
327
327
 
328
328
 
329
+ async function commandDelegationWorldIdBegin(ctx, flags, capabilities) {
330
+ const authHeaders = buildOwnerAuthHeaders(
331
+ flags,
332
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation world-id-begin'
333
+ );
334
+ const payload = {
335
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
336
+ chainId: Number(flags.chainId || 8453),
337
+ };
338
+ const response = await apiPost(ctx, '/api/v1/agent/delegation/world-id/begin', payload, {
339
+ ...authHeaders,
340
+ });
341
+ const success = Boolean(response?.success !== false);
342
+ printEnvelope({
343
+ success,
344
+ code: success ? 'DELEGATION_WORLD_ID_BEGIN_OK' : 'DELEGATION_WORLD_ID_BEGIN_FAILED',
345
+ message: response?.message || (success ? 'World ID agent challenge started' : 'World ID agent begin failed'),
346
+ data: response,
347
+ meta: { apiBase: ctx.apiBase, capabilitiesVersion: capabilities?.contractVersion || null },
348
+ });
349
+ }
350
+
351
+ async function commandDelegationWorldIdComplete(ctx, flags, capabilities) {
352
+ const authHeaders = buildOwnerAuthHeaders(
353
+ flags,
354
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation world-id-complete'
355
+ );
356
+ const payload = {
357
+ agentKey: requireFlag(flags, 'agentKey', 'Missing --agent-key'),
358
+ chainId: Number(flags.chainId || 8453),
359
+ proof: parseJsonFlag(flags, 'proof'),
360
+ };
361
+ const response = await apiPost(ctx, '/api/v1/agent/delegation/world-id/complete', payload, {
362
+ ...authHeaders,
363
+ });
364
+ const success = Boolean(response?.success !== false);
365
+ printEnvelope({
366
+ success,
367
+ code: success ? 'DELEGATION_WORLD_ID_COMPLETE_OK' : 'DELEGATION_WORLD_ID_COMPLETE_FAILED',
368
+ message: response?.message || (success ? 'World ID agent proof recorded' : 'World ID agent complete failed'),
369
+ data: response,
370
+ meta: { apiBase: ctx.apiBase, capabilitiesVersion: capabilities?.contractVersion || null },
371
+ });
372
+ }
373
+
374
+ async function commandDelegationWorldIdDepositBegin(ctx, flags, capabilities) {
375
+ const authHeaders = buildOwnerAuthHeaders(
376
+ flags,
377
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation world-id-deposit-begin'
378
+ );
379
+ const payload = {
380
+ orgId: requireFlag(flags, 'orgId', 'Missing --org-id'),
381
+ chainId: Number(flags.chainId || 8453),
382
+ };
383
+ if (flags.runtimeId) payload.runtimeId = String(flags.runtimeId).trim();
384
+ if (flags.amountUsd != null) payload.amountUsd = Number(flags.amountUsd);
385
+ if (flags.destinationAddress) payload.destinationAddress = String(flags.destinationAddress).trim();
386
+ if (flags.depositSignal) payload.depositSignal = String(flags.depositSignal).trim();
387
+ if (flags.idempotencyKey) payload.idempotencyKey = String(flags.idempotencyKey).trim();
388
+ const response = await apiPost(ctx, '/api/v1/agent/delegation/world-id/deposit/begin', payload, {
389
+ ...authHeaders,
390
+ });
391
+ const success = Boolean(response?.success !== false);
392
+ printEnvelope({
393
+ success,
394
+ code: success ? 'DELEGATION_WORLD_ID_DEPOSIT_BEGIN_OK' : 'DELEGATION_WORLD_ID_DEPOSIT_BEGIN_FAILED',
395
+ message: response?.message || (success ? 'World ID deposit challenge started' : 'World ID deposit begin failed'),
396
+ data: response,
397
+ meta: { apiBase: ctx.apiBase, capabilitiesVersion: capabilities?.contractVersion || null },
398
+ });
399
+ }
400
+
401
+ async function commandDelegationWorldIdDepositComplete(ctx, flags, capabilities) {
402
+ const authHeaders = buildOwnerAuthHeaders(
403
+ flags,
404
+ 'Missing --auth-token/--owner-session-token (or THIRDFY_AUTH_TOKEN/THIRDFY_OWNER_SESSION_TOKEN) for delegation world-id-deposit-complete'
405
+ );
406
+ const payload = {
407
+ orgId: requireFlag(flags, 'orgId', 'Missing --org-id'),
408
+ chainId: Number(flags.chainId || 8453),
409
+ proof: parseJsonFlag(flags, 'proof'),
410
+ };
411
+ const response = await apiPost(ctx, '/api/v1/agent/delegation/world-id/deposit/complete', payload, {
412
+ ...authHeaders,
413
+ });
414
+ const success = Boolean(response?.success !== false);
415
+ printEnvelope({
416
+ success,
417
+ code: success ? 'DELEGATION_WORLD_ID_DEPOSIT_COMPLETE_OK' : 'DELEGATION_WORLD_ID_DEPOSIT_COMPLETE_FAILED',
418
+ message: response?.message || (success ? 'World ID deposit proof recorded' : 'World ID deposit complete failed'),
419
+ data: response,
420
+ meta: { apiBase: ctx.apiBase, capabilitiesVersion: capabilities?.contractVersion || null },
421
+ });
422
+ }
423
+
329
424
  async function commandDelegationRedeem(ctx, flags, capabilities) {
330
425
  const selectedRunMode = getEffectiveRunMode(ctx, flags);
331
426
  if (!['thirdfy', 'hybrid', 'agent_wallet'].includes(selectedRunMode)) {
@@ -350,6 +445,10 @@ async function commandDelegationRedeem(ctx, flags, capabilities) {
350
445
  commandDelegationInspect,
351
446
  commandDelegationRevoke,
352
447
  commandDelegationBalance,
448
+ commandDelegationWorldIdBegin,
449
+ commandDelegationWorldIdComplete,
450
+ commandDelegationWorldIdDepositBegin,
451
+ commandDelegationWorldIdDepositComplete,
353
452
  commandDelegationRedeem,
354
453
  };
355
454
  }
@@ -122,7 +122,7 @@ async function commandTrackAction(ctx, flags, capabilities) {
122
122
  if (flags.decision) payload.decision = parseJsonFlag(flags, 'decision');
123
123
  if (flags.proof) payload.proof = parseJsonFlag(flags, 'proof');
124
124
 
125
- const response = await apiPost(ctx, '/api/v1/agent/telemetry/actions', payload);
125
+ const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/telemetry/actions', payload);
126
126
  printEnvelope({
127
127
  success: Boolean(response?.success !== false),
128
128
  code: response?.success === false ? 'TRACK_ACTION_FAILED' : 'TRACK_ACTION_OK',
@@ -1,4 +1,12 @@
1
1
  import { DEFAULT_TIMEOUT_MS } from './constants.mjs';
2
+ import { isReadOnlyActionName, isReadOnlyResolvedAction } from './readOnlyActions.mjs';
3
+
4
+ /**
5
+ * Default HTTP timeout for managed writes. A perps order or TP/SL update signs, broadcasts, and waits
6
+ * for a receipt inside one API call; that routinely takes longer than a read. Mirrors thirdfy-mcp
7
+ * `THIRDFY_API_WRITE_TIMEOUT_MS`.
8
+ */
9
+ const DEFAULT_WRITE_TIMEOUT_MS = 90_000;
2
10
 
3
11
  const LONG_RUNNING_ACTIONS = new Set([
4
12
  'complete_avantis_onboarding',
@@ -21,6 +29,10 @@ const LONG_RUNNING_ACTIONS = new Set([
21
29
  'swap',
22
30
  'execute_optimal_swap',
23
31
  'deposit_hyperliquid_cctp',
32
+ // Avantis market open/close: the API streams the batched order for up to 90s, then polls its
33
+ // tracking status for up to 90s more before it answers.
34
+ 'place_avantis_perps_order',
35
+ 'close_avantis_position',
24
36
  ]);
25
37
 
26
38
  function normalizeActionId(action) {
@@ -30,17 +42,51 @@ function normalizeActionId(action) {
30
42
  .toLowerCase();
31
43
  }
32
44
 
33
- /** Longer HTTP timeout for Avantis/Lighter onboarding, Avantis setup-status compose, and Morpho/Aave earn writes. */
34
- export function resolveActionTimeoutMs(action, baseTimeoutMs = DEFAULT_TIMEOUT_MS) {
45
+ function positiveEnvMs(...names) {
46
+ for (const name of names) {
47
+ const value = Number(process.env[name]);
48
+ if (Number.isFinite(value) && value > 0) return value;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ /** True when the action changes state (anything the read-only classifier does not claim). */
54
+ export function isWriteAction(action) {
55
+ const key = normalizeActionId(action);
56
+ return Boolean(key) && !isReadOnlyActionName(key);
57
+ }
58
+
59
+ /**
60
+ * HTTP timeout for one action. Long-running onboarding, earn, bridge, and compose actions get the
61
+ * long timeout. Other writes get the write timeout (90s). Reads keep the base timeout. Pass
62
+ * `options.write` when the caller already classified the action (catalog metadata).
63
+ */
64
+ export function resolveActionTimeoutMs(action, baseTimeoutMs = DEFAULT_TIMEOUT_MS, options = {}) {
35
65
  const base = Number.isFinite(baseTimeoutMs) && baseTimeoutMs > 0 ? baseTimeoutMs : DEFAULT_TIMEOUT_MS;
36
- if (!LONG_RUNNING_ACTIONS.has(normalizeActionId(action))) return base;
37
- const longMs = Number(
38
- process.env.THIRDFY_CLI_LONG_TIMEOUT_MS || process.env.THIRDFY_API_LONG_TIMEOUT_MS || 180_000,
39
- );
40
- const resolvedLong = Number.isFinite(longMs) && longMs > 0 ? longMs : 180_000;
41
- return Math.max(base, resolvedLong);
66
+ if (LONG_RUNNING_ACTIONS.has(normalizeActionId(action))) {
67
+ const longMs = positiveEnvMs('THIRDFY_CLI_LONG_TIMEOUT_MS', 'THIRDFY_API_LONG_TIMEOUT_MS') ?? 180_000;
68
+ return Math.max(base, longMs);
69
+ }
70
+ const write = typeof options.write === 'boolean' ? options.write : isWriteAction(action);
71
+ if (write) {
72
+ const writeMs =
73
+ positiveEnvMs('THIRDFY_CLI_WRITE_TIMEOUT_MS', 'THIRDFY_API_WRITE_TIMEOUT_MS') ?? DEFAULT_WRITE_TIMEOUT_MS;
74
+ return Math.max(base, writeMs);
75
+ }
76
+ return base;
42
77
  }
43
78
 
44
- export function withActionTimeout(ctx, action) {
45
- return { ...ctx, timeoutMs: resolveActionTimeoutMs(action, ctx.timeoutMs) };
79
+ /**
80
+ * Context for one action's HTTP calls. Accepts the action id or the resolved catalog action, whose
81
+ * metadata can mark a read the name does not. `writeAction` tells `requestJson` not to resend a
82
+ * write whose request timed out: the API may have broadcast it, so the outcome is unknown until
83
+ * state is re-read.
84
+ */
85
+ export function withActionTimeout(ctx, actionOrResolved) {
86
+ const resolved = actionOrResolved && typeof actionOrResolved === 'object' ? actionOrResolved : null;
87
+ const action = resolved ? resolved.resolvedAction : actionOrResolved;
88
+ const write = resolved
89
+ ? Boolean(normalizeActionId(action)) && !isReadOnlyResolvedAction(resolved)
90
+ : isWriteAction(action);
91
+ return { ...ctx, timeoutMs: resolveActionTimeoutMs(action, ctx.timeoutMs, { write }), writeAction: write };
46
92
  }
package/src/core/http.mjs CHANGED
@@ -12,6 +12,44 @@ function isRetryableApiStatus(statusCode) {
12
12
  return statusCode === 502 || statusCode === 503 || statusCode === 504;
13
13
  }
14
14
 
15
+ function isTimeoutError(error) {
16
+ const message = String(error?.message || '').toLowerCase();
17
+ return error?.name === 'AbortError' || message.includes('abort') || message.includes('timeout');
18
+ }
19
+
20
+ /**
21
+ * A write whose request timed out may already be on chain. Resending it can double a trade, so the
22
+ * caller gets `WRITE_OUTCOME_UNKNOWN` and re-reads state before any retry.
23
+ */
24
+ function writeOutcomeUnknownError(route, ctx, cause) {
25
+ const error = new Error(
26
+ `WRITE_OUTCOME_UNKNOWN: ${route} timed out after ${ctx.timeoutMs}ms; the write may have executed. Re-read state before retrying.`,
27
+ );
28
+ error.code = 'WRITE_OUTCOME_UNKNOWN';
29
+ error.statusCode = 504;
30
+ error.payload = { error: 'WRITE_OUTCOME_UNKNOWN', nextAction: 'reread_state_before_retry', timeoutMs: ctx.timeoutMs };
31
+ error.cause = cause;
32
+ return error;
33
+ }
34
+
35
+ /**
36
+ * Whether a request that timed out may be sent again. GETs, validation-only previews, and calls the
37
+ * caller marks `writeAction: false` are safe. Preflight sends `executionLane: validation_only`
38
+ * (not a `validationOnly` field). Any other POST changes state (an order, a venue funding, a
39
+ * delegation grant), so the default is not to resend it.
40
+ */
41
+ function resendAfterTimeoutIsSafe(ctx, init) {
42
+ if (ctx?.writeAction === true) return false;
43
+ if (ctx?.writeAction === false) return true;
44
+ if (String(init?.method || 'GET').toUpperCase() !== 'POST') return true;
45
+ try {
46
+ const body = JSON.parse(String(init?.body || '{}'));
47
+ return body?.validationOnly === true || body?.executionLane === 'validation_only';
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
15
53
  function isRetryableApiError(error) {
16
54
  const statusCode = Number(error?.statusCode || 0);
17
55
  const message = String(error?.message || '').toLowerCase();
@@ -77,6 +115,9 @@ export async function requestJson(ctx, route, init) {
77
115
  return await requestJsonOnce(ctx, route, init);
78
116
  } catch (error) {
79
117
  lastError = error;
118
+ if (isTimeoutError(error) && !resendAfterTimeoutIsSafe(ctx, init)) {
119
+ throw writeOutcomeUnknownError(route, ctx, error);
120
+ }
80
121
  if (attempt === 0 && isRetryableApiError(error)) {
81
122
  await sleep(250 + Math.floor(Math.random() * 250));
82
123
  continue;
@@ -192,7 +192,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
192
192
  runMode: 'self',
193
193
  mirrorOnly: false,
194
194
  });
195
- const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
195
+ const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
196
196
  const normalized = normalizeIntentResponse(response);
197
197
  const blockedCount = normalized.blocked || 0;
198
198
  if (!normalized.success || blockedCount > 0) {
@@ -240,7 +240,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
240
240
  mirrorOnly: false,
241
241
  hybridWalletMode: options.hybridWalletMode,
242
242
  });
243
- const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
243
+ const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
244
244
  const normalized = normalizeIntentResponse(response);
245
245
  const blockedCount = normalized.blocked || 0;
246
246
  if (!normalized.success || blockedCount > 0) {
@@ -267,7 +267,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
267
267
  mirrorOnly: false,
268
268
  hybridWalletMode: 'agent_wallet',
269
269
  });
270
- const managedResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', managedPayload);
270
+ const managedResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', managedPayload);
271
271
  const managedNormalized = normalizeIntentResponse(managedResponse);
272
272
  const mirrorPayload = buildIntentPayload(flags, {
273
273
  validationOnly: true,
@@ -277,7 +277,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
277
277
  mirrorOnly: true,
278
278
  hybridWalletMode: 'agent_wallet',
279
279
  });
280
- const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
280
+ const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
281
281
  const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
282
282
  return {
283
283
  message:
@@ -304,7 +304,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
304
304
  runMode,
305
305
  mirrorOnly: runMode === 'hybrid',
306
306
  });
307
- const response = await apiPost(ctx, '/api/v1/agent/execute-intent', payload);
307
+ const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
308
308
  const normalized = normalizeIntentResponse(response);
309
309
  return {
310
310
  message: normalized.success
@@ -403,7 +403,7 @@ async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
403
403
 
404
404
  async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'thirdfy') {
405
405
  const skipPreflight = Boolean(options?.skipPreflight);
406
- const actionCtx = withActionTimeout(ctx, resolved.resolvedAction);
406
+ const actionCtx = withActionTimeout(ctx, resolved);
407
407
  if (!skipPreflight) {
408
408
  const preflightPayload = buildIntentPayload(flags, {
409
409
  validationOnly: true,
@@ -413,7 +413,12 @@ async function executeThirdfyRun(ctx, flags, resolved, options, runMode = 'third
413
413
  mirrorOnly: false,
414
414
  hybridWalletMode: options?.hybridWalletMode,
415
415
  });
416
- const preflight = await apiPost(actionCtx, '/api/v1/agent/execute-intent', preflightPayload);
416
+ // validationOnly never writes, so a timed-out preflight is safe to resend.
417
+ const preflight = await apiPost(
418
+ { ...actionCtx, writeAction: false },
419
+ '/api/v1/agent/execute-intent',
420
+ preflightPayload,
421
+ );
417
422
  const preflightNormalized = normalizeIntentResponse(preflight);
418
423
  const blockedCount = preflightNormalized.blocked || 0;
419
424
  if (!preflightNormalized.success || blockedCount > 0) {
@@ -503,7 +508,7 @@ async function executeManagedWalletRun(ctx, flags, resolved, options) {
503
508
  runMode: 'agent_wallet',
504
509
  forceIdempotency: true,
505
510
  });
506
- const actionCtx = withActionTimeout(ctx, resolved.resolvedAction);
511
+ const actionCtx = withActionTimeout(ctx, resolved);
507
512
  const response = await apiPost(actionCtx, '/api/v1/agent/execute', payload);
508
513
  if (shouldFallbackAgentWalletToExecuteIntent(resolved, response)) {
509
514
  const intentResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, preflight);
@@ -539,7 +544,7 @@ async function executeSelfRun(ctx, flags, resolved, options) {
539
544
  runMode: 'self',
540
545
  mirrorOnly: false,
541
546
  });
542
- const preflight = await apiPost(ctx, '/api/v1/agent/execute-intent', preflightPayload);
547
+ const preflight = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', preflightPayload);
543
548
  const preflightNormalized = normalizeIntentResponse(preflight);
544
549
  const blockedCount = preflightNormalized.blocked || 0;
545
550
  if (!preflightNormalized.success || blockedCount > 0) {
@@ -561,7 +566,8 @@ async function executeSelfRun(ctx, flags, resolved, options) {
561
566
  return normalized;
562
567
  }
563
568
  const buildTxPayload = buildBuildTxPayload(flags, resolved.resolvedAction);
564
- const buildTx = await apiPost(ctx, '/api/v1/agent/build-tx', buildTxPayload);
569
+ // build-tx returns an unsigned transaction; nothing is broadcast, so a timeout may be resent.
570
+ const buildTx = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/build-tx', buildTxPayload);
565
571
  return normalizeIntentResponse({
566
572
  ...buildTx,
567
573
  status: buildTx?.status || 'ready',
@@ -598,7 +604,7 @@ async function executeHybridRun(ctx, flags, resolved, options) {
598
604
  mirrorOnly: true,
599
605
  hybridWalletMode,
600
606
  });
601
- const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
607
+ const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
602
608
  const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
603
609
  return normalizeIntentResponse({
604
610
  success: Boolean(managedResult.success && mirrorNormalized.success),
@@ -624,7 +630,7 @@ async function executeHybridRun(ctx, flags, resolved, options) {
624
630
  runMode: 'hybrid',
625
631
  mirrorOnly: true,
626
632
  });
627
- const mirrorResponse = await apiPost(ctx, '/api/v1/agent/execute-intent', mirrorPayload);
633
+ const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
628
634
  const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
629
635
  return normalizeIntentResponse({
630
636
  success: Boolean(selfResult.success && mirrorNormalized.success),