@shardflux/cli 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +42 -3
- package/dist/args.d.ts +1 -1
- package/dist/args.js +29 -5
- package/dist/commands.js +69 -5
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +13 -6
- package/dist/format.d.ts +5 -5
- package/dist/format.js +29 -0
- package/dist/http.d.ts +1 -10
- package/dist/http.js +6 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +8 -6
- package/dist/timing.d.ts +12 -0
- package/dist/timing.js +46 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -38,6 +38,29 @@ New organizations can start on a trial: 60 days at Developer limits, no card. Th
|
|
|
38
38
|
- An interrupted file-first `ws exec` says `(an execution runs to completion)`; `mode_not_available` drops "yet";
|
|
39
39
|
`--open` no longer says "best effort"; `templates init --help` no longer names an internal document.
|
|
40
40
|
|
|
41
|
+
## 0.7.0 (release candidate)
|
|
42
|
+
|
|
43
|
+
Completed exec output streams are drained before releasing their HTTP connections, with a bounded cleanup if a peer does not close.
|
|
44
|
+
|
|
45
|
+
Uses SDK 0.12.0 and its pooled transport. Adds --labels/--idle-policy to open, --labels to list, ws labels, ws idle, ws exec-start --stdin-open and ws exec-input --offset. Protocol-error JSON includes source. Resume recovers failed workspaces; completed deletion permits key reuse.
|
|
46
|
+
|
|
47
|
+
Requires the QM integration backend release for labels, failed recovery, key reuse and pipe stdin. No production deployment has occurred from this branch.
|
|
48
|
+
|
|
49
|
+
### Instant suspend: durable storage
|
|
50
|
+
|
|
51
|
+
- `shard ws suspend --wait` still returns when the suspend succeeds (sealed on its host); a finished suspend or fork
|
|
52
|
+
now prints `durable: yes (<n> ms after the suspend)` or `durable: in progress ...` when the result says so.
|
|
53
|
+
- `shard ws suspend --durable`: waits until `result.durable` is true (implies `--wait`, shares `--timeout`). A copy
|
|
54
|
+
that cannot be made exits 6 with code `durability_lost` (operation id, `details.state` `lost`, `details.reason`); a
|
|
55
|
+
timeout exits 5 with `details.durable` false and `details.state` `pending`. `--durable` with `--when-idle` or
|
|
56
|
+
`--cancel-when-idle` is a usage error.
|
|
57
|
+
- `shard operations wait <id> --durable`, and a `durable` line in `operations get|wait` for suspends and forks.
|
|
58
|
+
- `--timing --json`: `server` gains `durable`, `suspend_path`, `durability` and `lost_suspend`.
|
|
59
|
+
- A resume (or a command's wake) whose result carries `lost_suspend` prints a note on stderr naming the restored
|
|
60
|
+
checkpoint and its time.
|
|
61
|
+
- A suspend-when-idle canceled `workspace_active` says the workspace was in use and keeps running.
|
|
62
|
+
- The `ws suspend` summary no longer says "durable checkpoint".
|
|
63
|
+
|
|
41
64
|
## 0.6.1
|
|
42
65
|
|
|
43
66
|
Needs `@shardflux/sdk` 0.11.0 or later.
|
package/README.md
CHANGED
|
@@ -387,9 +387,10 @@ shows the status; `shard version --check` asks now (`--json`: `{"cli", "sdk", "u
|
|
|
387
387
|
| `files search <id\|key> <path> <pattern> [--regex] [-i\|--ignore-case] [--include G]... [--exclude G]... [--max N]` | Search file contents under a directory (or in one file; gitignore-style globs): `path:line:column: text` per match (0.5.0+). |
|
|
388
388
|
| `files patch <id\|key> <path> (--old T --new T)... [--replace-all] \| --content-file F\|- [--expected-revision R] [--if-revision N]` | Replace exact text (each `--old` must occur once unless `--replace-all`), or the whole content, atomically; `R` is a revision from `files stat --revision` or `absent` (0.5.0+). `--if-revision` (0.5.0+): file-first workspaces only. |
|
|
389
389
|
| `workspaces suspend\|resume <id\|key> [--wait] [--timeout T] [--timing]` | Lifecycle operation. Returns immediately unless you pass `--wait`. |
|
|
390
|
+
| `workspaces suspend <id\|key> --durable [--timeout T]` | (0.7.0+) Suspend and wait until the copy is in durable storage. See "Durable storage". |
|
|
390
391
|
| `workspaces suspend <id\|key> --when-idle D` / `--cancel-when-idle` | (0.5.1+) Suspend once the workspace has been idle for `D` (0 to 1h; `0` = as soon as it is idle, 0.6.1+), or cancel that. See "Suspend when idle". |
|
|
391
392
|
| `workspaces fork <id\|key> <new-key> [caps] [--wait]` | Fork into a new key. |
|
|
392
|
-
| `workspaces delete <id\|key> --yes [--wait]` | Delete the workspace. Tool access ends at once.
|
|
393
|
+
| `workspaces delete <id\|key> --yes [--wait]` | Delete the workspace. Tool access ends at once. After deletion finishes, the key can open a new workspace ID. |
|
|
393
394
|
| `workspaces close <id\|key> [--wait]` | End a session workspace now: it is deleted, and the key then opens a new workspace. A persistent workspace is refused (`not_session`). |
|
|
394
395
|
| `workspaces reset <id\|key> --yes [--wait]` | Layered workspaces: wipe every change and restart on the template. The previous state stays restorable for 7 days. |
|
|
395
396
|
| `workspaces save-as-template <id\|key> --template <slug> [--display-name N] [--description T] [--checkpoint ID] [--default-lifetime L] [--default-idle-timeout D] [--acknowledge PATH]... [--no-auto-publish] [--wait]` | Save a layered workspace as the next version of an organization template. `--wait` follows the build. |
|
|
@@ -792,11 +793,49 @@ printf %s "$NEW_KEY" | shard secrets rotate OPENAI_API_KEY
|
|
|
792
793
|
| 130 | interrupted (Ctrl-C). Waits stop at once, and exec sends a cancel to the session. A file-first execution runs to completion, and the error names the `shard executions get` command for its result. |
|
|
793
794
|
| exec, executions get | the command's own code: 124 if it timed out, 128+N if killed by signal N. Failures before the command ran use the codes above; use `--json` to tell them apart. |
|
|
794
795
|
|
|
796
|
+
## Durable storage (0.7.0+)
|
|
797
|
+
|
|
798
|
+
`shard ws suspend --wait` returns as soon as the workspace is sealed on its host, typically in a few hundred ms, and
|
|
799
|
+
its RAM and CPU are released then. The copy lands in durable storage right after, typically within a second:
|
|
800
|
+
|
|
801
|
+
```sh
|
|
802
|
+
shard ws suspend acme/demo --wait
|
|
803
|
+
# Suspending acme/demo (<id>): operation <op> suspend succeeded
|
|
804
|
+
# durable: in progress, typically within a second (wait with: shard operations wait <op> --durable)
|
|
805
|
+
shard ws suspend acme/demo --durable # returns once it is durable: "durable: yes (435 ms after the suspend)"
|
|
806
|
+
shard operations wait <op> --durable # the same for a suspend or fork you already started
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
- `--durable` implies `--wait` and shares its `--timeout`.
|
|
810
|
+
- `--json` prints the operation as the API returns it: `result.durable` and `result.durability` (`state`
|
|
811
|
+
`pending` or `durable`, `local_commit_at`, `durable_at`, ...). `--timing` adds `durable`, `suspend_path` and
|
|
812
|
+
`durability` to `server`.
|
|
813
|
+
- `operations get|wait` show a `durable` line for suspends and forks. A fork of a running workspace reports its copy the
|
|
814
|
+
same way.
|
|
815
|
+
- The exit codes and errors of `--durable` are in the CLI reference on docs.shardflux.dev.
|
|
816
|
+
|
|
795
817
|
## HTTP connections
|
|
796
818
|
|
|
797
|
-
|
|
798
|
-
connections.
|
|
819
|
+
`shard` 0.7.0 uses the SDK’s pooled HTTP/1.1 transport on Node 26. Custom fetch is preserved. `SHARDFLUX_HTTP_KEEPALIVE=0` forces connection closure for diagnosis; `=1` opts into native pooling.
|
|
799
820
|
|
|
800
821
|
## License
|
|
801
822
|
|
|
802
823
|
Apache-2.0
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
## Integration controls (0.7.0+)
|
|
827
|
+
|
|
828
|
+
```sh
|
|
829
|
+
shard ws open qm/project-42 --template python-node-browser --labels '{"scope":"project-42"}' --idle-policy never
|
|
830
|
+
shard ws list --labels '{"scope":"project-42"}' --json
|
|
831
|
+
shard ws labels qm/project-42 --set '{"scope":"project-42","owner":"qm"}'
|
|
832
|
+
shard ws idle qm/project-42 --json
|
|
833
|
+
shard ws idle qm/project-42 --keepalive 60
|
|
834
|
+
shard ws idle qm/project-42 --policy adaptive
|
|
835
|
+
shard ws suspend qm/project-42 --when-idle 2s
|
|
836
|
+
shard ws exec-start qm/project-42 --stdin-open --session-id qm-worker-1 -- cat
|
|
837
|
+
shard ws exec-input qm/project-42 qm-worker-1 hello --offset 0
|
|
838
|
+
shard ws exec-input qm/project-42 qm-worker-1 --offset 5 --close
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
Use the byte offset in each acknowledgement, including partial writes. `ws resume` can recover a failed workspace with its existing ID and disk. A completed delete frees the key; old IDs remain deleted. Protocol-error JSON includes `source`; HTTP 404 alone does not mean a workspace was deleted.
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util';
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
|
-
export const CLI_VERSION = '0.
|
|
15
|
+
export const CLI_VERSION = '0.7.0';
|
|
16
16
|
export const GLOBAL_OPTIONS = {
|
|
17
17
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
18
18
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -189,6 +189,8 @@ const KEY_COMMANDS = [
|
|
|
189
189
|
positionals: [{ name: 'key', required: true }],
|
|
190
190
|
options: {
|
|
191
191
|
template: { type: 'string', value: '<slug>', description: 'Template slug (required), e.g. python-node-browser.' },
|
|
192
|
+
labels: { type: 'string', value: '<json>', description: 'Replace workspace labels with a JSON object of strings.' },
|
|
193
|
+
'idle-policy': { type: 'string', value: '<policy>', description: 'adaptive, never or fixed:<seconds>.' },
|
|
192
194
|
secret: {
|
|
193
195
|
type: 'string',
|
|
194
196
|
multiple: true,
|
|
@@ -228,6 +230,7 @@ const KEY_COMMANDS = [
|
|
|
228
230
|
options: {
|
|
229
231
|
state: { type: 'string', value: '<state>', description: 'Observed state filter (running, suspended, creating, ...).' },
|
|
230
232
|
prefix: { type: 'string', value: '<key-prefix>', description: 'Only keys starting with this prefix.' },
|
|
233
|
+
labels: { type: 'string', value: '<json>', description: 'Exact label matches (JSON object; all pairs must match).' },
|
|
231
234
|
...LIFETIME_FILTER,
|
|
232
235
|
'include-deleted': { type: 'boolean', description: 'Include deleted workspaces and ended sessions (tombstones).' },
|
|
233
236
|
all: { type: 'boolean', description: 'Fetch every page.' },
|
|
@@ -235,6 +238,22 @@ const KEY_COMMANDS = [
|
|
|
235
238
|
cursor: { type: 'string', value: '<cursor>', description: 'Continue from a previous page.' },
|
|
236
239
|
},
|
|
237
240
|
},
|
|
241
|
+
{ path: ['workspaces', 'labels'], summary: 'Read or replace workspace labels', positionals: [REF], options: {
|
|
242
|
+
set: { type: 'string', value: '<json>', description: 'Replace all labels; {} clears them.' },
|
|
243
|
+
} },
|
|
244
|
+
{ path: ['workspaces', 'idle'], summary: 'Inspect idle status, set policy or declare a keepalive', positionals: [REF], options: {
|
|
245
|
+
policy: { type: 'string', value: '<policy>', description: 'adaptive, never, fixed:<seconds>, or default to clear the override.' },
|
|
246
|
+
keepalive: { type: 'string', value: '<seconds>', description: 'Declare ongoing work for this many seconds.' },
|
|
247
|
+
} },
|
|
248
|
+
{ path: ['workspaces', 'exec-start'], summary: 'Start a background exec session with optional pipe stdin', positionals: [REF, { name: 'command', required: true, variadic: true }], options: {
|
|
249
|
+
'stdin-open': { type: 'boolean', description: 'Keep pipe stdin open for exec-input.' },
|
|
250
|
+
'session-id': { type: 'string', value: '<id>', description: 'Caller-chosen id to deduplicate starts.' },
|
|
251
|
+
cwd: { type: 'string', value: '<dir>', description: 'Absolute working directory.' },
|
|
252
|
+
} },
|
|
253
|
+
{ path: ['workspaces', 'exec-input'], summary: 'Write pipe stdin at its acknowledged byte offset', positionals: [REF, { name: 'session-id', required: true }, { name: 'data' }], options: {
|
|
254
|
+
offset: { type: 'string', value: '<bytes>', description: 'Required acknowledged offset (0 initially).' },
|
|
255
|
+
close: { type: 'boolean', description: 'Send EOF after these bytes are accepted.' },
|
|
256
|
+
} },
|
|
238
257
|
{ path: ['workspaces', 'get'], summary: 'Show one workspace (state, template, startup)', positionals: [REF], options: {} },
|
|
239
258
|
{ path: ['workspaces', 'inputs'], summary: 'Show the text inputs of a workspace (secret inputs are bound secrets: shard ws secrets)', positionals: [REF], options: {} },
|
|
240
259
|
{
|
|
@@ -269,15 +288,16 @@ const KEY_COMMANDS = [
|
|
|
269
288
|
},
|
|
270
289
|
{
|
|
271
290
|
path: ['workspaces', 'suspend'],
|
|
272
|
-
summary: 'Suspend a running workspace (
|
|
273
|
-
description: 'Without options the suspend is requested now. --when-idle (0.5.1+) defers it instead: the workspace is suspended once it has been idle for that long, counted from the later of its last work and the request, so call it when an agent turn ends. A command still running, an attached exec or terminal stream, or a keepalive postpones it; the next tool call (the next turn) or a resume cancels it. Repeating replaces the pending request. --cancel-when-idle cancels it.',
|
|
291
|
+
summary: 'Suspend a running workspace (memory and processes checkpointed), now or once it has been idle',
|
|
292
|
+
description: 'Without options the suspend is requested now. With --wait it returns as soon as the workspace is sealed on its host, typically in a few hundred ms; the result\'s durable turns true when the copy lands in durable storage, typically within a second. --durable (0.7.0+) waits for that too. --when-idle (0.5.1+) defers it instead: the workspace is suspended once it has been idle for that long, counted from the later of its last work and the request, so call it when an agent turn ends. A command still running, an attached exec or terminal stream, or a keepalive postpones it; the next tool call (the next turn) or a resume cancels it. Repeating replaces the pending request. --cancel-when-idle cancels it.',
|
|
274
293
|
positionals: [REF],
|
|
275
294
|
options: {
|
|
276
295
|
'when-idle': { type: 'string', value: '<duration>', description: 'Suspend once the workspace has been idle this long (0.5.1+; 0 to 1h, e.g. 0, 30s, 2m; 0 = as soon as it is idle, 0.6.1+). Cannot be combined with --wait.' },
|
|
277
296
|
'cancel-when-idle': { type: 'boolean', description: 'Cancel a pending --when-idle request (0.5.1+; idempotent).' },
|
|
297
|
+
durable: { type: 'boolean', description: 'Wait until the suspend is in durable storage (result durable: true; 0.7.0+). Implies --wait and shares its --timeout.' },
|
|
278
298
|
...WAIT,
|
|
279
299
|
},
|
|
280
|
-
examples: ['shard ws suspend acme/demo --wait', 'shard ws suspend acme/demo --when-idle 2m', 'shard ws suspend acme/demo --cancel-when-idle'],
|
|
300
|
+
examples: ['shard ws suspend acme/demo --wait', 'shard ws suspend acme/demo --durable', 'shard ws suspend acme/demo --when-idle 2m', 'shard ws suspend acme/demo --cancel-when-idle'],
|
|
281
301
|
},
|
|
282
302
|
{ path: ['workspaces', 'resume'], summary: 'Resume a suspended workspace', positionals: [REF], options: { ...WAIT } },
|
|
283
303
|
{
|
|
@@ -695,7 +715,11 @@ const KEY_COMMANDS = [
|
|
|
695
715
|
path: ['operations', 'wait'],
|
|
696
716
|
summary: 'Wait for an operation to finish (exit 0 succeeded, 6 failed/canceled, 5 timeout)',
|
|
697
717
|
positionals: [{ name: 'operation-id', required: true }],
|
|
698
|
-
options: {
|
|
718
|
+
options: {
|
|
719
|
+
timeout: { type: 'string', value: '<duration>', description: 'Give up after this long (default 5m).' },
|
|
720
|
+
durable: { type: 'boolean', description: 'A suspend or fork: also wait until its copy is in durable storage (result durable: true; 0.7.0+).' },
|
|
721
|
+
...TIMING,
|
|
722
|
+
},
|
|
699
723
|
},
|
|
700
724
|
];
|
|
701
725
|
/** Every command: the account plane first (sign in, set up), then the project-key commands. */
|
package/dist/commands.js
CHANGED
|
@@ -16,7 +16,7 @@ import { updateCheckDisabled } from "./update-check.js";
|
|
|
16
16
|
import { initTemplateYaml, recipeYaml } from "./template-yaml.js";
|
|
17
17
|
import { BuildTimer, buildTimingJson, buildTimingText, progressLine } from "./timing.js";
|
|
18
18
|
import { timingJson } from "./timing.js";
|
|
19
|
-
import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
19
|
+
import { bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, modeText, operationDetail, pairs, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, durableText, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
|
|
20
20
|
const DEFAULT_WAIT_MS = 300_000;
|
|
21
21
|
const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
|
|
22
22
|
const MAX_STDIN = 1024 * 1024;
|
|
@@ -496,7 +496,7 @@ function reportExecution(ctx, ref, r, opts) {
|
|
|
496
496
|
}
|
|
497
497
|
function lifecycleText(verb, w, op) {
|
|
498
498
|
const finished = op.state === 'succeeded' || op.state === 'failed' || op.state === 'canceled';
|
|
499
|
-
return `${verb} ${w.key} (${w.id}): operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ?
|
|
499
|
+
return `${verb} ${w.key} (${w.id}): operation ${op.id} ${op.kind} ${op.state}${op.state_reason ? ` (${op.state_reason})` : ''}\n${finished ? durableText(op) : `wait with: shard operations wait ${op.id}\n`}`;
|
|
500
500
|
}
|
|
501
501
|
/** The project-key commands; handlers.ts adds the account plane's. */
|
|
502
502
|
export const KEY_HANDLERS = {
|
|
@@ -565,6 +565,46 @@ export const KEY_HANDLERS = {
|
|
|
565
565
|
out(ctx, usageText(summary), summary);
|
|
566
566
|
return 0;
|
|
567
567
|
},
|
|
568
|
+
async 'workspaces labels'(ctx, values, [ref]) {
|
|
569
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
570
|
+
const labels = labelMap(str(values.set));
|
|
571
|
+
if (labels !== undefined)
|
|
572
|
+
await ws.setLabels(labels);
|
|
573
|
+
out(ctx, JSON.stringify(ws.labels, null, 2) + '\n', ws.labels);
|
|
574
|
+
return 0;
|
|
575
|
+
},
|
|
576
|
+
async 'workspaces idle'(ctx, values, [ref]) {
|
|
577
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
578
|
+
const policy = str(values.policy);
|
|
579
|
+
const keepalive = str(values.keepalive);
|
|
580
|
+
if (policy !== undefined && keepalive !== undefined)
|
|
581
|
+
throw new UsageError('--policy and --keepalive cannot be combined');
|
|
582
|
+
let result;
|
|
583
|
+
if (policy !== undefined)
|
|
584
|
+
result = (await ws.setIdlePolicy(policy === 'default' ? null : policy)).data;
|
|
585
|
+
else if (keepalive !== undefined)
|
|
586
|
+
result = await ws.keepalive(parsePositiveInt(keepalive, 'keepalive', 86400));
|
|
587
|
+
else
|
|
588
|
+
result = await ws.idle(ctx.signal);
|
|
589
|
+
out(ctx, JSON.stringify(result, null, 2) + '\n', result);
|
|
590
|
+
return 0;
|
|
591
|
+
},
|
|
592
|
+
async 'workspaces exec-start'(ctx, values, [ref, ...argv]) {
|
|
593
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
594
|
+
const session = await cellOf(ctx, ws).exec.start({ argv, stdin_open: values['stdin-open'] === true,
|
|
595
|
+
...(str(values['session-id']) ? { session_id: str(values['session-id']) } : {}), ...(str(values.cwd) ? { cwd: str(values.cwd) } : {}) }, ctx.signal);
|
|
596
|
+
out(ctx, JSON.stringify(session, null, 2) + '\n', session);
|
|
597
|
+
return 0;
|
|
598
|
+
},
|
|
599
|
+
async 'workspaces exec-input'(ctx, values, [ref, sessionId, data]) {
|
|
600
|
+
const offset = Number(str(values.offset));
|
|
601
|
+
if (str(values.offset) === undefined || !Number.isSafeInteger(offset) || offset < 0)
|
|
602
|
+
throw new UsageError('--offset must be a non-negative integer');
|
|
603
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
604
|
+
const ack = await cellOf(ctx, ws).exec.input(sessionId, data ?? '', { offset, close: values.close === true, signal: ctx.signal });
|
|
605
|
+
out(ctx, JSON.stringify(ack) + '\n', ack);
|
|
606
|
+
return 0;
|
|
607
|
+
},
|
|
568
608
|
async 'workspaces open'(ctx, values, [key]) {
|
|
569
609
|
const template = str(values.template);
|
|
570
610
|
if (!template)
|
|
@@ -583,6 +623,8 @@ export const KEY_HANDLERS = {
|
|
|
583
623
|
const ws = await waited(ctx, 'the open continues server side (open the key again to keep waiting).', () => cloud.workspaces.open({
|
|
584
624
|
key: key,
|
|
585
625
|
template,
|
|
626
|
+
...(str(values.labels) !== undefined ? { labels: labelMap(str(values.labels)) } : {}),
|
|
627
|
+
...(str(values['idle-policy']) ? { idlePolicy: str(values['idle-policy']) } : {}),
|
|
586
628
|
...(c ? { caps: c } : {}),
|
|
587
629
|
...(bind.length ? { secrets: bind } : {}),
|
|
588
630
|
...(inputs ? { inputs } : {}),
|
|
@@ -609,6 +651,7 @@ export const KEY_HANDLERS = {
|
|
|
609
651
|
const params = {
|
|
610
652
|
...(state ? { state: state } : {}),
|
|
611
653
|
...(prefix ? { keyPrefix: prefix } : {}),
|
|
654
|
+
...(str(values.labels) !== undefined ? { labels: labelMap(str(values.labels)) } : {}),
|
|
612
655
|
...(lifetime ? { lifetime } : {}),
|
|
613
656
|
...(purpose ? { purpose } : {}),
|
|
614
657
|
...(values['include-deleted'] === true ? { includeDeleted: true } : {}),
|
|
@@ -854,8 +897,12 @@ export const KEY_HANDLERS = {
|
|
|
854
897
|
async 'workspaces suspend'(ctx, values, [ref]) {
|
|
855
898
|
const whenIdle = str(values['when-idle']);
|
|
856
899
|
const cancel = values['cancel-when-idle'] === true;
|
|
857
|
-
|
|
858
|
-
|
|
900
|
+
const durable = values.durable === true;
|
|
901
|
+
if (whenIdle === undefined && !cancel) {
|
|
902
|
+
return lifecycle(ctx, values, ref, 'Suspending', (ws, o) => ctx.cloud().workspaces.suspend(ws.id, durable ? { ...o, durable: true, wait: o.wait ?? { timeoutMs: waitTimeout(values), signal: ctx.signal } } : o), ['suspend', 'Suspend']);
|
|
903
|
+
}
|
|
904
|
+
if (durable)
|
|
905
|
+
throw new UsageError(`${whenIdle !== undefined ? '--when-idle' : '--cancel-when-idle'} cannot be combined with --durable (nothing is suspended now)`);
|
|
859
906
|
if (whenIdle !== undefined && cancel)
|
|
860
907
|
throw new UsageError('--when-idle and --cancel-when-idle cannot be combined');
|
|
861
908
|
const flag = cancel ? '--cancel-when-idle' : '--when-idle';
|
|
@@ -1526,7 +1573,10 @@ export const KEY_HANDLERS = {
|
|
|
1526
1573
|
async 'operations wait'(ctx, values, [id]) {
|
|
1527
1574
|
if (!UUID.test(id))
|
|
1528
1575
|
throw new UsageError(`<operation-id> must be a UUID (got "${id}")`);
|
|
1529
|
-
const
|
|
1576
|
+
const timeoutMs = waitTimeout(values);
|
|
1577
|
+
const op = values.durable === true
|
|
1578
|
+
? await waited(ctx, `operation ${id} continues server side.`, () => ctx.cloud().workspaces.waitForDurable(id, { timeoutMs, signal: ctx.signal }))
|
|
1579
|
+
: await waitOperation(ctx, id, timeoutMs);
|
|
1530
1580
|
out(ctx, operationDetail(op), op);
|
|
1531
1581
|
return 0;
|
|
1532
1582
|
},
|
|
@@ -1681,3 +1731,17 @@ async function lifecycle(ctx, values, ref, verb, start, processfulOnly) {
|
|
|
1681
1731
|
out(ctx, lifecycleText(verb, ws, op), op);
|
|
1682
1732
|
return 0;
|
|
1683
1733
|
}
|
|
1734
|
+
function labelMap(value) {
|
|
1735
|
+
if (value === undefined)
|
|
1736
|
+
return undefined;
|
|
1737
|
+
let parsed;
|
|
1738
|
+
try {
|
|
1739
|
+
parsed = JSON.parse(value);
|
|
1740
|
+
}
|
|
1741
|
+
catch {
|
|
1742
|
+
throw new UsageError('labels must be a JSON object of strings');
|
|
1743
|
+
}
|
|
1744
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || Object.values(parsed).some((v) => typeof v !== 'string'))
|
|
1745
|
+
throw new UsageError('labels must be a JSON object of strings');
|
|
1746
|
+
return parsed;
|
|
1747
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -84,7 +84,7 @@ export interface ErrorInfo {
|
|
|
84
84
|
retryable?: boolean;
|
|
85
85
|
operation_id?: string;
|
|
86
86
|
details?: Record<string, unknown>;
|
|
87
|
-
source?: 'api' | 'cell';
|
|
87
|
+
source?: 'api' | 'cell' | 'unknown';
|
|
88
88
|
last_state?: string;
|
|
89
89
|
operation?: unknown;
|
|
90
90
|
/** A file-first execution the error is about (failed, lost, still running, or left running by Ctrl-C). */
|
package/dist/errors.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* stderr with the API's error code, message and request id; the process exit
|
|
4
4
|
* code tells scripts what kind of failure it was (README "Exit codes").
|
|
5
5
|
*/
|
|
6
|
-
import { CheckoutTimeoutError, ExecStartError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
6
|
+
import { CheckoutTimeoutError, DurabilityLostError, ExecStartError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
7
7
|
export const EXIT = {
|
|
8
8
|
ok: 0,
|
|
9
9
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -137,15 +137,22 @@ export function describeError(err) {
|
|
|
137
137
|
source: err.source,
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
|
-
if (err instanceof OperationTimeoutError)
|
|
141
|
-
return { code: 'timeout', message: err.message, operation_id: err.operationId, last_state: err.lastState };
|
|
140
|
+
if (err instanceof OperationTimeoutError) {
|
|
141
|
+
return { code: 'timeout', message: err.message, operation_id: err.operationId, last_state: err.lastState, ...(err.durable ? { details: { durable: false, state: 'pending' } } : {}) };
|
|
142
|
+
}
|
|
143
|
+
// --durable (0.7.0+): the suspend succeeded; its durable copy could not be made (see the lifecycle reference).
|
|
144
|
+
if (err instanceof DurabilityLostError) {
|
|
145
|
+
return { code: 'durability_lost', message: err.message, operation_id: err.operationId, retryable: false, details: { state: 'lost', ...(err.durability.reason ? { reason: err.durability.reason } : {}), checkpoint_id: err.durability.checkpointId }, operation: err.operation };
|
|
146
|
+
}
|
|
142
147
|
if (err instanceof OperationFailedError) {
|
|
143
148
|
// retryable: the operation error's own flag (true for capacity_unavailable: nothing was started, retry later).
|
|
144
149
|
// details.reason: the operation error's reason (e.g. the scheduler's no_ready_hosts), printed after the message.
|
|
145
150
|
const opDetails = err.operation.error?.details;
|
|
146
151
|
const reason = typeof opDetails === 'object' && opDetails !== null ? opDetails.reason : undefined;
|
|
147
152
|
const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}) };
|
|
148
|
-
|
|
153
|
+
// A suspend-when-idle canceled workspace_active (0.7.0+): the workspace was in use and keeps running.
|
|
154
|
+
const message = err.workspaceActive ? `${err.message}: the workspace was in use, so the suspend-when-idle was canceled; it keeps running` : err.message;
|
|
155
|
+
return { code: 'operation_failed', message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
|
|
149
156
|
}
|
|
150
157
|
// File-first executions (0.5.0): failed or lost like a failed operation (details.reason says why;
|
|
151
158
|
// retryable: a new execution id may succeed), still running like a wait that gave up.
|
|
@@ -180,7 +187,7 @@ export function describeError(err) {
|
|
|
180
187
|
return { code: 'interrupted', message: err.message, ...(err.operationId ? { operation_id: err.operationId } : {}), ...(err.executionId ? { execution_id: err.executionId } : {}) };
|
|
181
188
|
}
|
|
182
189
|
if (err instanceof ShardfluxProtocolError)
|
|
183
|
-
return { code: 'protocol_error', message: err.message, status: err.status };
|
|
190
|
+
return { code: 'protocol_error', message: err.message, status: err.status, source: err.source };
|
|
184
191
|
if (isNamed(err, 'TimeoutError'))
|
|
185
192
|
return { code: 'timeout', message: 'The request timed out.' };
|
|
186
193
|
if (isNamed(err, 'AbortError'))
|
|
@@ -205,7 +212,7 @@ export function exitCodeFor(err, interrupted = false) {
|
|
|
205
212
|
return EXIT.notFound;
|
|
206
213
|
if (err instanceof OperationTimeoutError || err instanceof CheckoutTimeoutError || isNamed(err, 'TimeoutError'))
|
|
207
214
|
return EXIT.timeout;
|
|
208
|
-
if (err instanceof OperationFailedError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
|
|
215
|
+
if (err instanceof OperationFailedError || err instanceof DurabilityLostError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
|
|
209
216
|
return EXIT.operationFailed;
|
|
210
217
|
if (err instanceof ExecutionPendingError)
|
|
211
218
|
return EXIT.timeout;
|
package/dist/format.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Human-readable output (aligned tables, key/value blocks) and the error line.
|
|
3
|
-
* `--json` output is the API's own objects (see README "JSON output"), so
|
|
4
|
-
* these functions only shape what people read.
|
|
5
|
-
*/
|
|
6
1
|
import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, WorkspaceChange, WorkspaceSecretBindings, WorkspaceView } from '@shardflux/sdk';
|
|
7
2
|
import type { ErrorInfo } from './errors.js';
|
|
8
3
|
export declare function json(value: unknown): string;
|
|
@@ -41,6 +36,11 @@ export declare function suspendWhenIdleText(w: {
|
|
|
41
36
|
export declare function workspaceDetail(w: WorkspaceView): string;
|
|
42
37
|
export declare const OPERATION_HEADERS: readonly ["ID", "KIND", "STATE", "REASON", "CREATED", "COMPLETED"];
|
|
43
38
|
export declare function operationTable(ops: readonly Operation[]): string;
|
|
39
|
+
/**
|
|
40
|
+
* A finished suspend or fork (0.7.0+): whether its copy is in durable storage, when the result says (`durable`,
|
|
41
|
+
* `durability`). Empty when the result has no durability block (stored durably before the operation completed).
|
|
42
|
+
*/
|
|
43
|
+
export declare function durableText(op: Operation): string;
|
|
44
44
|
export declare function operationDetail(op: Operation): string;
|
|
45
45
|
export declare function sessionTable(sessions: readonly AgentSession[]): string;
|
|
46
46
|
/** `files stat`: one path's metadata; the revision only when requested and returned. */
|
package/dist/format.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable output (aligned tables, key/value blocks) and the error line.
|
|
3
|
+
* `--json` output is the API's own objects (see README "JSON output"), so
|
|
4
|
+
* these functions only shape what people read.
|
|
5
|
+
*/
|
|
6
|
+
import { durabilityOf, isDurable } from '@shardflux/sdk';
|
|
1
7
|
export function json(value) {
|
|
2
8
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
3
9
|
}
|
|
@@ -161,6 +167,28 @@ export const OPERATION_HEADERS = ['ID', 'KIND', 'STATE', 'REASON', 'CREATED', 'C
|
|
|
161
167
|
export function operationTable(ops) {
|
|
162
168
|
return table(OPERATION_HEADERS, ops.map((o) => [o.id, o.kind, o.state, o.state_reason ?? '-', time(o.created_at), time(o.completed_at)]));
|
|
163
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* A finished suspend or fork (0.7.0+): whether its copy is in durable storage, when the result says (`durable`,
|
|
172
|
+
* `durability`). Empty when the result has no durability block (stored durably before the operation completed).
|
|
173
|
+
*/
|
|
174
|
+
export function durableText(op) {
|
|
175
|
+
const d = durabilityOf(op);
|
|
176
|
+
if (op.state !== 'succeeded' || !d)
|
|
177
|
+
return '';
|
|
178
|
+
if (d.state === 'durable')
|
|
179
|
+
return `durable: yes${d.localCommitToDurableMs !== null ? ` (${d.localCommitToDurableMs} ms after the suspend)` : ''}\n`;
|
|
180
|
+
if (d.state === 'pending')
|
|
181
|
+
return `durable: in progress, typically within a second (wait with: shard operations wait ${op.id} --durable)\n`;
|
|
182
|
+
return `durable: no (${d.reason ?? 'lost'}); see docs.shardflux.dev/concepts/lifecycle\n`;
|
|
183
|
+
}
|
|
184
|
+
/** `durable` of a finished suspend or fork for `operations get`/`wait`: yes, in progress, no, or '-' for other kinds. */
|
|
185
|
+
function durableField(op) {
|
|
186
|
+
const d = durabilityOf(op);
|
|
187
|
+
if (d)
|
|
188
|
+
return d.state === 'durable' ? 'yes' : d.state === 'pending' ? 'in progress' : `no (${d.reason ?? 'lost'})`;
|
|
189
|
+
const v = isDurable(op);
|
|
190
|
+
return v === null ? '-' : v ? 'yes' : 'no';
|
|
191
|
+
}
|
|
164
192
|
export function operationDetail(op) {
|
|
165
193
|
const err = op.error;
|
|
166
194
|
return kv([
|
|
@@ -168,6 +196,7 @@ export function operationDetail(op) {
|
|
|
168
196
|
['kind', op.kind],
|
|
169
197
|
['state', op.state],
|
|
170
198
|
['reason', op.state_reason ?? '-'],
|
|
199
|
+
...(op.kind === 'suspend' || op.kind === 'fork' ? [['durable', durableField(op)]] : []),
|
|
171
200
|
['workspace', op.workspace_id ?? '-'],
|
|
172
201
|
['created', time(op.created_at)],
|
|
173
202
|
['updated', time(op.updated_at)],
|
package/dist/http.d.ts
CHANGED
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The fetch given to the SDK: the runtime's fetch (API and cell gateway). By
|
|
3
|
-
* default every request uses a fresh connection (`Connection: close`);
|
|
4
|
-
* SHARDFLUX_HTTP_KEEPALIVE=1 reuses connections.
|
|
5
|
-
*
|
|
6
|
-
* `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
|
|
7
|
-
* held by the server for up to 20 s). The SDK passes the caller's signal to its
|
|
8
|
-
* held polls itself, but not to the held open (`workspaces.open()` with `wait`
|
|
9
|
-
* sends its POST without one), so Ctrl-C would otherwise wait the hold out.
|
|
10
|
-
*/
|
|
1
|
+
/** Share the SDK pool and attach Ctrl-C to held API requests. */
|
|
11
2
|
export declare function makeFetch(env: Record<string, string | undefined>, base?: typeof fetch, signal?: AbortSignal): typeof fetch;
|
package/dist/http.js
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* SHARDFLUX_HTTP_KEEPALIVE=1 reuses connections.
|
|
5
|
-
*
|
|
6
|
-
* `signal` (the CLI's Ctrl-C) is attached to held requests (`Prefer: wait=N`,
|
|
7
|
-
* held by the server for up to 20 s). The SDK passes the caller's signal to its
|
|
8
|
-
* held polls itself, but not to the held open (`workspaces.open()` with `wait`
|
|
9
|
-
* sends its POST without one), so Ctrl-C would otherwise wait the hold out.
|
|
10
|
-
*/
|
|
11
|
-
export function makeFetch(env, base = fetch, signal) {
|
|
12
|
-
// Why a fresh connection by default (Node 26 keep-alive measurements): docs/progress/startup-latency.md.
|
|
13
|
-
const keepAlive = env.SHARDFLUX_HTTP_KEEPALIVE === '1';
|
|
1
|
+
import { defaultFetch } from '@shardflux/sdk';
|
|
2
|
+
/** Share the SDK pool and attach Ctrl-C to held API requests. */
|
|
3
|
+
export function makeFetch(env, base = defaultFetch(env), signal) {
|
|
14
4
|
return (input, init) => {
|
|
15
5
|
const headers = new Headers(init?.headers);
|
|
16
6
|
const held = signal !== undefined && /\bwait\s*=/i.test(headers.get('prefer') ?? '');
|
|
17
|
-
if (
|
|
7
|
+
if (!held)
|
|
18
8
|
return base(input, init);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const s = held && signal ? (init?.signal ? AbortSignal.any([init.signal, signal]) : signal) : init?.signal;
|
|
22
|
-
return base(input, { ...init, headers, ...(s ? { signal: s } : {}) });
|
|
9
|
+
const s = init?.signal && signal ? AbortSignal.any([init.signal, signal]) : signal;
|
|
10
|
+
return base(input, { ...init, ...(s ? { signal: s } : {}) });
|
|
23
11
|
};
|
|
24
12
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,4 +22,4 @@ export { openUrl, openerFor } from './browser.js';
|
|
|
22
22
|
export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from './errors.js';
|
|
23
23
|
export type { ErrorInfo } from './errors.js';
|
|
24
24
|
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from './format.js';
|
|
25
|
-
export { TimingRecorder, coldBootNotice, timingJson, timingText } from './timing.js';
|
|
25
|
+
export { TimingRecorder, coldBootNotice, lostSuspendNotice, notices, timingJson, timingText } from './timing.js';
|
package/dist/index.js
CHANGED
|
@@ -14,4 +14,4 @@ export { CHECK_INTERVAL_MS, CLI_DISTRIBUTION, NOTICE_WAIT_MS, UPDATE_CHECK_FILE,
|
|
|
14
14
|
export { openUrl, openerFor } from "./browser.js";
|
|
15
15
|
export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, ExecutionPendingError, InterruptedError, MfaPendingError, NotFoundError, UsageError, describeError, exitCodeFor, redact } from "./errors.js";
|
|
16
16
|
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from "./format.js";
|
|
17
|
-
export { TimingRecorder, coldBootNotice, timingJson, timingText } from "./timing.js";
|
|
17
|
+
export { TimingRecorder, coldBootNotice, lostSuspendNotice, notices, timingJson, timingText } from "./timing.js";
|
package/dist/main.js
CHANGED
|
@@ -19,7 +19,7 @@ import { errorText, feedbackCommand, json, time } from "./format.js";
|
|
|
19
19
|
import { HANDLERS } from "./handlers.js";
|
|
20
20
|
import { makeFetch } from "./http.js";
|
|
21
21
|
import { Input } from "./input.js";
|
|
22
|
-
import { TimingRecorder,
|
|
22
|
+
import { TimingRecorder, notices, timingJson, timingText } from "./timing.js";
|
|
23
23
|
import { CLI_DISTRIBUTION, NOTICE_WAIT_MS, UpdateCheck, noticeOf, updateCheckDisabled, updateView } from "./update-check.js";
|
|
24
24
|
const KEY_SHAPE = /^sfk_[a-z2-7]{16}_[A-Za-z0-9]+$/;
|
|
25
25
|
// Printable (no C0 controls or DEL), as the API's AgentLabel pattern.
|
|
@@ -327,7 +327,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
327
327
|
const feedback = nudges(info, command) ? feedbackCommand(info) : undefined;
|
|
328
328
|
const text = wantsJson
|
|
329
329
|
? json({ error: info, ...(showTiming ? { timing: timingJson(timing) } : {}), ...(feedback ? { feedback } : {}) })
|
|
330
|
-
: `${timing ? timingText(timing) : ''}${
|
|
330
|
+
: `${timing ? timingText(timing) : ''}${notices(traces)}${errorText(info, hint, feedback)}`;
|
|
331
331
|
io.stderr.write(redact(text, ...secrets));
|
|
332
332
|
return code;
|
|
333
333
|
};
|
|
@@ -359,7 +359,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
359
359
|
const wakeTimeoutMs = resolveWakeTimeout(parsed.global.wakeTimeout, io.env.SHARDFLUX_WAKE_TIMEOUT_MS);
|
|
360
360
|
showTiming = parsed.values.timing === true;
|
|
361
361
|
const userAgent = `shard-cli/${CLI_VERSION} shardflux-sdk-ts/${SDK_VERSION}${distribution.package === CLI_DISTRIBUTION.package ? '' : ` ${distribution.package}/${distribution.version}`}`;
|
|
362
|
-
const base = makeFetch(io.env,
|
|
362
|
+
const base = makeFetch(io.env, undefined, signal);
|
|
363
363
|
// Notes which credential a 401 unauthenticated answered (the hint, and removing a dead saved session).
|
|
364
364
|
const watchedFetch = async (input, init) => {
|
|
365
365
|
const res = await base(input, init);
|
|
@@ -560,9 +560,11 @@ export async function run(argv, io, opts = {}) {
|
|
|
560
560
|
// With --json the handler put the timing into its output; otherwise it follows the output, on stderr.
|
|
561
561
|
if (showTiming && !ctx.json && traces.last)
|
|
562
562
|
io.stderr.write(timingText(traces.last));
|
|
563
|
-
// A resume that booted the saved disk (0.6.0+): said on stderr with or
|
|
564
|
-
|
|
565
|
-
|
|
563
|
+
// A resume that booted the saved disk (0.6.0+), or restored an earlier checkpoint (0.7.0+): said on stderr with or
|
|
564
|
+
// without --timing, also with --json.
|
|
565
|
+
const notice = notices(traces);
|
|
566
|
+
if (notice)
|
|
567
|
+
io.stderr.write(notice);
|
|
566
568
|
code = outcome.code;
|
|
567
569
|
}
|
|
568
570
|
// The update notice: after the output, never after Ctrl-C or a --json failure (stderr then holds only the error).
|
package/dist/timing.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export declare class TimingRecorder {
|
|
|
10
10
|
* the wake of a command that found the workspace suspended. `coldBootNotice()` says it.
|
|
11
11
|
*/
|
|
12
12
|
coldBoot: LifecycleTiming | null;
|
|
13
|
+
/**
|
|
14
|
+
* (0.7.0+) The last traced resume whose result names a suspend it could not restore (`lost_suspend`): a `ws resume
|
|
15
|
+
* --wait`, an `operations wait` of that resume, or a command's wake. `lostSuspendNotice()` says it.
|
|
16
|
+
*/
|
|
17
|
+
lostSuspend: LifecycleTiming | null;
|
|
13
18
|
readonly listener: ProgressListener;
|
|
14
19
|
/**
|
|
15
20
|
* The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
|
|
@@ -22,6 +27,13 @@ export declare class TimingRecorder {
|
|
|
22
27
|
* `--timing`: the workspace runs and its files are as of the suspend, but running processes were restarted.
|
|
23
28
|
*/
|
|
24
29
|
export declare function coldBootNotice(t: LifecycleTiming): string;
|
|
30
|
+
/**
|
|
31
|
+
* The one-line stderr notice after a command whose resume restored an earlier checkpoint because the latest suspend
|
|
32
|
+
* could not be kept (0.7.0+; `lost_suspend`): the state the workspace runs with is as of `state_as_of`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function lostSuspendNotice(t: LifecycleTiming): string;
|
|
35
|
+
/** The notices a command's traced calls leave on stderr (cold boot, lost suspend), in that order. */
|
|
36
|
+
export declare function notices(r: TimingRecorder): string;
|
|
25
37
|
/** The human form (stderr): formatTiming() of the SDK, one block. */
|
|
26
38
|
export declare function timingText(t: LifecycleTiming): string;
|
|
27
39
|
/** The `--json` form: the SDK's LifecycleTiming with the CLI's snake_case keys (the rest of its JSON is the API's). */
|
package/dist/timing.js
CHANGED
|
@@ -17,6 +17,11 @@ export class TimingRecorder {
|
|
|
17
17
|
* the wake of a command that found the workspace suspended. `coldBootNotice()` says it.
|
|
18
18
|
*/
|
|
19
19
|
coldBoot = null;
|
|
20
|
+
/**
|
|
21
|
+
* (0.7.0+) The last traced resume whose result names a suspend it could not restore (`lost_suspend`): a `ws resume
|
|
22
|
+
* --wait`, an `operations wait` of that resume, or a command's wake. `lostSuspendNotice()` says it.
|
|
23
|
+
*/
|
|
24
|
+
lostSuspend = null;
|
|
20
25
|
listener = (e) => {
|
|
21
26
|
if (e.action === 'token' || e.action === 'tool')
|
|
22
27
|
return;
|
|
@@ -27,6 +32,8 @@ export class TimingRecorder {
|
|
|
27
32
|
this.last = e.timing;
|
|
28
33
|
if (restartedProcesses(e.timing))
|
|
29
34
|
this.coldBoot = e.timing;
|
|
35
|
+
if (e.timing.server?.lostSuspend)
|
|
36
|
+
this.lostSuspend = e.timing;
|
|
30
37
|
};
|
|
31
38
|
/**
|
|
32
39
|
* The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
|
|
@@ -49,6 +56,20 @@ export function coldBootNotice(t) {
|
|
|
49
56
|
const reason = t.server?.coldBootReason;
|
|
50
57
|
return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from its saved disk; running processes were restarted${reason ? ` (${reason})` : ''}\n`;
|
|
51
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The one-line stderr notice after a command whose resume restored an earlier checkpoint because the latest suspend
|
|
61
|
+
* could not be kept (0.7.0+; `lost_suspend`): the state the workspace runs with is as of `state_as_of`.
|
|
62
|
+
*/
|
|
63
|
+
export function lostSuspendNotice(t) {
|
|
64
|
+
const l = t.server?.lostSuspend;
|
|
65
|
+
if (!l)
|
|
66
|
+
return '';
|
|
67
|
+
return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from checkpoint ${l.restoredCheckpointId ?? '-'}${l.stateAsOf ? ` (state as of ${l.stateAsOf})` : ''}; the suspend of ${l.suspendedAt ?? l.checkpointId} was not kept${l.reason ? ` (${l.reason})` : ''}\n`;
|
|
68
|
+
}
|
|
69
|
+
/** The notices a command's traced calls leave on stderr (cold boot, lost suspend), in that order. */
|
|
70
|
+
export function notices(r) {
|
|
71
|
+
return `${r.coldBoot ? coldBootNotice(r.coldBoot) : ''}${r.lostSuspend ? lostSuspendNotice(r.lostSuspend) : ''}`;
|
|
72
|
+
}
|
|
52
73
|
/** The human form (stderr): formatTiming() of the SDK, one block. */
|
|
53
74
|
export function timingText(t) {
|
|
54
75
|
return `${formatTiming(t)}\n`;
|
|
@@ -79,6 +100,31 @@ export function timingJson(t) {
|
|
|
79
100
|
resume_path: s.resumePath,
|
|
80
101
|
memory_restored: s.memoryRestored ?? null,
|
|
81
102
|
cold_boot_reason: s.coldBootReason ?? null,
|
|
103
|
+
durable: s.durable ?? null,
|
|
104
|
+
suspend_path: s.suspendPath ?? null,
|
|
105
|
+
durability: s.durability
|
|
106
|
+
? {
|
|
107
|
+
state: s.durability.state,
|
|
108
|
+
checkpoint_id: s.durability.checkpointId,
|
|
109
|
+
generation_id: s.durability.generationId,
|
|
110
|
+
local_commit_at: s.durability.localCommitAt,
|
|
111
|
+
durable_by: s.durability.durableBy,
|
|
112
|
+
durable_at: s.durability.durableAt,
|
|
113
|
+
local_commit_to_durable_ms: s.durability.localCommitToDurableMs,
|
|
114
|
+
overdue_at: s.durability.overdueAt,
|
|
115
|
+
reason: s.durability.reason,
|
|
116
|
+
}
|
|
117
|
+
: null,
|
|
118
|
+
lost_suspend: s.lostSuspend
|
|
119
|
+
? {
|
|
120
|
+
checkpoint_id: s.lostSuspend.checkpointId,
|
|
121
|
+
generation_id: s.lostSuspend.generationId,
|
|
122
|
+
reason: s.lostSuspend.reason,
|
|
123
|
+
suspended_at: s.lostSuspend.suspendedAt,
|
|
124
|
+
restored_checkpoint_id: s.lostSuspend.restoredCheckpointId,
|
|
125
|
+
state_as_of: s.lostSuspend.stateAsOf,
|
|
126
|
+
}
|
|
127
|
+
: null,
|
|
82
128
|
boot_to_ready_ms: s.bootToReadyMs,
|
|
83
129
|
host_timings_ms: s.hostTimingsMs,
|
|
84
130
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork persistent agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"yaml": "2.9.1",
|
|
47
|
-
"@shardflux/sdk": "^0.
|
|
47
|
+
"@shardflux/sdk": "^0.12.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@eslint/js": "10.0.1",
|