@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 +20 -1
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/cli/help.mjs +4 -0
- package/src/cli/manifest.mjs +28 -0
- package/src/cli/options/delegation.mjs +7 -1
- package/src/commands/condor.mjs +2 -1
- package/src/commands/delegation.mjs +99 -0
- package/src/commands/telemetry.mjs +1 -1
- package/src/core/actionTimeouts.mjs +56 -10
- package/src/core/http.mjs +41 -0
- package/src/runtime/execution/runners.mjs +18 -12
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
|
-
## [
|
|
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.
|
|
43
|
+
## What's new in v0.2.76
|
|
44
44
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
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
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]
|
package/src/cli/manifest.mjs
CHANGED
|
@@ -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
|
}
|
package/src/commands/condor.mjs
CHANGED
|
@@ -108,7 +108,8 @@ export function createCondorCommands({ commandRun, printEnvelope }) {
|
|
|
108
108
|
...params,
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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 (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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),
|