@sanlabs/sanbox-cli 0.0.11 → 0.0.13
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/README.md +45 -13
- package/dist/activity.js +18 -1
- package/dist/api.js +10 -8
- package/dist/args.js +2 -1
- package/dist/cli.js +99 -54
- package/dist/output.js +12 -3
- package/dist/runs.js +15 -12
- package/dist/version.js +1 -1
- package/dist/watch.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,8 +15,14 @@ installed_cli_version="$(sanbox --version)"
|
|
|
15
15
|
test "$installed_cli_version" = "$latest_cli_version"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Always use the latest published CLI. CLI 0.0.
|
|
19
|
-
|
|
18
|
+
Always use the latest published CLI. CLI 0.0.13 allows service templates to start without an
|
|
19
|
+
initial task, including OpenCode Computers that are ready for later SDK steering, and supports
|
|
20
|
+
Custom OpenAI Proxy providers when creating templates, including Browser Use templates.
|
|
21
|
+
CLI 0.0.12 adopted the Stop, Resume, and Delete lifecycle,
|
|
22
|
+
separates run state from execution outcome, retains scrubbed deleted-run tombstones, and adds
|
|
23
|
+
administrator-confirmed template deletion.
|
|
24
|
+
CLI 0.0.11 added OpenCode Computer templates and short-lived native SDK connections for HTTP, SSE
|
|
25
|
+
streaming, and interactive steering.
|
|
20
26
|
CLI 0.0.10 added per-run Hermes email and Telegram channels, Supabase user authorization, and
|
|
21
27
|
removed the retired run-chat commands.
|
|
22
28
|
CLI 0.0.9 added user login for private SSH access to supported running sandboxes.
|
|
@@ -113,10 +119,20 @@ sanbox templates create \
|
|
|
113
119
|
Omit both `--channel` flags for a WebUI-only computer. Channel credentials never belong to the
|
|
114
120
|
template.
|
|
115
121
|
|
|
122
|
+
Delete a template when it should no longer be available for new runs:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
sanbox templates delete <template-id-or-slug> --force --json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Deletion preserves existing runs and retained sandboxes. It requires organization-admin access
|
|
129
|
+
and `--force` so scripts cannot remove a template accidentally.
|
|
130
|
+
|
|
116
131
|
## Create A Browser Use Template
|
|
117
132
|
|
|
118
133
|
Browser Use runs local headless Chromium inside the Firecracker sandbox. It requires OpenAI,
|
|
119
|
-
Anthropic, Google Gemini,
|
|
134
|
+
Anthropic, Google Gemini, Hetzner Inference, or a Custom OpenAI Proxy and at least one
|
|
135
|
+
explicit browser target:
|
|
120
136
|
|
|
121
137
|
```bash
|
|
122
138
|
sanbox templates create \
|
|
@@ -139,6 +155,8 @@ allowed downloads. Browser Use templates are one-shot agent executions.
|
|
|
139
155
|
|
|
140
156
|
## Run Idempotently
|
|
141
157
|
|
|
158
|
+
Task templates require a positional task or `--task`. Service templates may omit it:
|
|
159
|
+
|
|
142
160
|
```bash
|
|
143
161
|
sanbox run "Review this repo and write output/report.md" \
|
|
144
162
|
--input src/ \
|
|
@@ -151,8 +169,18 @@ sanbox run "Review this repo and write output/report.md" \
|
|
|
151
169
|
--input src/ \
|
|
152
170
|
--wait \
|
|
153
171
|
--json
|
|
172
|
+
|
|
173
|
+
sanbox run \
|
|
174
|
+
--template "<service-template-id>" \
|
|
175
|
+
--external-run-id "<stable-service-id>" \
|
|
176
|
+
--json
|
|
154
177
|
```
|
|
155
178
|
|
|
179
|
+
A taskless OpenCode Computer starts `opencode serve` without making an initial model call. A
|
|
180
|
+
taskless Hermes service starts its gateways and waits for WebUI or configured channel messages.
|
|
181
|
+
Supplying instructions starts the initial OpenCode task or seeds Hermes's persistent operating
|
|
182
|
+
context, preserving the existing behavior.
|
|
183
|
+
|
|
156
184
|
Repeat `--input` for files, directories, or globs. The CLI excludes common secrets and applies `.sanboxignore`. `--include` is a deprecated compatibility alias.
|
|
157
185
|
|
|
158
186
|
For a Hermes template that allows `email`, optionally request an address from one of the AgentMail
|
|
@@ -200,7 +228,7 @@ callback lands on a Sanbox-hosted completion page, so a CLI demo does not need a
|
|
|
200
228
|
Customer integrations may use `--return-url <https-url>` when its origin matches the return origin
|
|
201
229
|
configured on the organization Supabase connection.
|
|
202
230
|
|
|
203
|
-
Reuse the same external ID when retrying an ambiguous submission. To stream activity, replace `--wait --json` with `--jsonl`. Ctrl-C detaches without
|
|
231
|
+
Reuse the same external ID when retrying an ambiguous submission. To stream activity, replace `--wait --json` with `--jsonl`. Ctrl-C detaches without stopping unless `--stop-on-interrupt` is supplied.
|
|
204
232
|
|
|
205
233
|
## Inspect And Recover
|
|
206
234
|
|
|
@@ -243,20 +271,23 @@ working when the exact sandbox session stops, the link expires, or it is revoked
|
|
|
243
271
|
when created and must be handled as a bearer secret. See
|
|
244
272
|
[Live Filesystem Access](../docs/live-filesystem-access.md) for the HTTP contract and exclusions.
|
|
245
273
|
|
|
246
|
-
## Resume Or
|
|
274
|
+
## Stop, Resume, Or Delete A Run
|
|
247
275
|
|
|
248
276
|
```bash
|
|
249
277
|
sanbox runs get <run-id> --json
|
|
250
278
|
sanbox runs resume <run-id> --wait --json
|
|
251
279
|
sanbox runs share <run-id> --expires 1h --json
|
|
252
|
-
sanbox runs
|
|
280
|
+
sanbox runs stop <run-id> --wait --json
|
|
281
|
+
sanbox runs delete <run-id> --yes --json
|
|
253
282
|
```
|
|
254
283
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
`
|
|
259
|
-
|
|
284
|
+
Stop terminates compute and durably syncs the workspace. It does not retain RAM, process state, or
|
|
285
|
+
network connections. Resume fresh-boots the pinned runtime artifact with the retained workspace; it
|
|
286
|
+
does not silently replay a completed task. `state` is the single run phase; inspect
|
|
287
|
+
`latest_execution.outcome` for the last bounded execution outcome and `workspace.saved_at` for disk
|
|
288
|
+
persistence proof. Stopped runs have no automatic TTL. Delete permanently removes the workspace
|
|
289
|
+
after archiving usage. The control plane retains a scrubbed run tombstone, but normal CLI run listings
|
|
290
|
+
exclude deleted runs.
|
|
260
291
|
|
|
261
292
|
## SSH Into A Running Sandbox
|
|
262
293
|
|
|
@@ -274,8 +305,9 @@ authenticated WebSocket. The temporary key is deleted when the connection closes
|
|
|
274
305
|
user token is not passed to the OpenSSH child process.
|
|
275
306
|
|
|
276
307
|
OpenCode and Browser Use runs are one-shot agent executions. OpenCode Computer keeps its private
|
|
277
|
-
server running
|
|
278
|
-
|
|
308
|
+
server running so it can accept later steering; when an initial task is supplied, Sanbox submits it
|
|
309
|
+
after the server becomes ready. A persisted task workspace can still be fresh-booted manually for
|
|
310
|
+
inspection after the run stops.
|
|
279
311
|
|
|
280
312
|
## Connect The OpenCode SDK
|
|
281
313
|
|
package/dist/activity.js
CHANGED
|
@@ -35,6 +35,23 @@ export const formatActivityLine = (event, runCreatedAt) => {
|
|
|
35
35
|
return `${elapsed(runCreatedAt, event.created_at).padStart(8)} ${event.kind.padEnd(22)} ${bounded}`.trimEnd();
|
|
36
36
|
};
|
|
37
37
|
const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
const legacyGuestEventKinds = new Set([
|
|
39
|
+
"log.stdout",
|
|
40
|
+
"log.stderr",
|
|
41
|
+
"runner.event_invalid",
|
|
42
|
+
"sandbox.stderr",
|
|
43
|
+
"anthropic.worker.stdout",
|
|
44
|
+
"anthropic.worker.stderr"
|
|
45
|
+
]);
|
|
46
|
+
const legacyEventSource = (event) => {
|
|
47
|
+
if (legacyGuestEventKinds.has(event.kind)) {
|
|
48
|
+
return { plane: "sandbox", trust: "legacy_guest_content" };
|
|
49
|
+
}
|
|
50
|
+
if (event.kind === "sandbox.status") {
|
|
51
|
+
return { plane: "unknown", trust: "legacy_mixed_origin" };
|
|
52
|
+
}
|
|
53
|
+
return { plane: "control_plane", trust: "control_plane" };
|
|
54
|
+
};
|
|
38
55
|
const safeEventData = (value) => {
|
|
39
56
|
if (Array.isArray(value))
|
|
40
57
|
return value.map(safeEventData);
|
|
@@ -61,7 +78,7 @@ export const activityEnvelope = (event) => {
|
|
|
61
78
|
kind: event.kind,
|
|
62
79
|
level: event.level,
|
|
63
80
|
summary: event.message,
|
|
64
|
-
source: normalized ? event.payload.source :
|
|
81
|
+
source: normalized ? event.payload.source : legacyEventSource(event),
|
|
65
82
|
sequence: normalized && typeof event.payload.sequence === "number" ? event.payload.sequence : null,
|
|
66
83
|
correlation: normalized && isRecord(event.payload.correlation) ? event.payload.correlation : {},
|
|
67
84
|
data: safeEventData(normalized ? event.payload.data : event.payload),
|
package/dist/api.js
CHANGED
|
@@ -174,6 +174,9 @@ export class SanboxClient {
|
|
|
174
174
|
async validateTemplate(templateId) {
|
|
175
175
|
return this.request(await this.orgPath(`/templates/${encodeURIComponent(templateId)}/validate`));
|
|
176
176
|
}
|
|
177
|
+
async deleteTemplate(templateId) {
|
|
178
|
+
return this.request(await this.orgPath(`/templates/${encodeURIComponent(templateId)}`), { method: "DELETE" });
|
|
179
|
+
}
|
|
177
180
|
async createTemplate(body) {
|
|
178
181
|
return this.request(await this.orgPath("/templates"), {
|
|
179
182
|
method: "POST",
|
|
@@ -198,20 +201,19 @@ export class SanboxClient {
|
|
|
198
201
|
async listEvents(runId, afterEventId = 0, limit = 200, signal) {
|
|
199
202
|
return this.request(`${await this.orgPath(`/runs/${encodeURIComponent(runId)}/events`)}?after_event_id=${afterEventId}&limit=${limit}`, { signal });
|
|
200
203
|
}
|
|
201
|
-
async
|
|
202
|
-
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/
|
|
204
|
+
async stopRun(runId) {
|
|
205
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/stop`), {
|
|
203
206
|
method: "POST",
|
|
204
207
|
body: "{}"
|
|
205
208
|
});
|
|
206
209
|
}
|
|
207
|
-
async
|
|
208
|
-
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}
|
|
209
|
-
method: "
|
|
210
|
-
body: "{}"
|
|
210
|
+
async deleteRun(runId) {
|
|
211
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}`), {
|
|
212
|
+
method: "DELETE"
|
|
211
213
|
});
|
|
212
214
|
}
|
|
213
|
-
async
|
|
214
|
-
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/
|
|
215
|
+
async resumeRun(runId) {
|
|
216
|
+
return this.request(await this.orgPath(`/runs/${encodeURIComponent(runId)}/resume`), {
|
|
215
217
|
method: "POST",
|
|
216
218
|
body: "{}"
|
|
217
219
|
});
|
package/dist/args.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -10,8 +10,8 @@ import { loginWithBrowser, openBrowser, revokeUserSession } from "./deviceLogin.
|
|
|
10
10
|
import { CliError, commandAction, consoleAction } from "./errors.js";
|
|
11
11
|
import { parseFileAccessExpiry } from "./fileAccess.js";
|
|
12
12
|
import { previewInputs } from "./inputs.js";
|
|
13
|
-
import { printError, printJsonlError, printRun, printSuccess, publicRun, publicRunPayload } from "./output.js";
|
|
14
|
-
import { createRun, isTerminalRun, readTasks, runPool, stableBatchId, waitForRun,
|
|
13
|
+
import { isoUtcTimestamp, printError, printJsonlError, printRun, printSuccess, publicRun, publicRunPayload } from "./output.js";
|
|
14
|
+
import { createRun, isTerminalRun, runSucceeded, readTasks, runPool, stableBatchId, waitForRun, waitForRunState } from "./runs.js";
|
|
15
15
|
import { version } from "./version.js";
|
|
16
16
|
import { WatchInterruptedError, watchRun } from "./watch.js";
|
|
17
17
|
import { openSSH, runSSHProxy } from "./ssh.js";
|
|
@@ -41,8 +41,10 @@ Commands:
|
|
|
41
41
|
sanbox templates get <template-id> [--json]
|
|
42
42
|
sanbox templates validate <template-id> [--json]
|
|
43
43
|
sanbox templates create --name "..." --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--channel email|telegram] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
|
|
44
|
+
sanbox templates delete <template-id> --force [--json]
|
|
44
45
|
sanbox run "task" --template <template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
45
46
|
sanbox run --task "..." --template <template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--input <path>] [--wait | --watch] [--json | --jsonl]
|
|
47
|
+
sanbox run --template <service-template-id> [--email-address <address>] [--telegram-bot-token <token>] [--telegram-allowed-user <id>] [--json]
|
|
46
48
|
sanbox batch --tasks tasks.json --template <template-id> [--input <path>] [--max-parallel 5] [--wait] [--json]
|
|
47
49
|
sanbox runs list [--limit 50] [--json]
|
|
48
50
|
sanbox runs get <run-id> [--json]
|
|
@@ -52,10 +54,10 @@ Commands:
|
|
|
52
54
|
sanbox runs shares <run-id> [--json]
|
|
53
55
|
sanbox runs unshare <run-id> <access-point-id> [--json]
|
|
54
56
|
sanbox runs download <run-id> --output <directory> [--artifact <path>] [--overwrite] [--json]
|
|
55
|
-
sanbox runs watch <run-id> [--after-event-id 0] [--view activity|logs|compact] [--jsonl]
|
|
56
|
-
sanbox runs
|
|
57
|
+
sanbox runs watch <run-id> [--after-event-id 0] [--view activity|logs|compact] [--stop-on-interrupt] [--jsonl]
|
|
58
|
+
sanbox runs stop <run-id> [--wait] [--json]
|
|
57
59
|
sanbox runs resume <run-id> [--wait] [--json]
|
|
58
|
-
sanbox runs
|
|
60
|
+
sanbox runs delete <run-id> --yes [--json]
|
|
59
61
|
sanbox runs supabase authorize <run-id> [--open] [--return-url <https-url>] [--json]
|
|
60
62
|
sanbox opencode connect <run-id> [--expires 1h] [--json]
|
|
61
63
|
sanbox opencode connections list <run-id> [--json]
|
|
@@ -92,8 +94,10 @@ const runHelp = `Sanbox run
|
|
|
92
94
|
Usage:
|
|
93
95
|
sanbox run "Review these files" --template <template-id> --input report.pdf --input data/ --wait
|
|
94
96
|
sanbox run --task "Review these files" --template <template-id> --input report.pdf --wait --json
|
|
97
|
+
sanbox run --template <service-template-id> --json
|
|
95
98
|
|
|
96
99
|
Options:
|
|
100
|
+
--task <instruction> Required for task templates; optional for service templates.
|
|
97
101
|
--input <path> File, directory, or glob to upload. Repeatable.
|
|
98
102
|
--template <id> Template id or slug. Required unless SANBOX_TEMPLATE or project config sets it.
|
|
99
103
|
--external-run-id <id> Idempotency key for retries.
|
|
@@ -102,12 +106,12 @@ Options:
|
|
|
102
106
|
--telegram-bot-token <token> Optional Telegram bot token; SANBOX_TELEGRAM_BOT_TOKEN is also supported.
|
|
103
107
|
--telegram-allowed-user <id> Numeric Telegram user allowed to reach this run. Repeatable.
|
|
104
108
|
--dry-run Preview included files without creating a run.
|
|
105
|
-
--wait Poll until terminal
|
|
106
|
-
--watch Stream activity until terminal
|
|
109
|
+
--wait Poll until a terminal run state.
|
|
110
|
+
--watch Stream activity until a terminal run state.
|
|
107
111
|
--jsonl Stream one versioned activity event per line. Implies --watch.
|
|
108
|
-
--view <name> activity,
|
|
112
|
+
--view <name> activity, compact, or historical logs. Default: activity.
|
|
109
113
|
--after-event-id <id> Resume after an event cursor. Default: 0.
|
|
110
|
-
--
|
|
114
|
+
--stop-on-interrupt Request Stop when Ctrl-C is pressed.
|
|
111
115
|
--json Print JSON.
|
|
112
116
|
`;
|
|
113
117
|
const runsSupabaseHelp = `Sanbox Supabase run authorization
|
|
@@ -160,6 +164,7 @@ Usage:
|
|
|
160
164
|
sanbox templates get <template-id> [--json]
|
|
161
165
|
sanbox templates validate <template-id> [--json]
|
|
162
166
|
sanbox templates create --name "Code review" --model-provider <provider-id> --model <model-id> [--harness opencode|hermes|browser-use] [--mode task|computer] [--browser-domain <hostname>] [--llm-budget-usd <amount>] [--json]
|
|
167
|
+
sanbox templates delete <template-id> --force [--json]
|
|
163
168
|
|
|
164
169
|
Template creation requires an exact provider id and that provider's exact model id.
|
|
165
170
|
LiteLLM budgets are optional USD amounts and apply separately to each run.
|
|
@@ -172,6 +177,7 @@ or Anthropic and at least one repeatable --browser-domain hostname or leading wi
|
|
|
172
177
|
Optional controls: --browser-max-steps, --browser-step-timeout-seconds,
|
|
173
178
|
--browser-vision-mode auto|always|never, --browser-viewport WIDTHxHEIGHT,
|
|
174
179
|
--browser-download-policy allow|deny, and --browser-additional-instructions.
|
|
180
|
+
Deleting a template requires --force. Existing runs and their retained sandboxes are preserved.
|
|
175
181
|
`;
|
|
176
182
|
const agentInstructions = `# Operate Sanbox Autonomously
|
|
177
183
|
|
|
@@ -225,7 +231,8 @@ sanbox run "Investigate one focused task and write output/report.md" \\
|
|
|
225
231
|
|
|
226
232
|
Reuse the same --external-run-id after ambiguous failures. Retry network errors, HTTP 429, HTTP 5xx,
|
|
227
233
|
and workspace_busy with bounded backoff. Do not retry other 4xx errors unless next_actions directs
|
|
228
|
-
recovery.
|
|
234
|
+
recovery. A run is inactive when state is stopped; inspect latest_execution.outcome for the most
|
|
235
|
+
recent bounded execution outcome.
|
|
229
236
|
|
|
230
237
|
Recover and retrieve results:
|
|
231
238
|
\`\`\`bash
|
|
@@ -254,20 +261,20 @@ Manage the retained sandbox independently of its agent harness:
|
|
|
254
261
|
sanbox runs get <run-id> --json
|
|
255
262
|
sanbox runs resume <run-id> --wait --json
|
|
256
263
|
sanbox runs share <run-id> --expires 1h --json
|
|
257
|
-
sanbox runs
|
|
264
|
+
sanbox runs stop <run-id> --wait --json
|
|
265
|
+
sanbox runs delete <run-id> --yes --json
|
|
258
266
|
\`\`\`
|
|
259
267
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
positive snapshot_generation.
|
|
268
|
+
Stop terminates compute and durably syncs the workspace without retaining RAM or VM state. Resume
|
|
269
|
+
fresh-boots the pinned runtime artifact with that workspace; it does not restore process state or
|
|
270
|
+
silently replay a finished task. Delete permanently removes the workspace after archiving usage.
|
|
264
271
|
|
|
265
272
|
For independent fan-out, use \`sanbox batch\` with a stable external_run_id per task and keep the
|
|
266
273
|
client alive until submission completes.
|
|
267
274
|
|
|
268
|
-
Do not claim completion until
|
|
269
|
-
Report run/external/template IDs,
|
|
270
|
-
|
|
275
|
+
Do not claim completion until state is stopped, latest_execution.outcome is completed, and required
|
|
276
|
+
artifacts are downloaded and verified. Report run/external/template IDs, run state, execution outcome, workspace save time,
|
|
277
|
+
artifact paths/digests, and blockers. The CLI excludes common secrets by default; add
|
|
271
278
|
.sanboxignore for project rules.
|
|
272
279
|
`;
|
|
273
280
|
const cwd = () => process.cwd();
|
|
@@ -446,6 +453,7 @@ const flagSets = {
|
|
|
446
453
|
"templates.list": commonFlags,
|
|
447
454
|
"templates.get": commonFlags,
|
|
448
455
|
"templates.validate": commonFlags,
|
|
456
|
+
"templates.delete": [...commonFlags, "force"],
|
|
449
457
|
"templates.create": [
|
|
450
458
|
...commonFlags,
|
|
451
459
|
"name",
|
|
@@ -468,7 +476,7 @@ const flagSets = {
|
|
|
468
476
|
...commonFlags, "task", "input", "template", "external-run-id", "email-address",
|
|
469
477
|
"supabase-user-id",
|
|
470
478
|
"telegram-bot-token", "telegram-allowed-user",
|
|
471
|
-
"dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "
|
|
479
|
+
"dry-run", "wait", "watch", "jsonl", "view", "after-event-id", "stop-on-interrupt",
|
|
472
480
|
"poll-interval-ms", "event-page-size", "timeout-seconds", "verbose"
|
|
473
481
|
],
|
|
474
482
|
batch: [
|
|
@@ -484,12 +492,12 @@ const flagSets = {
|
|
|
484
492
|
"runs.unshare": commonFlags,
|
|
485
493
|
"runs.download": [...commonFlags, "output", "artifact", "overwrite"],
|
|
486
494
|
"runs.watch": [
|
|
487
|
-
...commonFlags, "after-event-id", "view", "jsonl", "
|
|
495
|
+
...commonFlags, "after-event-id", "view", "jsonl", "stop-on-interrupt",
|
|
488
496
|
"poll-interval-ms", "event-page-size", "timeout-seconds"
|
|
489
497
|
],
|
|
490
|
-
"runs.
|
|
498
|
+
"runs.stop": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
499
|
+
"runs.delete": [...commonFlags, "yes"],
|
|
491
500
|
"runs.resume": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
492
|
-
"runs.pause": [...commonFlags, "wait", "poll-interval-ms", "timeout-seconds"],
|
|
493
501
|
"runs.supabase": [...commonFlags, "open", "return-url"],
|
|
494
502
|
"opencode.connect": [...commonFlags, "expires"],
|
|
495
503
|
"opencode.connections": commonFlags,
|
|
@@ -598,6 +606,7 @@ const validatePositionals = (command, flags) => {
|
|
|
598
606
|
"templates.list": 2,
|
|
599
607
|
"templates.get": 3,
|
|
600
608
|
"templates.validate": 3,
|
|
609
|
+
"templates.delete": 3,
|
|
601
610
|
"templates.create": 2,
|
|
602
611
|
batch: 1,
|
|
603
612
|
"runs.list": 2,
|
|
@@ -609,9 +618,9 @@ const validatePositionals = (command, flags) => {
|
|
|
609
618
|
"runs.unshare": 4,
|
|
610
619
|
"runs.download": 3,
|
|
611
620
|
"runs.watch": 3,
|
|
612
|
-
"runs.
|
|
621
|
+
"runs.stop": 3,
|
|
622
|
+
"runs.delete": 3,
|
|
613
623
|
"runs.resume": 3,
|
|
614
|
-
"runs.pause": 3,
|
|
615
624
|
"runs.supabase": 4,
|
|
616
625
|
"opencode.connect": 3,
|
|
617
626
|
"opencode.connections": 5,
|
|
@@ -658,7 +667,7 @@ const watchRunWithOutput = async (client, runId, flags, initialPayload) => {
|
|
|
658
667
|
const onInterrupt = () => controller.abort();
|
|
659
668
|
process.once("SIGINT", onInterrupt);
|
|
660
669
|
if (!jsonl)
|
|
661
|
-
process.stdout.write(`Watching run ${runId}. Ctrl-C detaches without
|
|
670
|
+
process.stdout.write(`Watching run ${runId}. Ctrl-C detaches without stopping.\n`);
|
|
662
671
|
try {
|
|
663
672
|
return await watchRun(client, runId, {
|
|
664
673
|
afterEventId: integerFlag(flags, "after-event-id", 0, 0, Number.MAX_SAFE_INTEGER),
|
|
@@ -680,9 +689,9 @@ const watchRunWithOutput = async (client, runId, flags, initialPayload) => {
|
|
|
680
689
|
catch (error) {
|
|
681
690
|
if (!(error instanceof WatchInterruptedError))
|
|
682
691
|
throw error;
|
|
683
|
-
if (hasFlag(flags, "
|
|
684
|
-
await client.
|
|
685
|
-
process.stderr.write(`
|
|
692
|
+
if (hasFlag(flags, "stop-on-interrupt")) {
|
|
693
|
+
await client.stopRun(runId);
|
|
694
|
+
process.stderr.write(`Stop requested for run ${runId}.\n`);
|
|
686
695
|
}
|
|
687
696
|
else {
|
|
688
697
|
process.stderr.write(`Detached from run ${runId}; the run is still active.\n`);
|
|
@@ -720,8 +729,6 @@ const commandRun = async (command, flags) => {
|
|
|
720
729
|
throw new CliError("conflicting_arguments", "Use either --task or a positional task, not both.");
|
|
721
730
|
}
|
|
722
731
|
const task = runTask(command, flags);
|
|
723
|
-
if (!task)
|
|
724
|
-
throw new Error("--task or a positional task is required.");
|
|
725
732
|
if (hasFlag(flags, "wait") && wantsWatch(flags)) {
|
|
726
733
|
throw new Error("--wait cannot be combined with --watch or --jsonl.");
|
|
727
734
|
}
|
|
@@ -765,7 +772,7 @@ const commandRun = async (command, flags) => {
|
|
|
765
772
|
try {
|
|
766
773
|
payload = await createRun(client, {
|
|
767
774
|
cwd: cwd(),
|
|
768
|
-
instruction: task,
|
|
775
|
+
instruction: task || undefined,
|
|
769
776
|
inputs: flagList(flags, "input"),
|
|
770
777
|
externalRunId: flagString(flags, "external-run-id") || undefined,
|
|
771
778
|
supabaseUserId: supabaseUserId || undefined,
|
|
@@ -785,7 +792,7 @@ const commandRun = async (command, flags) => {
|
|
|
785
792
|
payload = watched;
|
|
786
793
|
if (!hasFlag(flags, "jsonl"))
|
|
787
794
|
printRun(payload);
|
|
788
|
-
if (isTerminalRun(payload.run) && payload.run
|
|
795
|
+
if (isTerminalRun(payload.run) && !runSucceeded(payload.run))
|
|
789
796
|
process.exitCode = 2;
|
|
790
797
|
return;
|
|
791
798
|
}
|
|
@@ -803,7 +810,7 @@ const commandRun = async (command, flags) => {
|
|
|
803
810
|
}
|
|
804
811
|
else
|
|
805
812
|
printRun(payload);
|
|
806
|
-
if (hasFlag(flags, "wait") && isTerminalRun(payload.run) && payload.run
|
|
813
|
+
if (hasFlag(flags, "wait") && isTerminalRun(payload.run) && !runSucceeded(payload.run))
|
|
807
814
|
process.exitCode = 2;
|
|
808
815
|
};
|
|
809
816
|
const commandBatch = async (flags) => {
|
|
@@ -850,7 +857,7 @@ const commandBatch = async (flags) => {
|
|
|
850
857
|
printSuccess("batch.create", output, jsonContext(client));
|
|
851
858
|
else
|
|
852
859
|
results.forEach(printRun);
|
|
853
|
-
if (wait && results.some((result) => result.run
|
|
860
|
+
if (wait && results.some((result) => !runSucceeded(result.run)))
|
|
854
861
|
process.exitCode = 2;
|
|
855
862
|
};
|
|
856
863
|
const commandAuthCheck = async (flags) => {
|
|
@@ -1013,7 +1020,7 @@ const commandAnthropicEnvironments = async (command, flags) => {
|
|
|
1013
1020
|
for (const environment of payload.environments) {
|
|
1014
1021
|
process.stdout.write(`${environment.environment_id}\t${environment.status}` +
|
|
1015
1022
|
`\tworker=${environment.assigned_worker_id || "unassigned"}` +
|
|
1016
|
-
`\tverified=${environment.last_verified_at
|
|
1023
|
+
`\tverified=${environment.last_verified_at ? isoUtcTimestamp(environment.last_verified_at) : "never"}\n`);
|
|
1017
1024
|
}
|
|
1018
1025
|
return;
|
|
1019
1026
|
}
|
|
@@ -1033,7 +1040,7 @@ const commandAnthropicEnvironments = async (command, flags) => {
|
|
|
1033
1040
|
const environment = payload.environment;
|
|
1034
1041
|
process.stdout.write(`${environment.environment_id} ${environment.status}` +
|
|
1035
1042
|
` worker=${environment.assigned_worker_id || "unassigned"}` +
|
|
1036
|
-
` verified=${environment.last_verified_at
|
|
1043
|
+
` verified=${environment.last_verified_at ? isoUtcTimestamp(environment.last_verified_at) : "never"}\n`);
|
|
1037
1044
|
return;
|
|
1038
1045
|
}
|
|
1039
1046
|
if (action === "connect") {
|
|
@@ -1192,6 +1199,9 @@ const commandTemplates = async (command, flags) => {
|
|
|
1192
1199
|
if (harness === "opencode" && mode === "computer" && llmBudgetUsd !== undefined) {
|
|
1193
1200
|
throw new CliError("opencode_computer_budget_unsupported", "OpenCode Computer templates do not support --llm-budget-usd yet.");
|
|
1194
1201
|
}
|
|
1202
|
+
if (modelProvider === "custom" && llmBudgetUsd !== undefined) {
|
|
1203
|
+
throw new CliError("custom_provider_budget_unsupported", "Custom OpenAI Proxy providers do not support --llm-budget-usd yet.");
|
|
1204
|
+
}
|
|
1195
1205
|
const channels = [...new Set(flagList(flags, "channel").map((value) => value.trim()).filter(Boolean))];
|
|
1196
1206
|
if (channels.some((channel) => channel !== "email" && channel !== "telegram")) {
|
|
1197
1207
|
throw new CliError("invalid_hermes_channels", "--channel must be email or telegram.");
|
|
@@ -1212,8 +1222,9 @@ const commandTemplates = async (command, flags) => {
|
|
|
1212
1222
|
modelProvider !== "openai" &&
|
|
1213
1223
|
modelProvider !== "anthropic" &&
|
|
1214
1224
|
modelProvider !== "google" &&
|
|
1215
|
-
modelProvider !== "hetzner"
|
|
1216
|
-
|
|
1225
|
+
modelProvider !== "hetzner" &&
|
|
1226
|
+
modelProvider !== "custom") {
|
|
1227
|
+
throw new CliError("browser_use_provider_unsupported", "Browser Use templates require --model-provider openai, anthropic, google, hetzner, or custom.");
|
|
1217
1228
|
}
|
|
1218
1229
|
const browserDomains = harness === "browser-use"
|
|
1219
1230
|
? parseBrowserDomains(flagList(flags, "browser-domain"))
|
|
@@ -1280,7 +1291,39 @@ const commandTemplates = async (command, flags) => {
|
|
|
1280
1291
|
`${payload.template.llm_budget_usd ? ` budget=$${payload.template.llm_budget_usd}/run` : ""}\n`);
|
|
1281
1292
|
return;
|
|
1282
1293
|
}
|
|
1283
|
-
|
|
1294
|
+
if (action === "delete") {
|
|
1295
|
+
const id = requiredPositional(command[2], "template_id_required", "templates delete requires a template id or slug.");
|
|
1296
|
+
if (!hasFlag(flags, "force")) {
|
|
1297
|
+
throw new CliError("confirmation_required", "templates delete requires --force.", {
|
|
1298
|
+
details: { template_id: id },
|
|
1299
|
+
nextActions: [commandAction(["sanbox", "templates", "delete", id, "--force", "--json"], "Delete the template from this organization while preserving existing runs.")]
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
let payload;
|
|
1303
|
+
try {
|
|
1304
|
+
payload = await client.deleteTemplate(id);
|
|
1305
|
+
}
|
|
1306
|
+
catch (error) {
|
|
1307
|
+
if (error instanceof SanboxApiError) {
|
|
1308
|
+
throw new CliError(error.code, error.message, {
|
|
1309
|
+
status: error.status,
|
|
1310
|
+
details: { template_id: id },
|
|
1311
|
+
nextActions: error.code === "template_not_found"
|
|
1312
|
+
? [commandAction(["sanbox", "templates", "list", "--json"], "List templates available to the API key's organization.")]
|
|
1313
|
+
: []
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1318
|
+
const nextActions = [commandAction(["sanbox", "templates", "list", "--json"], "Confirm that the template is no longer available for new runs.")];
|
|
1319
|
+
if (hasFlag(flags, "json")) {
|
|
1320
|
+
printSuccess("templates.delete", payload, jsonContext(client), nextActions);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
process.stdout.write(`deleted ${payload.template_id}\n`);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
throw new CliError("templates_action_required", "templates requires an action: list, get, validate, create, or delete.");
|
|
1284
1327
|
};
|
|
1285
1328
|
const commandRuns = async (command, flags) => {
|
|
1286
1329
|
const client = makeClient(flags);
|
|
@@ -1295,7 +1338,7 @@ const commandRuns = async (command, flags) => {
|
|
|
1295
1338
|
return;
|
|
1296
1339
|
}
|
|
1297
1340
|
for (const run of payload.runs) {
|
|
1298
|
-
process.stdout.write(`${run.id}\t${run.
|
|
1341
|
+
process.stdout.write(`${run.id}\t${run.state}\t${isoUtcTimestamp(run.created_at)}\t${run.instruction.replace(/\s+/g, " ").slice(0, 100)}\n`);
|
|
1299
1342
|
}
|
|
1300
1343
|
return;
|
|
1301
1344
|
}
|
|
@@ -1367,7 +1410,7 @@ const commandRuns = async (command, flags) => {
|
|
|
1367
1410
|
return;
|
|
1368
1411
|
}
|
|
1369
1412
|
for (const accessPoint of payload.access_points) {
|
|
1370
|
-
process.stdout.write(`${accessPoint.id}\t${accessPoint.status}\t${accessPoint.expires_at}\t${accessPoint.name}\n`);
|
|
1413
|
+
process.stdout.write(`${accessPoint.id}\t${accessPoint.status}\t${isoUtcTimestamp(accessPoint.expires_at)}\t${accessPoint.name}\n`);
|
|
1371
1414
|
}
|
|
1372
1415
|
return;
|
|
1373
1416
|
}
|
|
@@ -1435,24 +1478,26 @@ const commandRuns = async (command, flags) => {
|
|
|
1435
1478
|
return;
|
|
1436
1479
|
if (!hasFlag(flags, "jsonl"))
|
|
1437
1480
|
printRun(payload);
|
|
1438
|
-
if (isTerminalRun(payload.run) && payload.run
|
|
1481
|
+
if (isTerminalRun(payload.run) && !runSucceeded(payload.run))
|
|
1439
1482
|
process.exitCode = 2;
|
|
1440
1483
|
return;
|
|
1441
1484
|
}
|
|
1442
|
-
if (action === "
|
|
1443
|
-
|
|
1485
|
+
if (action === "delete") {
|
|
1486
|
+
if (!hasFlag(flags, "yes")) {
|
|
1487
|
+
throw new CliError("confirmation_required", "Run deletion permanently removes its workspace. Re-run with --yes.");
|
|
1488
|
+
}
|
|
1489
|
+
const payload = await client.deleteRun(runId);
|
|
1444
1490
|
if (hasFlag(flags, "json"))
|
|
1445
|
-
printSuccess("runs.
|
|
1491
|
+
printSuccess("runs.delete", publicRunPayload(payload), jsonContext(client));
|
|
1446
1492
|
else
|
|
1447
|
-
|
|
1493
|
+
process.stdout.write(`deletion requested for run ${runId}\n`);
|
|
1448
1494
|
return;
|
|
1449
1495
|
}
|
|
1450
|
-
if (action === "
|
|
1451
|
-
const submitted = action === "resume"
|
|
1452
|
-
|
|
1453
|
-
: await client.pauseRun(runId);
|
|
1496
|
+
if (action === "stop" || action === "resume") {
|
|
1497
|
+
const submitted = action === "resume" ? await client.resumeRun(runId) : await client.stopRun(runId);
|
|
1498
|
+
const target = action === "resume" ? "running" : "stopped";
|
|
1454
1499
|
const payload = hasFlag(flags, "wait")
|
|
1455
|
-
? await
|
|
1500
|
+
? await waitForRunState(client, runId, target, {
|
|
1456
1501
|
pollIntervalMs: integerFlag(flags, "poll-interval-ms", 2000, 250, 60_000),
|
|
1457
1502
|
timeoutSeconds: integerFlag(flags, "timeout-seconds", 1800, 1, 604_800)
|
|
1458
1503
|
})
|
|
@@ -1464,7 +1509,7 @@ const commandRuns = async (command, flags) => {
|
|
|
1464
1509
|
printRun(payload);
|
|
1465
1510
|
}
|
|
1466
1511
|
else {
|
|
1467
|
-
process.stdout.write(
|
|
1512
|
+
process.stdout.write(`${action} requested for run ${runId}\n`);
|
|
1468
1513
|
}
|
|
1469
1514
|
return;
|
|
1470
1515
|
}
|
|
@@ -1486,7 +1531,7 @@ const commandOpenCode = async (command, flags) => {
|
|
|
1486
1531
|
]);
|
|
1487
1532
|
return;
|
|
1488
1533
|
}
|
|
1489
|
-
process.stdout.write(`URL ${payload.url}\nTOKEN ${payload.access_token}\nEXPIRES ${payload.connection.expires_at}\n`);
|
|
1534
|
+
process.stdout.write(`URL ${payload.url}\nTOKEN ${payload.access_token}\nEXPIRES ${isoUtcTimestamp(payload.connection.expires_at)}\n`);
|
|
1490
1535
|
return;
|
|
1491
1536
|
}
|
|
1492
1537
|
if (action === "connections") {
|
|
@@ -1502,7 +1547,7 @@ const commandOpenCode = async (command, flags) => {
|
|
|
1502
1547
|
return;
|
|
1503
1548
|
}
|
|
1504
1549
|
for (const connection of payload.connections) {
|
|
1505
|
-
process.stdout.write(`${connection.id}\t${connection.status}\t${connection.expires_at}\t${connection.token_prefix}\n`);
|
|
1550
|
+
process.stdout.write(`${connection.id}\t${connection.status}\t${isoUtcTimestamp(connection.expires_at)}\t${connection.token_prefix}\n`);
|
|
1506
1551
|
}
|
|
1507
1552
|
return;
|
|
1508
1553
|
}
|
package/dist/output.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { CliError } from "./errors.js";
|
|
2
2
|
import { SanboxApiError } from "./api.js";
|
|
3
|
+
// Human-readable CLI output uses one canonical ISO 8601 UTC representation.
|
|
4
|
+
// Structured JSON output preserves the API payload unchanged.
|
|
5
|
+
export const isoUtcTimestamp = (value) => {
|
|
6
|
+
const date = new Date(value);
|
|
7
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : value;
|
|
8
|
+
};
|
|
3
9
|
export const printJson = (value) => {
|
|
4
10
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
5
11
|
};
|
|
@@ -73,18 +79,21 @@ export const publicRunPayload = (payload) => ({
|
|
|
73
79
|
});
|
|
74
80
|
export const summarizeRun = (payload) => {
|
|
75
81
|
const { run } = payload;
|
|
82
|
+
const execution = run.latest_execution;
|
|
76
83
|
const templateId = run.template_id || run.workload_id;
|
|
77
84
|
const selection = [
|
|
78
85
|
templateId ? `template=${templateId}` : "",
|
|
79
86
|
run.provider_id ? `provider=${run.provider_id}` : "",
|
|
80
87
|
run.model_id ? `model=${run.model_id}` : "",
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
`state=${run.state}`,
|
|
89
|
+
execution?.outcome
|
|
90
|
+
? `result=${execution.outcome}`
|
|
91
|
+
: "",
|
|
83
92
|
run.email?.address ? `email=${run.email.address}` : run.email ? `email=${run.email.status}` : "",
|
|
84
93
|
run.channels?.telegram.enabled ? "telegram=enabled" : "",
|
|
85
94
|
run.supabase ? `supabase=${run.supabase.status}` : ""
|
|
86
95
|
].filter(Boolean).join(" ");
|
|
87
|
-
return `${run.id}
|
|
96
|
+
return `${run.id}${selection ? ` ${selection}` : ""}${execution?.exit_code == null ? "" : ` exit=${execution.exit_code}`}${execution?.error ? ` error=${execution.error}` : ""}`;
|
|
88
97
|
};
|
|
89
98
|
export const printRun = (payload) => {
|
|
90
99
|
process.stdout.write(`${summarizeRun(payload)}\n`);
|
package/dist/runs.js
CHANGED
|
@@ -2,8 +2,8 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildInputBundle } from "./inputs.js";
|
|
5
|
-
const
|
|
6
|
-
export const
|
|
5
|
+
export const isTerminalRun = (run) => run.state === "stopped" || run.state === "deleting" || run.state === "deleted";
|
|
6
|
+
export const runSucceeded = (run) => run.latest_execution?.outcome === "completed";
|
|
7
7
|
export const waitForRun = async (client, runId, options = {}) => {
|
|
8
8
|
const pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
9
9
|
const deadline = Date.now() + (options.timeoutSeconds ?? 1800) * 1000;
|
|
@@ -16,20 +16,23 @@ export const waitForRun = async (client, runId, options = {}) => {
|
|
|
16
16
|
}
|
|
17
17
|
return payload;
|
|
18
18
|
};
|
|
19
|
-
export const
|
|
19
|
+
export const waitForRunState = async (client, runId, target, options = {}) => {
|
|
20
20
|
const pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
21
21
|
const deadline = Date.now() + (options.timeoutSeconds ?? 1800) * 1000;
|
|
22
22
|
let payload = await client.getRun(runId);
|
|
23
|
-
while (payload.run.
|
|
24
|
-
if (payload.run.
|
|
25
|
-
throw new Error(
|
|
23
|
+
while (payload.run.state !== target) {
|
|
24
|
+
if (payload.run.state === "deleting" || payload.run.state === "deleted") {
|
|
25
|
+
throw new Error("Run is being deleted.");
|
|
26
26
|
}
|
|
27
|
-
if (target === "running" && payload.run.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
if (target === "running" && payload.run.state === "stopped") {
|
|
28
|
+
const lifecycleMessage = typeof payload.run.lifecycle_error?.message === "string"
|
|
29
|
+
? payload.run.lifecycle_error.message
|
|
30
|
+
: null;
|
|
31
|
+
const detail = lifecycleMessage || payload.run.latest_execution?.error;
|
|
32
|
+
throw new Error(`Run ${runId} stopped before it became running${detail ? `: ${detail}` : "."}`);
|
|
32
33
|
}
|
|
34
|
+
if (Date.now() > deadline)
|
|
35
|
+
throw new Error(`Timed out waiting for run ${runId} to become ${target}.`);
|
|
33
36
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
34
37
|
payload = await client.getRun(runId);
|
|
35
38
|
}
|
|
@@ -49,7 +52,7 @@ export const createRun = async (client, options) => {
|
|
|
49
52
|
return client.createRun({
|
|
50
53
|
external_run_id: options.externalRunId,
|
|
51
54
|
workload_id: options.templateId,
|
|
52
|
-
instruction: options.instruction,
|
|
55
|
+
...(options.instruction ? { instruction: options.instruction } : {}),
|
|
53
56
|
...(options.supabaseUserId ? { supabase_user_id: options.supabaseUserId } : {}),
|
|
54
57
|
...(options.emailAddress ? { email_address: options.emailAddress } : {}),
|
|
55
58
|
...(options.telegramBotToken ? {
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "0.0.
|
|
1
|
+
export const version = "0.0.13";
|
package/dist/watch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SanboxApiError } from "./api.js";
|
|
2
|
-
const
|
|
3
|
-
const terminalEventKinds = new Set(["run.completed", "run.failed", "run.
|
|
2
|
+
const terminalStates = new Set(["stopped", "deleting", "deleted"]);
|
|
3
|
+
const terminalEventKinds = new Set(["run.completed", "run.failed", "run.stopped"]);
|
|
4
4
|
const retryableNetworkCodes = new Set(["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH"]);
|
|
5
5
|
export class WatchInterruptedError extends Error {
|
|
6
6
|
constructor() {
|
|
@@ -37,7 +37,7 @@ const assertActive = (signal, deadline, now, runId) => {
|
|
|
37
37
|
if (now() >= deadline)
|
|
38
38
|
throw new Error(`Timed out watching run ${runId}.`);
|
|
39
39
|
};
|
|
40
|
-
export const isTerminalStatus = (
|
|
40
|
+
export const isTerminalStatus = (state) => terminalStates.has(state);
|
|
41
41
|
export const watchEventsUntil = async (client, runId, options) => {
|
|
42
42
|
const pageSize = Math.max(1, Math.min(500, Math.floor(options.pageSize ?? 200)));
|
|
43
43
|
const pollIntervalMs = Math.max(1, Math.floor(options.pollIntervalMs ?? 2000));
|
|
@@ -156,7 +156,7 @@ export const watchRun = async (client, runId, options) => {
|
|
|
156
156
|
fetchAnotherPage = reportedMore && cursor > previousCursor;
|
|
157
157
|
}
|
|
158
158
|
const payload = await request(() => client.getRun(runId, options.signal));
|
|
159
|
-
if (isTerminalStatus(payload.run.
|
|
159
|
+
if (isTerminalStatus(payload.run.state)) {
|
|
160
160
|
emptyTerminalReads = deliveredThisCycle === 0 ? emptyTerminalReads + 1 : 0;
|
|
161
161
|
const requiredEmptyReads = sawTerminalEvent ? 1 : 2;
|
|
162
162
|
if (emptyTerminalReads >= requiredEmptyReads)
|