@shardflux/cli 0.19.1 → 0.22.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +278 -1
  2. package/README.md +225 -73
  3. package/dist/account-args.js +7 -2
  4. package/dist/account-commands.js +56 -106
  5. package/dist/account-pool.d.ts +20 -15
  6. package/dist/account-pool.js +51 -97
  7. package/dist/account-scope.d.ts +65 -0
  8. package/dist/account-scope.js +246 -0
  9. package/dist/acp-login.d.ts +4 -3
  10. package/dist/acp-login.js +7 -5
  11. package/dist/acp-models.js +20 -15
  12. package/dist/acp.js +66 -13
  13. package/dist/agent-commands.d.ts +4 -1
  14. package/dist/agent-commands.js +86 -32
  15. package/dist/agent-config-files.d.ts +19 -0
  16. package/dist/agent-config-files.js +70 -1
  17. package/dist/agent-config-return.d.ts +194 -0
  18. package/dist/agent-config-return.js +724 -0
  19. package/dist/agent-config-sync.d.ts +10 -2
  20. package/dist/agent-config-sync.js +45 -51
  21. package/dist/agent-credentials.js +7 -3
  22. package/dist/agent-repository.d.ts +16 -3
  23. package/dist/agent-repository.js +149 -74
  24. package/dist/agent-setup.d.ts +14 -3
  25. package/dist/agent-setup.js +78 -36
  26. package/dist/agent-target-commands.js +24 -13
  27. package/dist/args.d.ts +1 -1
  28. package/dist/args.js +104 -55
  29. package/dist/bin.js +5 -1
  30. package/dist/browser.d.ts +13 -2
  31. package/dist/browser.js +30 -14
  32. package/dist/claude-cloud-login.d.ts +24 -2
  33. package/dist/claude-cloud-login.js +151 -11
  34. package/dist/cli-proxy-forward.d.ts +2 -0
  35. package/dist/cli-proxy-forward.js +9 -2
  36. package/dist/cli-proxy-hub.d.ts +69 -25
  37. package/dist/cli-proxy-hub.js +231 -54
  38. package/dist/cli-proxy.d.ts +70 -1
  39. package/dist/cli-proxy.js +298 -32
  40. package/dist/client-settings.d.ts +39 -0
  41. package/dist/client-settings.js +91 -0
  42. package/dist/codex-cloud.d.ts +5 -0
  43. package/dist/codex-cloud.js +88 -17
  44. package/dist/command-center-actions.js +5 -1
  45. package/dist/commands.js +171 -22
  46. package/dist/conversation-commands.d.ts +5 -0
  47. package/dist/conversation-commands.js +8 -5
  48. package/dist/conversation-terminal.d.ts +10 -2
  49. package/dist/conversation-terminal.js +81 -15
  50. package/dist/conversations.d.ts +77 -1
  51. package/dist/conversations.js +257 -23
  52. package/dist/device-bridge.d.ts +1 -1
  53. package/dist/device-bridge.js +6 -4
  54. package/dist/device-helper.js +52 -4
  55. package/dist/errors.d.ts +34 -1
  56. package/dist/errors.js +56 -5
  57. package/dist/filesystem-commands.d.ts +22 -3
  58. package/dist/filesystem-commands.js +132 -16
  59. package/dist/format.d.ts +2 -1
  60. package/dist/format.js +10 -1
  61. package/dist/handlers.js +4 -1
  62. package/dist/initial-prompt.d.ts +8 -0
  63. package/dist/initial-prompt.js +43 -1
  64. package/dist/large-media.d.ts +14 -0
  65. package/dist/large-media.js +100 -0
  66. package/dist/linux-terminal.d.ts +5 -3
  67. package/dist/linux-terminal.js +6 -20
  68. package/dist/local-sessions.d.ts +26 -2
  69. package/dist/local-sessions.js +113 -24
  70. package/dist/machine-setup.d.ts +190 -0
  71. package/dist/machine-setup.js +849 -0
  72. package/dist/machine-status.d.ts +67 -0
  73. package/dist/machine-status.js +140 -0
  74. package/dist/main.js +23 -13
  75. package/dist/move-worktrees-restore.py +144 -0
  76. package/dist/move-worktrees.d.ts +63 -0
  77. package/dist/move-worktrees.js +201 -0
  78. package/dist/move.d.ts +10 -1
  79. package/dist/move.js +116 -16
  80. package/dist/onboarding.d.ts +11 -7
  81. package/dist/onboarding.js +59 -27
  82. package/dist/prepared-repository.d.ts +202 -0
  83. package/dist/prepared-repository.js +860 -0
  84. package/dist/prepared-repository.py +1306 -0
  85. package/dist/project-preparation.d.ts +48 -0
  86. package/dist/project-preparation.js +304 -0
  87. package/dist/project-sync-manifest.d.ts +4 -0
  88. package/dist/project-sync-manifest.js +5 -1
  89. package/dist/project-sync-transfer.js +1 -1
  90. package/dist/project-sync.d.ts +19 -4
  91. package/dist/project-sync.js +31 -8
  92. package/dist/project-template-setup.d.ts +33 -1
  93. package/dist/project-template-setup.js +59 -2
  94. package/dist/secret-ask.d.ts +72 -0
  95. package/dist/secret-ask.js +351 -0
  96. package/dist/seed-phases.d.ts +13 -3
  97. package/dist/seed-phases.js +21 -3
  98. package/dist/setup-cache.d.ts +7 -0
  99. package/dist/setup-cache.js +32 -0
  100. package/dist/t3-launch.js +1 -4
  101. package/dist/t3-mode.d.ts +8 -2
  102. package/dist/t3-mode.js +19 -4
  103. package/dist/t3-phone.d.ts +4 -2
  104. package/dist/t3-phone.js +12 -4
  105. package/dist/terminal-apps.d.ts +157 -0
  106. package/dist/terminal-apps.js +561 -0
  107. package/dist/terminal-onboarding.d.ts +2 -0
  108. package/dist/terminal-onboarding.js +11 -1
  109. package/dist/this-computer.d.ts +11 -0
  110. package/dist/this-computer.js +36 -0
  111. package/dist/thread-on-computer.js +4 -1
  112. package/dist/update.js +11 -3
  113. package/dist/worktree-twin.d.ts +10 -2
  114. package/dist/worktree-twin.js +27 -9
  115. package/package.json +5 -5
