@shardflux/cli 0.5.2 → 0.6.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 CHANGED
@@ -3,7 +3,32 @@
3
3
  Every command and option the README shows is available from the version named here. Below 1.0, a minor release may
4
4
  break compatibility; breaking changes are marked **Breaking**.
5
5
 
6
- ## 0.5.2 (not yet published)
6
+ ## 0.6.0 (not yet published)
7
+
8
+ Needs `@shardflux/sdk` 0.11.0 or later.
9
+
10
+ ### A resume that restarted processes says so (cold boot, contracts §12)
11
+
12
+ When the platform's VM runtime changed after a suspend and no host can restore the memory snapshot, the cell resumes
13
+ the workspace by booting its saved disk (a cold boot), automatically, also when a command wakes it. Files are as of the
14
+ suspend; every process was restarted.
15
+
16
+ - After a command whose resume or wake did that (`ws resume --wait`, `operations wait` of that resume, or `ws exec`,
17
+ `files ...` and `ws changes` waking a suspended workspace), `shard` prints one line on stderr after the output, with
18
+ or without `--timing`: `note: workspace <id> resumed from its saved disk; running processes were restarted
19
+ (runtime_changed)`. Also with `--json` (stdout unchanged); not after a `--json` failure, whose stderr is only the
20
+ error document. On a text failure it comes before the error line. The exit code does not change.
21
+ - `--json --timing`: `timing.server` adds `memory_restored` (`false` for a cold boot, `true` for a memory resume,
22
+ `null` when the API did not say) and `cold_boot_reason`. The human `--timing` block says `resume from cold_boot:
23
+ processes restarted (runtime_changed)` (the SDK's `formatTiming()`).
24
+ - `coldBootNotice()` is exported next to `TimingRecorder`, which records the call as `coldBoot`.
25
+
26
+ ## 0.5.3
27
+
28
+ Fix: `shard --version`, the User-Agent and the update check reports 0.5.3. 0.5.2 was published reporting 0.5.1, its previous version; the build now
29
+ fails when `CLI_VERSION` differs from package.json.
30
+
31
+ ## 0.5.2
7
32
 
8
33
  Public support: package metadata and the README now link to the [shared bug tracker](https://github.com/shardfluxdev/community/issues),
9
34
  feature requests, and private support/security reporting.
package/README.md CHANGED
@@ -576,6 +576,21 @@ without waking it (the state at suspension); `files search` then notes it on std
576
576
  `shard operations wait <id>` keeps waiting.
577
577
  - A resume or open that fails exits 6.
578
578
 
579
+ **A resume can restart processes (0.6.0+).** A resume normally restores memory and running processes. When the
580
+ platform's VM runtime changed after the suspend and no host can restore that memory snapshot, the resume boots the
581
+ workspace's saved disk instead (a cold boot), automatically, also when a command wakes the workspace. The command
582
+ still runs, and the files are as of the suspend, but every process was restarted, as after a reboot: start dev
583
+ servers and background jobs again. `shard` then prints one line on stderr after the output (also with `--json`,
584
+ except on a failure, whose stderr is only the error document), whether or not you pass `--timing`:
585
+
586
+ ```text
587
+ note: workspace 01a0e5a8-3edd-74ba-b489-d62b8925e342 resumed from its saved disk; running processes were restarted (runtime_changed)
588
+ ```
589
+
590
+ `shard ws resume --wait --json` prints the operation with its `result` as the API sent it (`resume_path`
591
+ `cold_boot`, `memory_restored` false, `cold_boot_reason`), and `--timing` shows `resume from cold_boot: processes
592
+ restarted (runtime_changed)`. The same applies to `operations wait` of such a resume.
593
+
579
594
  ## Suspend when idle (0.5.1+)
580
595
 
581
596
  A running workspace is billed while it is awake. When an agent's turn ends, ask for a suspend once the workspace has
@@ -686,8 +701,9 @@ Which call is timed:
686
701
 
687
702
  With `--json`, stdout carries the timing as a `timing` field of the command's JSON instead: `action`, `outcome`,
688
703
  `workspace_id`, `operation_id`, `total_ms`, `phases` (`phase`, `reason`, `operation_id`, `start_ms`, `duration_ms`),
689
- `retries`, `server` (`queued_ms`, `run_ms`, `total_ms`, `start_path`, `resume_path`, `boot_to_ready_ms`, ...) and
690
- `outside_server_ms`. It is `null` when the command made no lifecycle call. On a failure the error document on stderr
704
+ `retries`, `server` (`queued_ms`, `run_ms`, `total_ms`, `start_path`, `resume_path`, `memory_restored` and
705
+ `cold_boot_reason` (0.6.0+; `memory_restored` is `false` when a resume booted the saved disk, `null` when the API did
706
+ not say), `boot_to_ready_ms`, ...) and `outside_server_ms`. It is `null` when the command made no lifecycle call. On a failure the error document on stderr
691
707
  carries it: `{"error": {...}, "timing": {...}}`.
692
708
 
693
709
  `shard` does not stream progress while it waits. For live events, use the SDK's `onProgress`.
package/dist/args.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CLI_VERSION = "0.5.1";
1
+ export declare const CLI_VERSION = "0.6.0";
2
2
  export interface OptionSpec {
3
3
  type: 'string' | 'boolean';
4
4
  short?: string;
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.5.1';
15
+ export const CLI_VERSION = '0.6.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).' },
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, timingJson, timingText } from './timing.js';
25
+ export { TimingRecorder, coldBootNotice, 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, timingJson, timingText } from "./timing.js";
17
+ export { TimingRecorder, coldBootNotice, 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, timingJson, timingText } from "./timing.js";
22
+ import { TimingRecorder, coldBootNotice, 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.
@@ -329,7 +329,7 @@ export async function run(argv, io, opts = {}) {
329
329
  const feedback = nudges(info, command) ? feedbackCommand(info) : undefined;
330
330
  const text = wantsJson
331
331
  ? json({ error: info, ...(showTiming ? { timing: timingJson(timing) } : {}), ...(feedback ? { feedback } : {}) })
332
- : `${timing ? timingText(timing) : ''}${errorText(info, hint, feedback)}`;
332
+ : `${timing ? timingText(timing) : ''}${traces.coldBoot ? coldBootNotice(traces.coldBoot) : ''}${errorText(info, hint, feedback)}`;
333
333
  io.stderr.write(redact(text, ...secrets));
334
334
  return code;
335
335
  };
@@ -562,6 +562,9 @@ export async function run(argv, io, opts = {}) {
562
562
  // With --json the handler put the timing into its output; otherwise it follows the output, on stderr.
563
563
  if (showTiming && !ctx.json && traces.last)
564
564
  io.stderr.write(timingText(traces.last));
565
+ // A resume that booted the saved disk (0.6.0+): said on stderr with or without --timing, also with --json.
566
+ if (traces.coldBoot)
567
+ io.stderr.write(coldBootNotice(traces.coldBoot));
565
568
  code = outcome.code;
566
569
  }
567
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
@@ -4,6 +4,12 @@ export declare class TimingRecorder {
4
4
  last: LifecycleTiming | null;
5
5
  /** The operation the command's traced calls last observed: named when Ctrl-C interrupts a wait. */
6
6
  operationId: string | null;
7
+ /**
8
+ * (0.6.0+) The last traced call whose resume booted the workspace's saved disk instead of restoring its memory
9
+ * (`resume_path` `cold_boot`, `memory_restored` false): a `ws resume --wait`, an `operations wait` of that resume, or
10
+ * the wake of a command that found the workspace suspended. `coldBootNotice()` says it.
11
+ */
12
+ coldBoot: LifecycleTiming | null;
7
13
  readonly listener: ProgressListener;
8
14
  /**
9
15
  * The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
@@ -11,6 +17,11 @@ export declare class TimingRecorder {
11
17
  */
12
18
  forError(err: unknown): LifecycleTiming | null;
13
19
  }
20
+ /**
21
+ * The one-line stderr notice after a command whose resume booted the workspace's saved disk (0.6.0+), with or without
22
+ * `--timing`: the workspace runs and its files are as of the suspend, but running processes were restarted.
23
+ */
24
+ export declare function coldBootNotice(t: LifecycleTiming): string;
14
25
  /** The human form (stderr): formatTiming() of the SDK, one block. */
15
26
  export declare function timingText(t: LifecycleTiming): string;
16
27
  /** 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
@@ -11,13 +11,22 @@ export class TimingRecorder {
11
11
  last = null;
12
12
  /** The operation the command's traced calls last observed: named when Ctrl-C interrupts a wait. */
13
13
  operationId = null;
14
+ /**
15
+ * (0.6.0+) The last traced call whose resume booted the workspace's saved disk instead of restoring its memory
16
+ * (`resume_path` `cold_boot`, `memory_restored` false): a `ws resume --wait`, an `operations wait` of that resume, or
17
+ * the wake of a command that found the workspace suspended. `coldBootNotice()` says it.
18
+ */
19
+ coldBoot = null;
14
20
  listener = (e) => {
15
21
  if (e.action === 'token' || e.action === 'tool')
16
22
  return;
17
23
  if (e.operationId)
18
24
  this.operationId = e.operationId;
19
- if (e.type === 'done')
20
- this.last = e.timing;
25
+ if (e.type !== 'done')
26
+ return;
27
+ this.last = e.timing;
28
+ if (restartedProcesses(e.timing))
29
+ this.coldBoot = e.timing;
21
30
  };
22
31
  /**
23
32
  * The failed call's own timing (`err.timing`) when the SDK attached one, else the last recorded. A refused tool token
@@ -28,6 +37,18 @@ export class TimingRecorder {
28
37
  return own && own.action !== 'token' ? own : this.last;
29
38
  }
30
39
  }
40
+ /** A resume that booted the saved disk: files as of the suspend, every process restarted (never a null memory_restored). */
41
+ function restartedProcesses(t) {
42
+ return t.server?.resumePath === 'cold_boot' && t.server.memoryRestored === false;
43
+ }
44
+ /**
45
+ * The one-line stderr notice after a command whose resume booted the workspace's saved disk (0.6.0+), with or without
46
+ * `--timing`: the workspace runs and its files are as of the suspend, but running processes were restarted.
47
+ */
48
+ export function coldBootNotice(t) {
49
+ const reason = t.server?.coldBootReason;
50
+ return `note: ${t.workspaceId ? `workspace ${t.workspaceId} ` : ''}resumed from its saved disk; running processes were restarted${reason ? ` (${reason})` : ''}\n`;
51
+ }
31
52
  /** The human form (stderr): formatTiming() of the SDK, one block. */
32
53
  export function timingText(t) {
33
54
  return `${formatTiming(t)}\n`;
@@ -56,6 +77,8 @@ export function timingJson(t) {
56
77
  start_path: s.startPath,
57
78
  warm_fallback: s.warmFallback,
58
79
  resume_path: s.resumePath,
80
+ memory_restored: s.memoryRestored ?? null,
81
+ cold_boot_reason: s.coldBootReason ?? null,
59
82
  boot_to_ready_ms: s.bootToReadyMs,
60
83
  host_timings_ms: s.hostTimingsMs,
61
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.5.2",
3
+ "version": "0.6.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.10.1"
47
+ "@shardflux/sdk": "^0.11.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@eslint/js": "10.0.1",