@@ -63,7 +63,7 @@ export declare class DeviceGrants {
63
63
  allows(api: string, project: string, root: string, kind: string, outside: boolean): boolean;
64
64
  save(api: string, project: string, root: string, kind: string): Promise<void>;
65
65
  }
66
- export declare function deviceSummary(r: DeviceRequest): string;
66
+ export declare function deviceSummary(r: DeviceRequest, platform?: NodeJS.Platform): string;
67
67
  /** Structured local approval details; the summary remains for older menu bar and ACP clients. */
68
68
  export declare function deviceApproval(r: DeviceRequest, c: Conversation, target: {
69
69
  path: string;
@@ -14,6 +14,7 @@ import { UsageError } from "./errors.js";
14
14
  import { conversationClient } from "./conversation-commands.js";
15
15
  import { withTargetLock } from "./target-lock.js";
16
16
  import { checkConversationWorktree, rememberConversationWorktree } from "./conversation-worktree.js";
17
+ import { yourComputer } from "./this-computer.js";
17
18
  const LIMIT = 256 << 20, OUTPUT = (192 << 10) - 128;
18
19
  const inside = (root, target) => target === root || target.startsWith(root + sep);
19
20
  const safeId = (value) => { if (!/^[a-zA-Z0-9_-]{1,96}$/.test(value))
@@ -67,14 +68,15 @@ export class DeviceGrants {
67
68
  });
68
69
  }
69
70
  }
70
- export function deviceSummary(r) {
71
+ export function deviceSummary(r, platform = process.platform) {
72
+ const yours = yourComputer(platform);
71
73
  if (tryLocallyAction(r))
72
74
  return tryLocallyAction(r) === 'stop' ? 'Stop this conversation’s local sync and ports.' : 'Try this conversation in an isolated local worktree with two-way sync and localhost ports.';
73
75
  if (r.kind === 'exec')
74
- return `Run on your Mac: ${JSON.stringify(r.exec?.argv)} · directory ${r.exec?.cwd_relative ?? '.'}`;
76
+ return `Run on ${yours}: ${JSON.stringify(r.exec?.argv)} · directory ${r.exec?.cwd_relative ?? '.'}`;
75
77
  if (r.files)
76
- return `${r.kind === 'files_pull' ? 'Share with conversation' : 'Save on your Mac'}: ${r.files.local_path_relative} ↔ ${r.files.remote_path}`;
77
- return `${r.kind === 'port_forward' ? 'Open conversation preview on your Mac' : 'Connect conversation to your Mac'}: ${r.port?.remote_port} ↔ localhost:${r.port?.local_port}`;
78
+ return `${r.kind === 'files_pull' ? 'Share with conversation' : `Save on ${yours}`}: ${r.files.local_path_relative} ↔ ${r.files.remote_path}`;
79
+ return `${r.kind === 'port_forward' ? `Open conversation preview on ${yours}` : `Connect conversation to ${yours}`}: ${r.port?.remote_port} ↔ localhost:${r.port?.local_port}`;
78
80
  }
79
81
  /** Structured local approval details; the summary remains for older menu bar and ACP clients. */
80
82
  export function deviceApproval(r, c, target, folder) {
@@ -4,15 +4,17 @@ import { ConversationMirrorState, mirrorProjectConversations } from "./conversat
4
4
  import { FilesMirrorState, mirrorFilesOnce } from "./files-mirror.js";
5
5
  import { withTargetLock } from "./target-lock.js";
6
6
  import { join } from 'node:path';
7
- import { conversationsUnavailable } from "./conversations.js";
8
- import { localConversations } from "./conversations.js";
7
+ import { completeDeferredPreparation, conversationsUnavailable, localConversations } from "./conversations.js";
9
8
  import { randomUUID } from 'node:crypto';
10
9
  import { readFileSync } from 'node:fs';
11
10
  import { writePrivateFile } from "./credentials.js";
12
11
  import { conversationClient } from "./conversation-commands.js";
13
12
  import { Readable } from 'node:stream';
14
13
  import { runWorktreeTwin } from "./worktree-twin-engine.js";
14
+ import { AgentConfigReturnState, runAgentConfigLoop } from "./agent-config-return.js";
15
15
  import { askLocalDeviceApproval } from "./device-approvals.js";
16
+ import { resumeCloudStartup, startCarried } from "./codex-cloud.js";
17
+ import { runProjectPreparation } from "./project-preparation.js";
16
18
  /** The menu bar owns this helper and its stdin; VM data can request actions but cannot answer approvals. */
17
19
  export async function runDeviceHelper(ctx, options = {}) {
18
20
  const abort = new AbortController(), signal = AbortSignal.any([ctx.signal, abort.signal]);
@@ -55,7 +57,7 @@ export async function runDeviceHelper(ctx, options = {}) {
55
57
  ctx.io.stdout.write(JSON.stringify({ type: 'approval_resolved', request_id: approval.request_id }) + '\n'); });
56
58
  };
57
59
  const working = new Map();
58
- const mirrored = new ConversationMirrorState(), files = new FilesMirrorState();
60
+ const mirrored = new ConversationMirrorState(), files = new FilesMirrorState(), config = new AgentConfigReturnState();
59
61
  const deviceFile = join(ctx.store.dir, 'device-id');
60
62
  let deviceId;
61
63
  try {
@@ -67,11 +69,53 @@ export async function runDeviceHelper(ctx, options = {}) {
67
69
  }
68
70
  const executor = new DeviceExecutor(ctx, ask, deviceId);
69
71
  const report = (e) => { ctx.io.stderr.write(`Device bridge: ${e instanceof Error ? e.message : String(e)}\n`); };
70
- let mirroring, twin;
72
+ let mirroring, twin, returning, preparing, projects;
71
73
  try {
72
74
  return await withTargetLock(join(ctx.store.dir, 'device-helper-lock'), async () => {
73
75
  // Finished conversations land in their local branches (worktree-twin-engine.ts); its own loop and backoff.
74
76
  twin = runWorktreeTwin(ctx, deviceId, signal).catch(report);
77
+ // Skills, memories and CLAUDE.md lines saved in the cloud come back on their own cadence (agent-config-return.ts):
78
+ // never behind a files-mirror pass, which takes minutes for an account with hundreds of conversations. Every 2 s
79
+ // while one of this user's conversations is active, backing off to 60 s when none is.
80
+ returning = runAgentConfigLoop(ctx, config, signal).catch(report);
81
+ projects = runProjectPreparation(ctx).catch(report);
82
+ // Starts outlive the foreground command (0.22.0+): an accepted Codex Cloud start resumes with its own revision
83
+ // and idempotent admission and message; large media that followed a first prompt land while the VM is awake.
84
+ preparing = (async () => {
85
+ const failures = new Map();
86
+ while (!signal.aborted) {
87
+ let deferred = false;
88
+ for (const local of Object.values(localConversations(ctx))) {
89
+ if (local.conversation.ended_at || (failures.get(local.conversation.id) ?? 0) > Date.now())
90
+ continue;
91
+ try {
92
+ // A start a foreground command still carries is its own: resumed only once nobody does.
93
+ if (local.cloud_start && local.cloud_start.state !== 'active' && !local.cloud_start.attachments_pending) {
94
+ if (!startCarried(local.cloud_start))
95
+ await resumeCloudStartup(ctx, local.conversation);
96
+ }
97
+ else if (local.preparation?.state === 'deferred') {
98
+ // A parked VM is never woken for this: its media land the next time it is awake.
99
+ if (await completeDeferredPreparation(ctx, local.conversation))
100
+ failures.delete(local.conversation.id);
101
+ else {
102
+ deferred = true;
103
+ failures.set(local.conversation.id, Date.now() + 15_000);
104
+ }
105
+ continue;
106
+ }
107
+ failures.delete(local.conversation.id);
108
+ }
109
+ catch (error) {
110
+ if (signal.aborted)
111
+ break;
112
+ report(error);
113
+ failures.set(local.conversation.id, Date.now() + 60_000);
114
+ }
115
+ }
116
+ await delay(deferred ? 5_000 : 30_000, undefined, { signal }).catch(() => undefined);
117
+ }
118
+ })();
75
119
  // A slow or failed transcript copy must not delay heartbeats, request polling or approvals.
76
120
  mirroring = (async () => {
77
121
  let failures = 0;
@@ -133,7 +177,11 @@ export async function runDeviceHelper(ctx, options = {}) {
133
177
  await executor.close();
134
178
  await Promise.all([...working.values()]);
135
179
  await mirroring;
180
+ await returning;
181
+ await config.folding;
136
182
  await twin;
183
+ await preparing;
184
+ await projects;
137
185
  await input.catch(() => undefined);
138
186
  }
139
187
  }
package/dist/errors.d.ts CHANGED
@@ -58,9 +58,21 @@ export declare class TermsNotAcceptedError extends CodedError {
58
58
  export declare class AuthConfigError extends HintedError {
59
59
  name: string;
60
60
  }
61
- /** A workspace key that no workspace of this project has (or another name that resolves to nothing). */
61
+ /**
62
+ * A workspace key that no workspace of this project has (or another name that resolves to nothing). `details` keeps the
63
+ * API's reason when shard rewords its 404 (0.20.1+: `filesystem_not_found` from shard template delete).
64
+ */
62
65
  export declare class NotFoundError extends HintedError {
63
66
  name: string;
67
+ readonly details: Record<string, unknown> | undefined;
68
+ constructor(message: string, hint?: string, details?: Record<string, unknown>);
69
+ }
70
+ /**
71
+ * `shard machine setup --from-this-computer` (0.20.0+) ran but did not save a machine: the agent failed, ran past its
72
+ * time box, or the save failed. Retryable: the same command runs it again (exit 6).
73
+ */
74
+ export declare class MachineSetupError extends HintedError {
75
+ name: string;
64
76
  }
65
77
  /** `auth login` of a two-factor account without a code: the pending sign-in is saved; `shard auth mfa` completes it (exit 4). */
66
78
  export declare class MfaPendingError extends HintedError {
@@ -140,6 +152,27 @@ export interface ErrorInfo {
140
152
  */
141
153
  export declare function redact(text: string, ...secrets: (string | null | undefined)[]): string;
142
154
  /** The `hint:` a CLI error carries itself, if any. */
155
+ /**
156
+ * After a save that failed (0.20.2+): the version in use and the source VM are as they were, so the same save can run
157
+ * again; the build's log says what happened.
158
+ */
159
+ export declare function saveRecovery(level: 'machine' | 'template', inUse: number, buildId?: string, failure?: SaveFailure): string;
160
+ /** What a failed save reports about its build (SDK 0.26.0+ on an API that carries them). */
161
+ export interface SaveFailure {
162
+ code?: string | undefined;
163
+ retryable?: boolean | undefined;
164
+ details?: Record<string, unknown> | undefined;
165
+ }
166
+ /**
167
+ * A save larger than the plan's workspace disk (0.21.1+): resource_limit with details.limit template_org_bytes_max;
168
+ * `limit` is the plan's workspace disk and `actual` the version's size, in bytes.
169
+ */
170
+ export declare function planDiskLimit(failure: SaveFailure | undefined): {
171
+ limit: number;
172
+ actual: number;
173
+ } | undefined;
174
+ /** Bytes as the plan names a disk: `2 GiB`, `2.5 GiB`. */
175
+ export declare const gib: (bytes: number) => string;
143
176
  export declare function ownHint(err: unknown): string | undefined;
144
177
  export declare function describeError(err: unknown): ErrorInfo;
145
178
  /** Maps an error to the documented exit code. */
package/dist/errors.js CHANGED
@@ -83,9 +83,24 @@ export class TermsNotAcceptedError extends CodedError {
83
83
  export class AuthConfigError extends HintedError {
84
84
  name = 'AuthConfigError';
85
85
  }
86
- /** A workspace key that no workspace of this project has (or another name that resolves to nothing). */
86
+ /**
87
+ * A workspace key that no workspace of this project has (or another name that resolves to nothing). `details` keeps the
88
+ * API's reason when shard rewords its 404 (0.20.1+: `filesystem_not_found` from shard template delete).
89
+ */
87
90
  export class NotFoundError extends HintedError {
88
91
  name = 'NotFoundError';
92
+ details;
93
+ constructor(message, hint, details) {
94
+ super(message, hint);
95
+ this.details = details;
96
+ }
97
+ }
98
+ /**
99
+ * `shard machine setup --from-this-computer` (0.20.0+) ran but did not save a machine: the agent failed, ran past its
100
+ * time box, or the save failed. Retryable: the same command runs it again (exit 6).
101
+ */
102
+ export class MachineSetupError extends HintedError {
103
+ name = 'MachineSetupError';
89
104
  }
90
105
  /** `auth login` of a two-factor account without a code: the pending sign-in is saved; `shard auth mfa` completes it (exit 4). */
91
106
  export class MfaPendingError extends HintedError {
@@ -168,6 +183,34 @@ export function redact(text, ...secrets) {
168
183
  return out;
169
184
  }
170
185
  /** The `hint:` a CLI error carries itself, if any. */
186
+ /**
187
+ * After a save that failed (0.20.2+): the version in use and the source VM are as they were, so the same save can run
188
+ * again; the build's log says what happened.
189
+ */
190
+ export function saveRecovery(level, inUse, buildId, failure) {
191
+ const kept = inUse > 0 ? `version ${inUse} stays in use and ` : '';
192
+ const vm = level === 'machine' ? 'machine' : 'project';
193
+ // Larger than the plan's workspace disk (0.21.1+): the same save fails again until the VM or the plan changes.
194
+ const disk = planDiskLimit(failure);
195
+ if (disk) {
196
+ const open = level === 'machine' ? ' (shard machine open)' : '';
197
+ return `${kept}your ${vm} VM is untouched; delete what it does not need there${open}, such as caches and build output, then run shard ${level} save --wait; or upgrade to a plan with more than ${gib(disk.limit)} of disk: shard billing plans`;
198
+ }
199
+ if (failure?.retryable === false)
200
+ return `${kept}your ${vm} VM is untouched; change what the message names in it, then run shard ${level} save --wait`;
201
+ const log = buildId ? `; build log: shard templates builds log ${buildId}` : '';
202
+ return `${kept}your ${vm} VM is untouched; run shard ${level} save --wait again (it saves from the same VM)${log}`;
203
+ }
204
+ /**
205
+ * A save larger than the plan's workspace disk (0.21.1+): resource_limit with details.limit template_org_bytes_max;
206
+ * `limit` is the plan's workspace disk and `actual` the version's size, in bytes.
207
+ */
208
+ export function planDiskLimit(failure) {
209
+ const d = failure?.details;
210
+ return failure?.code === 'resource_limit' && d?.limit === 'template_org_bytes_max' && typeof d.value === 'number' && typeof d.actual === 'number' ? { limit: d.value, actual: d.actual } : undefined;
211
+ }
212
+ /** Bytes as the plan names a disk: `2 GiB`, `2.5 GiB`. */
213
+ export const gib = (bytes) => `${Number((bytes / 2 ** 30).toFixed(2))} GiB`;
171
214
  export function ownHint(err) {
172
215
  return err instanceof HintedError ? err.hint : undefined;
173
216
  }
@@ -253,10 +296,14 @@ export function describeError(err) {
253
296
  if (err instanceof TemplateBuildTimeoutError)
254
297
  return { code: 'timeout', message: `${err.message}; follow it with: shard templates builds get ${err.build.id}`, last_state: err.build.state };
255
298
  // machine/template save --wait (0.13.3+): the save ended without its version, or is still building at the deadline.
256
- if (err instanceof FilesystemSaveFailedError)
257
- return { code: 'filesystem_save_failed', message: err.message, retryable: true, details: { level: err.state.level, version: err.version } };
299
+ // 0.20.2+: details.build_id, and a failure's recovery as `fix` (the hint line).
300
+ // 0.21.1+: the build's failure (error_code and its details, e.g. limit/value/actual) and whether the same save can
301
+ // succeed (retryable false: larger than the plan's workspace disk, or another refusal that repeats until the VM changes).
302
+ if (err instanceof FilesystemSaveFailedError) {
303
+ return { code: 'filesystem_save_failed', message: err.message, retryable: err.retryable, fix: saveRecovery(err.state.level, err.state.version, err.buildId, err), details: { ...(err.details ?? {}), ...(err.code ? { error_code: err.code } : {}), level: err.state.level, version: err.version, ...(err.buildId ? { build_id: err.buildId } : {}), ...(err.progress ? { last_progress: err.progress } : {}) } };
304
+ }
258
305
  if (err instanceof FilesystemSaveTimeoutError)
259
- return { code: 'timeout', message: `${err.message}; check with shard ${err.state.level}`, last_state: err.state.state, details: { level: err.state.level, version: err.version } };
306
+ return { code: 'timeout', message: `${err.message}; check with shard ${err.state.level}`, last_state: err.state.state, details: { level: err.state.level, version: err.version, ...(err.buildId ? { build_id: err.buildId } : {}), ...(err.progress ? { last_progress: err.progress } : {}) } };
260
307
  if (err instanceof TemplateUploadError)
261
308
  return { code: 'upload_failed', message: err.message, ...(err.status ? { status: err.status } : {}), details: { sha256: err.sha256, ...(err.code ? { storage_code: err.code } : {}) } };
262
309
  if (err instanceof AuthConfigError)
@@ -264,7 +311,9 @@ export function describeError(err) {
264
311
  if (err instanceof MfaPendingError)
265
312
  return { code: 'mfa_required', message: err.message };
266
313
  if (err instanceof NotFoundError)
267
- return { code: 'not_found', message: err.message };
314
+ return { code: 'not_found', message: err.message, ...(err.details ? { details: err.details } : {}) };
315
+ if (err instanceof MachineSetupError)
316
+ return { code: 'machine_setup_failed', message: err.message, retryable: true };
268
317
  if (err instanceof InterruptedError) {
269
318
  return { code: 'interrupted', message: err.message, ...(err.operationId ? { operation_id: err.operationId } : {}), ...(err.executionId ? { execution_id: err.executionId } : {}) };
270
319
  }
@@ -294,6 +343,8 @@ export function exitCodeFor(err, interrupted = false) {
294
343
  return EXIT.operationFailed;
295
344
  if (err instanceof NotFoundError)
296
345
  return EXIT.notFound;
346
+ if (err instanceof MachineSetupError)
347
+ return EXIT.operationFailed;
297
348
  if (err instanceof OperationTimeoutError || err instanceof CheckoutTimeoutError || isNamed(err, 'TimeoutError'))
298
349
  return EXIT.timeout;
299
350
  if (err instanceof OperationFailedError || err instanceof DurabilityLostError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
@@ -1,12 +1,31 @@
1
- import type { AgentFilesystem, Workspace } from '@shardflux/sdk';
1
+ import type { AgentFilesystem, AgentFilesystemDeleted, Workspace } from '@shardflux/sdk';
2
2
  import type { Ctx, Handler } from './commands.js';
3
- /** One line per status: what is in use and what is saving or failed, its platform base and setup cache. */
3
+ /**
4
+ * One line per status: what is in use and what is saving or failed, its platform base and setup cache. A project
5
+ * template unused for 30 days (rebase_deferred, 0.20.1+) follows the machine when the next conversation starts.
6
+ */
4
7
  export declare function filesystemLine(state: AgentFilesystem & {
5
8
  cache_scope?: string;
6
9
  }): string;
10
+ /** A build's place while it waits for one of the plan's build slots (API: TemplateBuild.queue). */
11
+ export interface BuildQueue {
12
+ position: number;
13
+ slots: number;
14
+ expected_wait_seconds: number;
15
+ }
16
+ /** The pending build's queue, if it waits for a slot; nothing when the API predates it or the build cannot be read. */
17
+ export declare function pendingQueue(ctx: Ctx, state: AgentFilesystem): Promise<BuildQueue | undefined>;
18
+ /** Within one phase of a waited save, a changed counter line is printed at most this often (0.20.2+). */
19
+ export declare const SAVE_PROGRESS_LINE_MS = 30000;
7
20
  /** The save's one line about software under the home that went into the image (0.18.0+), as the in-VM shard says it. */
8
21
  export declare function movedLine(state: AgentFilesystem, level: 'machine' | 'template'): string;
9
- /** Creates or resumes the machine VM of the saved machine version; the first one gets this computer's agent config. */
22
+ /** `shard template delete` (0.20.1+): what went, in one line. A binding-only delete has no template. */
23
+ export declare function deletedLine(deleted: AgentFilesystemDeleted): string;
24
+ /**
25
+ * The VM `shard machine save` saves: the machine VM of the version in use, else the one the last save came from. A
26
+ * deleted one is not there (0.20.2+: the machine row offers Save again only while it is).
27
+ */
28
+ export declare function machineSaveSource(ctx: Ctx, state: AgentFilesystem): Promise<Workspace | null>;
10
29
  /** The account's machine VM, created or resumed; `sync: false` skips the first-open config sync (an account sign-in). */
11
30
  export declare function openMachine(ctx: Ctx, state?: AgentFilesystem, opts?: {
12
31
  sync?: boolean;
@@ -1,6 +1,8 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { FilesystemSaveFailedError, FilesystemSaveTimeoutError, SavePhases, ShardfluxApiError, savePreflightText, saveProgressText } from '@shardflux/sdk';
1
3
  import { codingAgentShell } from "./agent-setup.js";
2
4
  import { waitTimeout } from "./commands.js";
3
- import { UsageError } from "./errors.js";
5
+ import { NotFoundError, UsageError } from "./errors.js";
4
6
  import { projectIdentity, normalizeRemote } from "./project-identity.js";
5
7
  import { AGENT_HANDLERS } from "./agent-commands.js";
6
8
  import { readSetupProgress } from "./assisted-setup.js";
@@ -10,9 +12,13 @@ function remote() {
10
12
  const project = projectIdentity('./');
11
13
  return { project, repoRemote: project.remote ?? normalizeRemote(`file://${project.folder}`) };
12
14
  }
13
- /** One line per status: what is in use and what is saving or failed, its platform base and setup cache. */
15
+ /**
16
+ * One line per status: what is in use and what is saving or failed, its platform base and setup cache. A project
17
+ * template unused for 30 days (rebase_deferred, 0.20.1+) follows the machine when the next conversation starts.
18
+ */
14
19
  export function filesystemLine(state) {
15
- const base = state.platform_base ? `; platform ${state.platform_base.slug}@${state.platform_base.version}${state.rebase_pending ? ', rebase pending' : ''}` : '';
20
+ const rebase = state.rebase_deferred ? ', follows your machine on next use' : state.rebase_pending ? ', rebase pending' : '';
21
+ const base = state.platform_base ? `; platform ${state.platform_base.slug}@${state.platform_base.version}${rebase}` : '';
16
22
  return usageLine(state) + base + (state.cache_scope ? `; cache: ${state.cache_scope}` : '');
17
23
  }
18
24
  function usageLine(state) {
@@ -29,17 +35,87 @@ function output(ctx, state) {
29
35
  ctx.io.stdout.write(ctx.json ? JSON.stringify(state) + '\n' : `${filesystemLine(state)}\n`);
30
36
  return 0;
31
37
  }
32
- /** Save output: one line naming the version being saved, or with --wait the saved version (progress on stderr). */
33
- async function saved(ctx, values, save, level) {
38
+ /** The pending build's queue, if it waits for a slot; nothing when the API predates it or the build cannot be read. */
39
+ export async function pendingQueue(ctx, state) {
40
+ if (!state.pending)
41
+ return undefined;
42
+ try {
43
+ const build = await ctx.cloud().templates.builds.get(state.organization_id, state.pending.build_id);
44
+ return build.queue ?? undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ /** Within one phase of a waited save, a changed counter line is printed at most this often (0.20.2+). */
51
+ export const SAVE_PROGRESS_LINE_MS = 30_000;
52
+ /**
53
+ * Save output: the preflight line (what the save copies and about how long it takes, 0.20.2+), then one line naming
54
+ * the version being saved and its state (its place in the queue while the plan's build slots are in use, 0.20.0+), or
55
+ * with --wait one stderr line per phase with its counters, and the saved version. --json: one document at the end
56
+ * (`phases` with --wait, `preflight` when known), the progress as {"event":"save_progress"} lines on stderr.
57
+ */
58
+ async function saved(ctx, values, save, preflight, level) {
34
59
  if (values.wait !== true && values.timeout !== undefined)
35
60
  throw new UsageError('--timeout needs --wait');
36
- const wait = values.wait === true ? { timeoutMs: waitTimeout(values, 600_000), onChange: (state) => { if (state.state === 'building' && state.pending)
37
- progress(ctx, `Saving ${level} version ${state.pending.version}…`); } } : undefined;
38
- const state = await save({ signal: ctx.signal, ...(wait ? { wait } : {}) });
61
+ const pf = await preflight();
62
+ if (pf && !ctx.json)
63
+ progress(ctx, savePreflightText(pf));
64
+ else if (pf && values.wait === true)
65
+ ctx.io.stderr.write(`${JSON.stringify({ event: 'save_preflight', ...pf })}\n`);
66
+ const phases = new SavePhases();
67
+ // The first line names the version; later lines say what changed: a new phase or place in the queue at once, newer
68
+ // counters within a phase at most every SAVE_PROGRESS_LINE_MS.
69
+ let announced = false;
70
+ let last = { line: '', milestone: '', at: 0 };
71
+ const onProgress = (p, final = false) => {
72
+ phases.update(p);
73
+ const line = saveProgressText(p);
74
+ const milestone = JSON.stringify([p.state, p.progress?.phase ?? null, p.queue?.position ?? null, p.queue?.slots ?? null]);
75
+ const now = Date.now();
76
+ if (announced && (line === last.line || (!final && milestone === last.milestone && now - last.at < SAVE_PROGRESS_LINE_MS)))
77
+ return;
78
+ if (ctx.json)
79
+ ctx.io.stderr.write(`${JSON.stringify({ event: 'save_progress', ...p })}\n`);
80
+ else
81
+ progress(ctx, announced ? line : `${level}: saving version ${p.version} · ${line}`);
82
+ announced = true;
83
+ last = { line, milestone, at: now };
84
+ };
85
+ const wait = values.wait === true ? { timeoutMs: waitTimeout(values, 600_000), onProgress, onChange: (state) => {
86
+ // An API that does not show the build: the version alone.
87
+ if (!announced && state.state === 'building' && state.pending) {
88
+ // The filesystem says saving; the unavailable build view cannot tell us queued versus running.
89
+ if (ctx.json)
90
+ ctx.io.stderr.write(`${JSON.stringify({ event: 'save_started', level, version: state.pending.version, build_id: state.pending.build_id })}\n`);
91
+ else
92
+ progress(ctx, `${level}: saving version ${state.pending.version}`);
93
+ announced = true;
94
+ }
95
+ } } : undefined;
96
+ const document = (state, extra = {}) => ({ ...state, ...extra, ...(wait ? { phases: phases.list() } : {}), ...(pf ? { preflight: pf } : {}) });
97
+ let state;
98
+ try {
99
+ state = await save({ signal: ctx.signal, ...(wait ? { wait } : {}) });
100
+ }
101
+ catch (err) {
102
+ if ((err instanceof FilesystemSaveTimeoutError || err instanceof FilesystemSaveFailedError) && err.progress)
103
+ onProgress(err.progress, true);
104
+ // --json: stdout still gets its one document, with how far the save got (still building at a timeout).
105
+ if (ctx.json && (err instanceof FilesystemSaveTimeoutError || err instanceof FilesystemSaveFailedError)) {
106
+ if (err instanceof FilesystemSaveFailedError)
107
+ phases.end();
108
+ ctx.io.stdout.write(`${JSON.stringify(document(err.state))}\n`);
109
+ }
110
+ throw err;
111
+ }
112
+ phases.end();
113
+ const queue = wait ? undefined : await pendingQueue(ctx, state);
39
114
  if (ctx.json)
40
- return output(ctx, state);
115
+ return output(ctx, document(state, queue ? { queue } : {}));
41
116
  const moved = movedLine(state, level);
42
- ctx.io.stdout.write(wait ? `${level}: version ${state.version} saved${moved}\n` : state.pending ? `${filesystemLine(state)}; shard ${level} shows when it is ready${moved}\n` : `${filesystemLine(state)}${moved}\n`);
117
+ const queued = queue ? `; ${saveProgressText({ version: state.pending?.version ?? 0, build_id: state.pending?.build_id ?? '', state: 'queued', queue })}` : '';
118
+ ctx.io.stdout.write(wait ? `${level}: version ${state.version} saved${moved}\n` : state.pending ? `${filesystemLine(state)}${queued}; shard ${level} shows when it is ready${moved}\n` : `${filesystemLine(state)}${moved}\n`);
43
119
  return 0;
44
120
  }
45
121
  /** The save's one line about software under the home that went into the image (0.18.0+), as the in-VM shard says it. */
@@ -49,6 +125,12 @@ export function movedLine(state, level) {
49
125
  return '';
50
126
  return `; ${moved.length} large tool folder${moved.length === 1 ? '' : 's'} went into the ${level} image (${moved.join(', ')}); they keep working at the same paths`;
51
127
  }
128
+ /** `shard template delete` (0.20.1+): what went, in one line. A binding-only delete has no template. */
129
+ export function deletedLine(deleted) {
130
+ const versions = deleted.template ? ` (${deleted.template.versions} saved version${deleted.template.versions === 1 ? '' : 's'})` : '';
131
+ const builds = deleted.canceled_builds > 0 ? `, stopped ${deleted.canceled_builds} build${deleted.canceled_builds === 1 ? '' : 's'}` : '';
132
+ return `template: deleted${versions}${builds}; new conversations start from your machine until setup saves a new one`;
133
+ }
52
134
  const rollbackTo = (value) => {
53
135
  if (value === undefined)
54
136
  return undefined;
@@ -57,7 +139,17 @@ const rollbackTo = (value) => {
57
139
  throw new UsageError('--to must be a saved version number');
58
140
  return n;
59
141
  };
60
- /** Creates or resumes the machine VM of the saved machine version; the first one gets this computer's agent config. */
142
+ /**
143
+ * The VM `shard machine save` saves: the machine VM of the version in use, else the one the last save came from. A
144
+ * deleted one is not there (0.20.2+: the machine row offers Save again only while it is).
145
+ */
146
+ export async function machineSaveSource(ctx, state) {
147
+ const cloud = ctx.cloud();
148
+ const ws = await cloud.workspaces.findByKey(`machine/${state.organization_id}/v${state.version}`, { includeDeleted: false, signal: ctx.signal }) ??
149
+ (state.workspace_id ? await cloud.workspaces.get(state.workspace_id).catch((e) => { if (e instanceof ShardfluxApiError && e.status === 404)
150
+ return null; throw e; }) : null);
151
+ return ws && !ws.data.deleted_at ? ws : null;
152
+ }
61
153
  /** The account's machine VM, created or resumed; `sync: false` skips the first-open config sync (an account sign-in). */
62
154
  export async function openMachine(ctx, state, opts = {}) {
63
155
  const cloud = ctx.cloud();
@@ -99,12 +191,10 @@ export const FILESYSTEM_HANDLERS = {
99
191
  },
100
192
  async 'machine save'(ctx, values) {
101
193
  const cloud = ctx.cloud();
102
- const state = await cloud.machine.get({ signal: ctx.signal });
103
- const ws = await cloud.workspaces.findByKey(`machine/${state.organization_id}/v${state.version}`, { signal: ctx.signal }) ??
104
- (state.workspace_id ? await cloud.workspaces.get(state.workspace_id) : null);
194
+ const ws = await machineSaveSource(ctx, await cloud.machine.get({ signal: ctx.signal }));
105
195
  if (!ws)
106
196
  throw new UsageError('Open your machine and change it first, then run shard machine save', 'shard machine in a terminal, or shard machine open --json for an agent (0.17.1+)');
107
- return saved(ctx, values, options => cloud.machine.save({ workspaceId: ws.id }, options), 'machine');
197
+ return saved(ctx, values, options => cloud.machine.save({ workspaceId: ws.id }, options), () => cloud.machine.preflight({ workspaceId: ws.id }, { signal: ctx.signal }), 'machine');
108
198
  },
109
199
  async 'machine rollback'(ctx, values) {
110
200
  return output(ctx, await ctx.cloud().machine.rollback({ to: rollbackTo(values.to) }, { signal: ctx.signal }));
@@ -123,9 +213,35 @@ export const FILESYSTEM_HANDLERS = {
123
213
  const ws = await ctx.cloud().workspaces.findByKey(`${project.slug}/main`, { signal: ctx.signal });
124
214
  if (!ws)
125
215
  throw new UsageError('Run shard ./ for this project before saving its template');
126
- return saved(ctx, values, options => ctx.cloud().templates.saveProject({ repoRemote, workspaceId: ws.id }, options), 'template');
216
+ return saved(ctx, values, options => ctx.cloud().templates.saveProject({ repoRemote, workspaceId: ws.id }, options), () => ctx.cloud().templates.preflightProject({ workspaceId: ws.id }, { signal: ctx.signal }), 'template');
127
217
  },
128
218
  async 'template rollback'(ctx, values) {
129
219
  return output(ctx, await ctx.cloud().templates.rollbackProject({ repoRemote: remote().repoRemote, to: rollbackTo(values.to) }, { signal: ctx.signal }));
130
220
  },
221
+ /** `shard template delete` (0.20.1+): asks first in a terminal; elsewhere it needs --yes. */
222
+ async 'template delete'(ctx, values) {
223
+ const { repoRemote } = remote();
224
+ if (values.yes !== true) {
225
+ if (!ctx.input.interactive)
226
+ throw new UsageError(`refusing to delete without --yes (the project template of ${repoRemote} and its saved versions are deleted; new conversations start from your machine until setup saves a new one)`, 'run: shard template delete --yes');
227
+ const answer = (await ctx.input.answer(`Delete the project template for ${repoRemote}? Its saved versions go; new conversations start from your machine until setup saves a new one. [y/N] `))?.trim() ?? '';
228
+ if (!/^y(es)?$/i.test(answer)) {
229
+ ctx.io.stderr.write('Kept the project template.\n');
230
+ return 0;
231
+ }
232
+ }
233
+ let deleted;
234
+ try {
235
+ // One key per run: a retry after a dropped connection replays the same delete.
236
+ deleted = await ctx.cloud().templates.deleteProject({ repoRemote }, { idempotencyKey: randomUUID(), signal: ctx.signal });
237
+ }
238
+ catch (err) {
239
+ if (err instanceof ShardfluxApiError && err.status === 404 && err.details?.reason === 'filesystem_not_found') {
240
+ throw new NotFoundError(`template: nothing saved yet for ${repoRemote}, so there is nothing to delete`, 'setup saves it after shard ./, or run shard template save', { reason: 'filesystem_not_found', repo_remote: repoRemote });
241
+ }
242
+ throw err;
243
+ }
244
+ ctx.io.stdout.write(ctx.json ? JSON.stringify(deleted) + '\n' : `${deletedLine(deleted)}\n`);
245
+ return 0;
246
+ },
131
247
  };
package/dist/format.d.ts CHANGED
@@ -1,4 +1,4 @@
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, TemplateVersion, WorkspaceChange, WorkspaceSecretBindings, WorkspaceMemory, WorkspaceView, MemoryGrow, BurstSummary, ResizeResult, ExposePortResult, ExposedPort } from '@shardflux/sdk';
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, SuspendProgress, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, TemplateVersion, WorkspaceChange, WorkspaceSecretBindings, WorkspaceMemory, WorkspaceView, MemoryGrow, BurstSummary, ResizeResult, ExposePortResult, ExposedPort } from '@shardflux/sdk';
2
2
  import type { ErrorInfo } from './errors.js';
3
3
  /** A workspace as the port commands name it. */
4
4
  interface PortWorkspace {
@@ -70,6 +70,7 @@ export declare function operationTable(ops: readonly Operation[]): string;
70
70
  */
71
71
  export declare function durableText(op: Operation): string;
72
72
  export declare function operationDetail(op: Operation): string;
73
+ export declare function suspendProgressText(p: SuspendProgress): string;
73
74
  export declare function sessionTable(sessions: readonly AgentSession[]): string;
74
75
  /** `files stat`: one path's metadata; the revision only when requested and returned. */
75
76
  export declare function fileInfoText(f: FileInfo): string;
package/dist/format.js CHANGED
@@ -300,6 +300,15 @@ export function operationDetail(op) {
300
300
  ['updated', time(op.updated_at)],
301
301
  ['completed', time(op.completed_at)],
302
302
  ['error', err ? `${scalar(err.code)}${err.message ? `: ${scalar(err.message)}` : ''}` : '-'],
303
+ ]) + (op.progress ? `\n${suspendProgressText(op.progress)}` : '');
304
+ }
305
+ export function suspendProgressText(p) {
306
+ return kv([
307
+ ['operation', p.operation_id], ['phase', p.phase], ['operation state', p.state], ['workspace state', p.workspace_state],
308
+ ['cancelable', p.cancelable ? 'yes' : 'no'], ['progress supported', p.supported ? 'yes' : 'unavailable'], ['next action', p.safe_action],
309
+ ...(p.phase_started_at ? [['phase started', p.phase_started_at]] : []),
310
+ ...(p.deadline_at ? [['host deadline', p.deadline_at]] : []),
311
+ ...(p.accepted === true ? [['cancel request', 'accepted; use the operation state to confirm recovery']] : []),
303
312
  ]);
304
313
  }
305
314
  export function sessionTable(sessions) {
@@ -546,7 +555,7 @@ export function feedbackCommand(e) {
546
555
  if (SHELL_SAFE.test(code))
547
556
  parts.push(`--error-code ${code}`);
548
557
  parts.push('"<what you expected>"');
549
- return parts.join(' ');
558
+ return `${parts.join(' ')} # preview only; show the message and context, then use --yes only after explicit user authorization`;
550
559
  }
551
560
  /**
552
561
  * One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one, and the
package/dist/handlers.js CHANGED
@@ -23,4 +23,7 @@ import { CLI_PROXY_HANDLERS } from "./cli-proxy.js";
23
23
  import { ACCOUNT_POOL_HANDLERS } from "./account-pool.js";
24
24
  import { MOVE_HANDLERS } from "./move.js";
25
25
  import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
26
- export const HANDLERS = { ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...UPDATE_HANDLERS, 'agents config-sync': AGENT_CONFIG_HANDLERS['agents config-sync'] };
26
+ import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
27
+ import { SETTINGS_HANDLERS } from "./client-settings.js";
28
+ import { PROJECT_PREPARATION_HANDLERS } from "./project-preparation.js";
29
+ export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS };
@@ -30,6 +30,14 @@ export declare const INBOX_MAX_CHARS = 65536;
30
30
  * options. The guest's own first-message delivery reads the screen the same way (coding 39+).
31
31
  */
32
32
  export declare function inputBoxReady(tail: string, glyph: string): boolean;
33
+ /**
34
+ * The first prompt typed by the VM's own guest (0.21.1+, with a guest that has it): `sf-agent conversation first-prompt` waits for
35
+ * the input box, pastes, waits for the paste to show and presses Enter inside the VM, with the same checks as
36
+ * `submitInitialPrompt`, in one request from here instead of an attachment and three round trips. Nothing was typed
37
+ * when the result is `unsupported` (an older guest without the command: this terminal types it) or `not_ready` (the
38
+ * harness never showed its input box within the guest's wait: the conversation's inbox takes it, where there is one).
39
+ */
40
+ export declare function guestFirstPrompt(cell: Pick<CellClient, 'exec'>, ptyId: string, app: 'claude' | 'codex', prompt: string, workspaceKey: string, signal: AbortSignal): Promise<'submitted' | 'unsupported' | 'not_ready'>;
33
41
  /**
34
42
  * Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay.
35
43
  * When nothing was typed yet and the terminal never became ready, `queue` hands the prompt to the conversation's guest