@shardflux/cli 0.19.0 → 0.20.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 +77 -1
- package/README.md +14 -2
- package/dist/account-pool.d.ts +1 -0
- package/dist/account-pool.js +4 -1
- package/dist/agent-commands.js +7 -2
- package/dist/agent-config-files.d.ts +19 -0
- package/dist/agent-config-files.js +70 -1
- package/dist/agent-config-return.d.ts +150 -0
- package/dist/agent-config-return.js +618 -0
- package/dist/agent-config-sync.d.ts +10 -2
- package/dist/agent-config-sync.js +45 -51
- package/dist/agent-setup.js +2 -2
- package/dist/args.d.ts +1 -1
- package/dist/args.js +11 -2
- package/dist/conversation-terminal.js +4 -2
- package/dist/conversations.d.ts +4 -0
- package/dist/device-helper.js +29 -2
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +11 -0
- package/dist/filesystem-commands.d.ts +12 -0
- package/dist/filesystem-commands.js +68 -6
- package/dist/handlers.js +2 -1
- package/dist/initial-prompt.d.ts +15 -2
- package/dist/initial-prompt.js +29 -4
- package/dist/machine-setup.d.ts +180 -0
- package/dist/machine-setup.js +802 -0
- package/dist/main.js +14 -2
- package/dist/seed-phases.d.ts +5 -2
- package/dist/seed-phases.js +4 -1
- package/dist/terminal-onboarding.d.ts +2 -0
- package/dist/terminal-onboarding.js +5 -0
- package/package.json +5 -5
|
@@ -7,7 +7,8 @@ import { parse, stringify } from 'smol-toml';
|
|
|
7
7
|
import { ShardfluxApiError, tarEnd, tarHeader, tarPadding } from '@shardflux/sdk';
|
|
8
8
|
import { AgentStateStore } from "./agent-state.js";
|
|
9
9
|
import { UsageError } from "./errors.js";
|
|
10
|
-
import { collectPlugins, collectUserFiles, object, readObject, readOptional, rewritePaths } from "./agent-config-files.js";
|
|
10
|
+
import { collectMemory, collectPlugins, collectUserFiles, object, readObject, readOptional, rewritePaths, vmMemoryPath } from "./agent-config-files.js";
|
|
11
|
+
import { localConversations } from "./conversations.js";
|
|
11
12
|
import { digest, prepareMcp } from "./agent-config-mcp.js";
|
|
12
13
|
import { configIssueLines } from "./agent-config-report.js";
|
|
13
14
|
const missing = (e) => e instanceof ShardfluxApiError && e.status === 404;
|
|
@@ -15,7 +16,8 @@ const parseFailure = (e) => e instanceof SyntaxError || (e instanceof Error && /
|
|
|
15
16
|
const markerPath = '.config/shardflux/agent-config.json';
|
|
16
17
|
const safePath = (path) => !path.split('/').some(p => !p || p === '.' || p === '..') &&
|
|
17
18
|
(/^(?:\.claude\/(?:settings\.json|CLAUDE\.md)|\.codex\/(?:config\.toml|AGENTS\.md))$/.test(path) ||
|
|
18
|
-
/^\.claude\/(?:skills|commands|agents|output-styles|plugins)\/.+/.test(path) || /^\.codex\/(?:prompts|skills)\/.+/.test(path)
|
|
19
|
+
/^\.claude\/(?:skills|commands|agents|output-styles|plugins)\/.+/.test(path) || /^\.codex\/(?:prompts|skills)\/.+/.test(path) ||
|
|
20
|
+
/^\.claude\/projects\/[^/]+\/memory\/.+/.test(path));
|
|
19
21
|
const key = (ctx, id) => `${ctx.apiUrl}:${id}`;
|
|
20
22
|
export function configManifests(ctx) {
|
|
21
23
|
const value = object(new AgentStateStore(ctx.store.dir).read().agent_config);
|
|
@@ -285,33 +287,51 @@ async function installPlugins(cell, plan, issues, signal) {
|
|
|
285
287
|
if (line.startsWith('failed '))
|
|
286
288
|
issues.push({ item: `plugin:${line.slice(7)}`, reason: 'could not be installed in the VM' });
|
|
287
289
|
}
|
|
288
|
-
|
|
290
|
+
/**
|
|
291
|
+
* `memoryOnly` (0.20.0+): just the project memory, for an account whose saved machine carries the rest. A conversation
|
|
292
|
+
* VM gets this computer's memory for its project, under the VM project folder's name (Claude keys memory by path).
|
|
293
|
+
*/
|
|
294
|
+
export async function syncAgentConfig(ctx, ws, opts = {}) {
|
|
289
295
|
const home = ctx.io.env.HOME || homedir();
|
|
290
296
|
const store = new AgentStateStore(ctx.store.dir);
|
|
291
297
|
const previous = configManifests(ctx)[key(ctx, ws.id)];
|
|
292
298
|
const issues = [];
|
|
293
|
-
const files = await collectUserFiles(home, issues);
|
|
294
|
-
const codexBytes = await readOptional(join(home, '.codex/config.toml'));
|
|
299
|
+
const files = opts.memoryOnly ? new Map() : await collectUserFiles(home, issues);
|
|
300
|
+
const codexBytes = opts.memoryOnly ? undefined : await readOptional(join(home, '.codex/config.toml'));
|
|
295
301
|
// One file that does not parse is skipped with a reason; it never stops the rest of the sync.
|
|
296
302
|
let claude;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
303
|
+
if (!opts.memoryOnly) {
|
|
304
|
+
try {
|
|
305
|
+
claude = await readObject(join(home, '.claude.json'));
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (!parseFailure(error))
|
|
309
|
+
throw error;
|
|
310
|
+
issues.push({ item: '.claude.json', reason: 'not valid JSON; its MCP servers were not copied' });
|
|
311
|
+
}
|
|
304
312
|
}
|
|
305
|
-
const plugins = await collectPlugins(home, issues);
|
|
313
|
+
const plugins = opts.memoryOnly ? { marketplaces: [], plugins: [] } : await collectPlugins(home, issues);
|
|
306
314
|
const hasPlugins = plugins.marketplaces.length > 0 || plugins.plugins.length > 0;
|
|
315
|
+
const records = Object.values(localConversations(ctx));
|
|
316
|
+
const conversation = records.find(r => r.conversation.id === ws.labels.shard_conversation) ?? records.find(r => r.conversation.workspace_id === ws.id);
|
|
317
|
+
const memory = conversation ? await collectMemory(home, conversation.folder, issues) : undefined;
|
|
307
318
|
const empty = { workspace_id: ws.id, workspace_key: ws.key, copied: [], removed: [], skipped: issues, mcp: [], synced_at: null };
|
|
308
|
-
if (!files.size && !codexBytes && !hasPlugins && !Object.keys(object(claude?.mcpServers)).length && !previous)
|
|
319
|
+
if (!files.size && !memory?.files.size && !codexBytes && !hasPlugins && !Object.keys(object(claude?.mcpServers)).length && !previous)
|
|
309
320
|
return empty;
|
|
310
321
|
const cell = ws.cell({ agentLabel: ctx.agentLabel, transitionTimeoutMs: ctx.wakeTimeoutMs });
|
|
311
|
-
|
|
312
|
-
const
|
|
322
|
+
// The VM home, and the project folder its Claude keys memory by (the main checkout of the conversation's worktree).
|
|
323
|
+
const run = await cell.exec.run(['sh', '-c', 'printf "%s\\n" "$HOME"; [ -n "$1" ] && git -C "$1" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true', 'shard-config', conversation?.conversation.worktree_path ?? ''], { signal: ctx.signal, timeoutMs: 5000, maxOutputBytes: 4096 });
|
|
324
|
+
const [remoteHome = '', common = ''] = run.stdout.trim().split('\n').map(line => line.trim());
|
|
313
325
|
if (run.exitCode !== 0 || !/^\/(?:[a-zA-Z0-9._-]+\/)*[a-zA-Z0-9._-]+$/.test(remoteHome) || remoteHome.split('/').some(p => p === '..' || p === '.'))
|
|
314
326
|
throw new Error('agent config could not resolve the VM user home');
|
|
327
|
+
if (memory && conversation) {
|
|
328
|
+
const vmRoot = common.endsWith('/.git') ? common.slice(0, -5) : conversation.conversation.worktree_path;
|
|
329
|
+
for (const [path, file] of memory.files) {
|
|
330
|
+
const dest = vmMemoryPath(path, memory.root, vmRoot);
|
|
331
|
+
if (dest)
|
|
332
|
+
files.set(dest, file);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
315
335
|
// Claude settings stay the user's. sf-agent hooks are in /etc/claude-code/managed-settings.json.
|
|
316
336
|
const secrets = { ...previous?.secrets, ...object(object(store.read().agent_config_secrets)[key(ctx, ws.id)]) };
|
|
317
337
|
const commandCache = new Map();
|
|
@@ -432,8 +452,10 @@ export async function syncAgentConfig(ctx, ws) {
|
|
|
432
452
|
sync_state: 'synced', synced_at, files: applied.files, claude_mcp: Object.keys(claudeMcp), claude_mcp_owned: partial.claude_mcp_owned, secrets, skipped: issues, mcp, tunnels,
|
|
433
453
|
...(pluginsHash ? { plugins_hash: pluginsHash } : {}) };
|
|
434
454
|
manifest.remote_hash = digest(JSON.stringify({ files: applied.files, claude_mcp: claudeMcp, mcp, tunnels }));
|
|
435
|
-
|
|
436
|
-
|
|
455
|
+
// The marker names each file's hash (0.20.0+): the guest tells this copy's writes from the agent's own changes.
|
|
456
|
+
if (marker?.workspace_id !== ws.id || marker.manifest_hash !== manifest.remote_hash || !marker.files)
|
|
457
|
+
await cell.files.write(`${remoteHome}/${markerPath}`, JSON.stringify({ workspace_id: ws.id, manifest_hash: manifest.remote_hash,
|
|
458
|
+
files: Object.fromEntries(Object.entries(applied.files).map(([path, file]) => [path, file.hash])) }) + '\n', { createParents: true, mode: '0600' });
|
|
437
459
|
store.update(doc => { const all = object(doc.agent_config); all[key(ctx, ws.id)] = manifest; doc.agent_config = all; });
|
|
438
460
|
const result = { ...empty, copied: applied.copied, removed: applied.removed, skipped: issues, mcp, synced_at };
|
|
439
461
|
if (!ctx.json) {
|
|
@@ -451,13 +473,13 @@ export async function syncAgentConfig(ctx, ws) {
|
|
|
451
473
|
* output in agent-config-sync.log of the config directory), so the command that started the conversation reports the
|
|
452
474
|
* start at once; without a runtime (tests, an embedding) it runs here and the returned promise settles when it ends.
|
|
453
475
|
*/
|
|
454
|
-
export function syncAgentConfigAfterStart(ctx, ws) {
|
|
476
|
+
export function syncAgentConfigAfterStart(ctx, ws, opts = {}) {
|
|
455
477
|
if (ctx.runtime?.script) {
|
|
456
478
|
try {
|
|
457
479
|
mkdirSync(ctx.store.dir, { recursive: true, mode: 0o700 });
|
|
458
480
|
const log = openSync(join(ctx.store.dir, 'agent-config-sync.log'), 'a', 0o600);
|
|
459
481
|
try {
|
|
460
|
-
const child = spawn(ctx.runtime.node, [ctx.runtime.script, 'agents', 'config-sync', ws.id], {
|
|
482
|
+
const child = spawn(ctx.runtime.node, [ctx.runtime.script, 'agents', 'config-sync', ws.id, ...(opts.memoryOnly ? ['--memory-only'] : [])], {
|
|
461
483
|
detached: true, stdio: ['ignore', log, log],
|
|
462
484
|
env: { ...process.env, ...ctx.io.env, SHARDFLUX_CONFIG_DIR: ctx.store.dir, SHARDFLUX_API_URL: ctx.apiUrl, SHARDFLUX_NO_UPDATE_CHECK: '1' }
|
|
463
485
|
});
|
|
@@ -471,7 +493,7 @@ export function syncAgentConfigAfterStart(ctx, ws) {
|
|
|
471
493
|
}
|
|
472
494
|
catch { /* runs in this process below */ }
|
|
473
495
|
}
|
|
474
|
-
return syncAgentConfig(ctx, ws).then(() => undefined, (error) => {
|
|
496
|
+
return syncAgentConfig(ctx, ws, opts).then(() => undefined, (error) => {
|
|
475
497
|
ctx.io.stderr.write(`Agent config not copied to ${ws.key}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
476
498
|
});
|
|
477
499
|
}
|
|
@@ -501,41 +523,13 @@ export async function startAgentConfigTunnels(ctx, ws) {
|
|
|
501
523
|
}
|
|
502
524
|
export const AGENT_CONFIG_HANDLERS = {
|
|
503
525
|
/** The detached copy syncAgentConfigAfterStart starts; one line per run in agent-config-sync.log. */
|
|
504
|
-
async 'agents config-sync'(ctx,
|
|
526
|
+
async 'agents config-sync'(ctx, values, [id]) {
|
|
505
527
|
const ws = await ctx.cloud().workspaces.get(id);
|
|
506
528
|
const started = Date.now();
|
|
507
|
-
const result = await syncAgentConfig({ ...ctx, json: true }, ws);
|
|
529
|
+
const result = await syncAgentConfig({ ...ctx, json: true }, ws, { memoryOnly: values['memory-only'] === true });
|
|
508
530
|
ctx.io.stderr.write(`${new Date().toISOString()} ${ws.key}: ${result.copied.length} copied, ${result.removed.length} removed, ${result.skipped.length} skipped in ${Date.now() - started} ms\n`);
|
|
509
531
|
for (const line of configIssueLines(result.skipped))
|
|
510
532
|
ctx.io.stderr.write(` ${line}\n`);
|
|
511
533
|
return 0;
|
|
512
534
|
},
|
|
513
|
-
'agents config status'(ctx, values) {
|
|
514
|
-
const workspace = typeof values.workspace === 'string' ? values.workspace : undefined;
|
|
515
|
-
const manifests = Object.values(configManifests(ctx)).filter(m => m.api_url === ctx.apiUrl && (!workspace || m.workspace_id === workspace || m.workspace_key === workspace));
|
|
516
|
-
const report = { observation: 'last sync on this machine', workspaces: manifests };
|
|
517
|
-
ctx.io.stdout.write(ctx.json ? JSON.stringify(report, null, 2) + '\n' : manifests.length ? manifests.map(m => `${m.workspace_key}: ${Object.keys(m.files).length} files; ${m.sync_state === 'synced' ? `last sync ${m.synced_at}` : 'sync interrupted; run config sync again'}\n` +
|
|
518
|
-
m.skipped.map(s => ` skipped ${s.item}: ${s.reason}\n`).join('') +
|
|
519
|
-
m.mcp.map(s => ` MCP ${s.app}/${s.name}: ${s.status}${s.reason ? ` (${s.reason})` : ''}\n`).join('')).join('') : 'No agent config has been synced on this machine.\n');
|
|
520
|
-
return Promise.resolve(0);
|
|
521
|
-
},
|
|
522
|
-
async 'agents config sync'(ctx, values) {
|
|
523
|
-
const workspace = typeof values.workspace === 'string' ? values.workspace : undefined;
|
|
524
|
-
const cloud = ctx.cloud();
|
|
525
|
-
const targets = [];
|
|
526
|
-
if (workspace) {
|
|
527
|
-
const ws = await cloud.workspaces.findByKey(workspace, { includeDeleted: false, signal: ctx.signal });
|
|
528
|
-
targets.push(ws ?? await cloud.workspaces.get(workspace));
|
|
529
|
-
}
|
|
530
|
-
else
|
|
531
|
-
for await (const ws of cloud.workspaces.listAll()) {
|
|
532
|
-
if (ws.labels.shard_project && ws.mode === 'processful')
|
|
533
|
-
targets.push(ws);
|
|
534
|
-
}
|
|
535
|
-
const results = [];
|
|
536
|
-
for (const ws of targets)
|
|
537
|
-
results.push(await syncAgentConfig(ctx, ws));
|
|
538
|
-
ctx.io.stdout.write(ctx.json ? JSON.stringify({ workspaces: results }, null, 2) + '\n' : results.length ? results.map(r => r.synced_at === null ? `${r.workspace_key}: no local agent config found\n` : `${r.workspace_key}: ${r.copied.length} files copied, ${r.removed.length} removed\n`).join('') : 'No agent workspaces found.\n');
|
|
539
|
-
return 0;
|
|
540
|
-
},
|
|
541
535
|
};
|
package/dist/agent-setup.js
CHANGED
|
@@ -54,8 +54,8 @@ export function setupGuide(state) {
|
|
|
54
54
|
{
|
|
55
55
|
step: 'Build the machine',
|
|
56
56
|
ask: 'What should every cloud agent start with? Languages and runtimes, CLIs and tools, dotfiles and shell, secrets and credentials, editor and agent config.',
|
|
57
|
-
commands: ['shard machine open --json', "shard ws exec <key> -- bash -lc '<install commands>'", 'shard files write <key> <path> --from -', 'shard machine save --wait --json'],
|
|
58
|
-
note: 'machine open prints the machine VM’s workspace key. Install with ws exec, write dotfiles and secret files with files write (contents on standard input, never in arguments), then save: every new conversation starts from it.',
|
|
57
|
+
commands: ['shard machine setup --from-this-computer --json', 'shard machine open --json', "shard ws exec <key> -- bash -lc '<install commands>'", 'shard files write <key> <path> --from -', 'shard machine save --wait --json'],
|
|
58
|
+
note: 'machine setup recreates this computer’s tools, runtimes, shell, git identity and dotfiles on the machine with this computer’s Claude Code or Codex, and saves it (0.20.0+). For the rest: machine open prints the machine VM’s workspace key. Install with ws exec, write dotfiles and secret files with files write (contents on standard input, never in arguments), then save: every new conversation starts from it.',
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
step: 'Set up the project',
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -13,7 +13,7 @@ import { parseArgs } from 'node:util';
|
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
15
|
import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
|
|
16
|
-
export const CLI_VERSION = '0.
|
|
16
|
+
export const CLI_VERSION = '0.20.0';
|
|
17
17
|
export const GLOBAL_OPTIONS = {
|
|
18
18
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
19
19
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -176,6 +176,15 @@ const KEY_COMMANDS = [
|
|
|
176
176
|
{ path: ['machine', 'open'], summary: 'Create or resume your machine VM without attaching to it, and print its workspace key (0.17.1+)', positionals: [], options: {},
|
|
177
177
|
description: 'For an agent building your machine from a terminal it does not attach to: install with "shard ws exec <key> -- bash -lc ...", write dotfiles and secret files with "shard files write <key> <path> --from -" (contents on standard input), then "shard machine save --wait". The first open copies your Claude and Codex settings into it, as "shard machine" does.',
|
|
178
178
|
examples: ['shard machine open --json', "shard ws exec \"$(shard machine open)\" -- bash -lc 'sudo apt-get install -y ripgrep'", 'shard machine save --wait --json'] },
|
|
179
|
+
{ path: ['machine', 'setup'], summary: 'Let your own Claude Code or Codex set up your machine from this computer (0.20.0+)', positionals: [], options: {
|
|
180
|
+
'from-this-computer': { type: 'boolean', description: 'Recreate this computer’s development setup on your machine: its CLIs and tools, language runtimes, shell config and aliases, git identity and dotfiles. Required.' },
|
|
181
|
+
'if-needed': { type: 'boolean', description: 'Only when your machine was never set up from this computer (what the apps run after onboarding).' },
|
|
182
|
+
status: { type: 'boolean', description: 'Show the last setup from this computer instead of running one.' },
|
|
183
|
+
agent: { type: 'string', value: '<claude|codex|cloud>', description: 'Which agent sets it up (default: this computer’s Claude Code, else its Codex, else your cloud Claude in the machine VM).' },
|
|
184
|
+
timeout: { type: 'string', value: '<duration>', description: 'Time box for the agent (default 10m).' },
|
|
185
|
+
},
|
|
186
|
+
description: 'Your own coding agent does it, on your subscription, with no question asked: shard starts this computer’s Claude Code (claude -p) or Codex (codex exec) with a fixed task. On this computer it may only read (no edits, no ~/.ssh, keychain, token or credential files) and run shard on your machine VM; files go over standard input. Without Claude Code or Codex here, your cloud Claude does it inside the machine VM from a list of this computer’s tools and versions. Then shard copies your Claude and Codex config and saves the machine (shard machine save --wait). Progress goes to stderr, one line per step.',
|
|
187
|
+
examples: ['shard machine setup --from-this-computer', 'shard machine setup --from-this-computer --if-needed --json', 'shard machine setup --status --json'] },
|
|
179
188
|
{ path: ['agents', 'list'], summary: 'List coding agent sessions.', positionals: [], options: {
|
|
180
189
|
...PAGE,
|
|
181
190
|
state: { type: 'string', value: '<working|waiting|exited>', description: 'Agent state filter.' },
|
|
@@ -1299,7 +1308,7 @@ const AGENT_COMMANDS = [
|
|
|
1299
1308
|
} },
|
|
1300
1309
|
{ path: ['agents', 'fork'], summary: 'Fork an agent’s memory and disk', positionals: [{ name: 'agent', required: true }, { name: 'name', required: true }], options: WAIT },
|
|
1301
1310
|
{ path: ['agents', 'park'], summary: 'Suspend an agent when idle', positionals: [{ name: 'agent', required: true }], options: { 'when-idle': { type: 'string', value: '<duration>', description: 'Idle grace before suspend (default 1s).' } } },
|
|
1302
|
-
{ path: ['agents', 'config-sync'], hidden: true, summary: 'Copy this computer’s Claude and Codex config into a conversation VM (started in the background after a start, 0.19.0+)', positionals: [{ name: 'workspace', required: true }], options: {} },
|
|
1311
|
+
{ path: ['agents', 'config-sync'], hidden: true, summary: 'Copy this computer’s Claude and Codex config into a conversation VM (started in the background after a start, 0.19.0+)', positionals: [{ name: 'workspace', required: true }], options: { 'memory-only': { type: 'boolean', description: 'Only the project memory (an account with a saved machine, 0.20.0+).' } } },
|
|
1303
1312
|
{ path: ['agents', 'logs'], summary: 'Tail an agent’s terminal from its output offset: a conversation id (0.18.1+) or an agent workspace key', positionals: [{ name: 'agent', required: true }], options: {
|
|
1304
1313
|
app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice; a conversation uses its own).' },
|
|
1305
1314
|
offset: { type: 'string', value: '<bytes>', description: 'Read from this output byte offset (default last 64 KiB).' },
|
|
@@ -24,6 +24,8 @@ import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-comma
|
|
|
24
24
|
import { openUrl } from "./browser.js";
|
|
25
25
|
import { out } from "./commands.js";
|
|
26
26
|
import { progress } from "./progress.js";
|
|
27
|
+
/** This computer's agent config, copied after the start; with a saved machine (which carries it) just the project memory. */
|
|
28
|
+
const afterStartConfig = (ctx, ws, machine) => () => syncAgentConfigAfterStart(ctx, ws, machine ? { memoryOnly: true } : {});
|
|
27
29
|
/** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
|
|
28
30
|
export async function conversationTerminal(ctx, ws, c, project, values, afterStart) {
|
|
29
31
|
if (c.route === 'codex-cloud')
|
|
@@ -48,7 +50,7 @@ export async function reopenConversation(ctx, c, values) {
|
|
|
48
50
|
progress(ctx, 'Restored');
|
|
49
51
|
return { conversation: restored, ws };
|
|
50
52
|
}
|
|
51
|
-
const afterStart = (await savedMachine(ctx))?.template
|
|
53
|
+
const afterStart = afterStartConfig(ctx, ws, !!(await savedMachine(ctx))?.template);
|
|
52
54
|
const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
|
|
53
55
|
await conversationTerminal(quiet, ws, restored, ws.labels.repo_remote || ws.labels.shard_project || ws.key, { ...values, detach: true }, afterStart);
|
|
54
56
|
return { conversation: restored, ws };
|
|
@@ -148,7 +150,7 @@ async function startConversation(ctx, values, [path = './', explicit], openIn) {
|
|
|
148
150
|
if (!signedIn)
|
|
149
151
|
await loginClaudeInWorkspace(ctx, ws);
|
|
150
152
|
// Without a saved machine the laptop's Claude and Codex config is copied in, after the agent has its first prompt.
|
|
151
|
-
const afterStart = (await savedMachine(ctx))?.template
|
|
153
|
+
const afterStart = afterStartConfig(ctx, ws, !!(await savedMachine(ctx))?.template);
|
|
152
154
|
await ensureMenubar(ctx.store.dir, ctx.apiUrl, new MenubarManager({ home: ctx.io.env.HOME, signal: ctx.signal }));
|
|
153
155
|
if (openIn) {
|
|
154
156
|
// The agent starts detached in its VM; the conversation then opens in Claude Desktop or claude.ai (Remote Control).
|
package/dist/conversations.d.ts
CHANGED
|
@@ -49,6 +49,10 @@ export interface Conversation {
|
|
|
49
49
|
} | null;
|
|
50
50
|
/** Why it needs you, while `state` is needs_you; null otherwise (0.18.0+, APIs that return it). */
|
|
51
51
|
question?: string | null;
|
|
52
|
+
/** The user who started it (0.20.0+, APIs that record it); null when unknown. */
|
|
53
|
+
started_by?: {
|
|
54
|
+
user_id: string;
|
|
55
|
+
} | null;
|
|
52
56
|
}
|
|
53
57
|
export interface LocalConversation {
|
|
54
58
|
api_url: string;
|
package/dist/device-helper.js
CHANGED
|
@@ -12,6 +12,7 @@ import { writePrivateFile } from "./credentials.js";
|
|
|
12
12
|
import { conversationClient } from "./conversation-commands.js";
|
|
13
13
|
import { Readable } from 'node:stream';
|
|
14
14
|
import { runWorktreeTwin } from "./worktree-twin-engine.js";
|
|
15
|
+
import { AGENT_CONFIG_POLL_MS, AgentConfigReturnState, returnAgentConfigsOnce } from "./agent-config-return.js";
|
|
15
16
|
import { askLocalDeviceApproval } from "./device-approvals.js";
|
|
16
17
|
/** The menu bar owns this helper and its stdin; VM data can request actions but cannot answer approvals. */
|
|
17
18
|
export async function runDeviceHelper(ctx, options = {}) {
|
|
@@ -55,7 +56,7 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
55
56
|
ctx.io.stdout.write(JSON.stringify({ type: 'approval_resolved', request_id: approval.request_id }) + '\n'); });
|
|
56
57
|
};
|
|
57
58
|
const working = new Map();
|
|
58
|
-
const mirrored = new ConversationMirrorState(), files = new FilesMirrorState();
|
|
59
|
+
const mirrored = new ConversationMirrorState(), files = new FilesMirrorState(), config = new AgentConfigReturnState();
|
|
59
60
|
const deviceFile = join(ctx.store.dir, 'device-id');
|
|
60
61
|
let deviceId;
|
|
61
62
|
try {
|
|
@@ -67,11 +68,35 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
67
68
|
}
|
|
68
69
|
const executor = new DeviceExecutor(ctx, ask, deviceId);
|
|
69
70
|
const report = (e) => { ctx.io.stderr.write(`Device bridge: ${e instanceof Error ? e.message : String(e)}\n`); };
|
|
70
|
-
let mirroring, twin;
|
|
71
|
+
let mirroring, twin, returning;
|
|
71
72
|
try {
|
|
72
73
|
return await withTargetLock(join(ctx.store.dir, 'device-helper-lock'), async () => {
|
|
73
74
|
// Finished conversations land in their local branches (worktree-twin-engine.ts); its own loop and backoff.
|
|
74
75
|
twin = runWorktreeTwin(ctx, deviceId, signal).catch(report);
|
|
76
|
+
// Skills, memories and CLAUDE.md lines saved in the cloud come back on their own cadence (agent-config-return.ts):
|
|
77
|
+
// never behind a files-mirror pass, which takes minutes for an account with hundreds of conversations.
|
|
78
|
+
returning = (async () => {
|
|
79
|
+
let failures = 0;
|
|
80
|
+
const reported = new Set();
|
|
81
|
+
while (!signal.aborted) {
|
|
82
|
+
let wait = AGENT_CONFIG_POLL_MS;
|
|
83
|
+
try {
|
|
84
|
+
await returnAgentConfigsOnce(ctx, config);
|
|
85
|
+
failures = 0;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
if (signal.aborted)
|
|
89
|
+
break;
|
|
90
|
+
wait = Math.min(300_000, 5000 * 2 ** Math.min(failures++, 6));
|
|
91
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
92
|
+
if (!conversationsUnavailable(e) && !reported.has(message)) {
|
|
93
|
+
reported.add(message);
|
|
94
|
+
ctx.io.stderr.write(`Agent config: ${message} (retrying in ${wait / 1000}s).\n`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await delay(wait, undefined, { signal }).catch(() => undefined);
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
75
100
|
// A slow or failed transcript copy must not delay heartbeats, request polling or approvals.
|
|
76
101
|
mirroring = (async () => {
|
|
77
102
|
let failures = 0;
|
|
@@ -133,6 +158,8 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
133
158
|
await executor.close();
|
|
134
159
|
await Promise.all([...working.values()]);
|
|
135
160
|
await mirroring;
|
|
161
|
+
await returning;
|
|
162
|
+
await config.folding;
|
|
136
163
|
await twin;
|
|
137
164
|
await input.catch(() => undefined);
|
|
138
165
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -62,6 +62,13 @@ export declare class AuthConfigError extends HintedError {
|
|
|
62
62
|
export declare class NotFoundError extends HintedError {
|
|
63
63
|
name: string;
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* `shard machine setup --from-this-computer` (0.20.0+) ran but did not save a machine: the agent failed, ran past its
|
|
67
|
+
* time box, or the save failed. Retryable: the same command runs it again (exit 6).
|
|
68
|
+
*/
|
|
69
|
+
export declare class MachineSetupError extends HintedError {
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
65
72
|
/** `auth login` of a two-factor account without a code: the pending sign-in is saved; `shard auth mfa` completes it (exit 4). */
|
|
66
73
|
export declare class MfaPendingError extends HintedError {
|
|
67
74
|
name: string;
|
package/dist/errors.js
CHANGED
|
@@ -87,6 +87,13 @@ export class AuthConfigError extends HintedError {
|
|
|
87
87
|
export class NotFoundError extends HintedError {
|
|
88
88
|
name = 'NotFoundError';
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* `shard machine setup --from-this-computer` (0.20.0+) ran but did not save a machine: the agent failed, ran past its
|
|
92
|
+
* time box, or the save failed. Retryable: the same command runs it again (exit 6).
|
|
93
|
+
*/
|
|
94
|
+
export class MachineSetupError extends HintedError {
|
|
95
|
+
name = 'MachineSetupError';
|
|
96
|
+
}
|
|
90
97
|
/** `auth login` of a two-factor account without a code: the pending sign-in is saved; `shard auth mfa` completes it (exit 4). */
|
|
91
98
|
export class MfaPendingError extends HintedError {
|
|
92
99
|
name = 'MfaPendingError';
|
|
@@ -265,6 +272,8 @@ export function describeError(err) {
|
|
|
265
272
|
return { code: 'mfa_required', message: err.message };
|
|
266
273
|
if (err instanceof NotFoundError)
|
|
267
274
|
return { code: 'not_found', message: err.message };
|
|
275
|
+
if (err instanceof MachineSetupError)
|
|
276
|
+
return { code: 'machine_setup_failed', message: err.message, retryable: true };
|
|
268
277
|
if (err instanceof InterruptedError) {
|
|
269
278
|
return { code: 'interrupted', message: err.message, ...(err.operationId ? { operation_id: err.operationId } : {}), ...(err.executionId ? { execution_id: err.executionId } : {}) };
|
|
270
279
|
}
|
|
@@ -294,6 +303,8 @@ export function exitCodeFor(err, interrupted = false) {
|
|
|
294
303
|
return EXIT.operationFailed;
|
|
295
304
|
if (err instanceof NotFoundError)
|
|
296
305
|
return EXIT.notFound;
|
|
306
|
+
if (err instanceof MachineSetupError)
|
|
307
|
+
return EXIT.operationFailed;
|
|
297
308
|
if (err instanceof OperationTimeoutError || err instanceof CheckoutTimeoutError || isNamed(err, 'TimeoutError'))
|
|
298
309
|
return EXIT.timeout;
|
|
299
310
|
if (err instanceof OperationFailedError || err instanceof DurabilityLostError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
|
|
@@ -4,6 +4,18 @@ import type { Ctx, Handler } from './commands.js';
|
|
|
4
4
|
export declare function filesystemLine(state: AgentFilesystem & {
|
|
5
5
|
cache_scope?: string;
|
|
6
6
|
}): string;
|
|
7
|
+
/** A build's place while it waits for one of the plan's build slots (API: TemplateBuild.queue). */
|
|
8
|
+
export interface BuildQueue {
|
|
9
|
+
position: number;
|
|
10
|
+
slots: number;
|
|
11
|
+
expected_wait_seconds: number;
|
|
12
|
+
}
|
|
13
|
+
/** "about 40 s" / "about 3 min": the queue's estimate, rounded the way people say it. */
|
|
14
|
+
export declare function aboutWait(seconds: number): string;
|
|
15
|
+
/** One line for a build that waits for a slot (0.20.0+): "Queued behind 3 builds · about 40 s" (running ones + those ahead). */
|
|
16
|
+
export declare function queueLine(q: BuildQueue): string;
|
|
17
|
+
/** How often a waited save reads its build's place in the queue. */
|
|
18
|
+
export declare const QUEUE_POLL_MS = 2500;
|
|
7
19
|
/** 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
20
|
export declare function movedLine(state: AgentFilesystem, level: 'machine' | 'template'): string;
|
|
9
21
|
/** Creates or resumes the machine VM of the saved machine version; the first one gets this computer's agent config. */
|
|
@@ -29,17 +29,79 @@ function output(ctx, state) {
|
|
|
29
29
|
ctx.io.stdout.write(ctx.json ? JSON.stringify(state) + '\n' : `${filesystemLine(state)}\n`);
|
|
30
30
|
return 0;
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
32
|
+
/** "about 40 s" / "about 3 min": the queue's estimate, rounded the way people say it. */
|
|
33
|
+
export function aboutWait(seconds) {
|
|
34
|
+
if (seconds < 90)
|
|
35
|
+
return `about ${Math.max(10, Math.round(seconds / 10) * 10)} s`;
|
|
36
|
+
return `about ${Math.round(seconds / 60)} min`;
|
|
37
|
+
}
|
|
38
|
+
/** One line for a build that waits for a slot (0.20.0+): "Queued behind 3 builds · about 40 s" (running ones + those ahead). */
|
|
39
|
+
export function queueLine(q) {
|
|
40
|
+
const behind = q.slots + q.position - 1;
|
|
41
|
+
return `Queued behind ${behind} build${behind === 1 ? '' : 's'} · ${aboutWait(q.expected_wait_seconds)}`;
|
|
42
|
+
}
|
|
43
|
+
/** How often a waited save reads its build's place in the queue. */
|
|
44
|
+
export const QUEUE_POLL_MS = 2_500;
|
|
45
|
+
/** The pending build's queue, if it waits for a slot; nothing when the API predates it or the build cannot be read. */
|
|
46
|
+
async function pendingQueue(ctx, state) {
|
|
47
|
+
if (!state.pending)
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const build = await ctx.cloud().templates.builds.get(state.organization_id, state.pending.build_id);
|
|
51
|
+
return build.queue ?? undefined;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Save output: one line naming the version being saved (and its place in the queue when the plan's build slots are in
|
|
59
|
+
* use, 0.20.0+), or with --wait the saved version (progress on stderr, the queue line updating while it waits).
|
|
60
|
+
*/
|
|
33
61
|
async function saved(ctx, values, save, level) {
|
|
34
62
|
if (values.wait !== true && values.timeout !== undefined)
|
|
35
63
|
throw new UsageError('--timeout needs --wait');
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
const stop = new AbortController();
|
|
65
|
+
let watcher;
|
|
66
|
+
// While the save waits, its build's place in the queue: one line each time it changes.
|
|
67
|
+
const watchQueue = (state) => {
|
|
68
|
+
if (watcher || !state.pending)
|
|
69
|
+
return;
|
|
70
|
+
watcher = (async () => {
|
|
71
|
+
let last = '';
|
|
72
|
+
while (!stop.signal.aborted) {
|
|
73
|
+
const q = await pendingQueue(ctx, state);
|
|
74
|
+
const line = q ? queueLine(q) : '';
|
|
75
|
+
if (line && line !== last && !stop.signal.aborted)
|
|
76
|
+
progress(ctx, line);
|
|
77
|
+
last = line;
|
|
78
|
+
await new Promise(resolve => {
|
|
79
|
+
const timer = setTimeout(resolve, QUEUE_POLL_MS);
|
|
80
|
+
stop.signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
};
|
|
85
|
+
const wait = values.wait === true ? { timeoutMs: waitTimeout(values, 600_000), onChange: (state) => {
|
|
86
|
+
if (state.state === 'building' && state.pending) {
|
|
87
|
+
progress(ctx, `Saving ${level} version ${state.pending.version}…`);
|
|
88
|
+
watchQueue(state);
|
|
89
|
+
}
|
|
90
|
+
} } : undefined;
|
|
91
|
+
let state;
|
|
92
|
+
try {
|
|
93
|
+
state = await save({ signal: ctx.signal, ...(wait ? { wait } : {}) });
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
stop.abort();
|
|
97
|
+
await watcher;
|
|
98
|
+
}
|
|
99
|
+
const queue = wait ? undefined : await pendingQueue(ctx, state);
|
|
39
100
|
if (ctx.json)
|
|
40
|
-
return output(ctx, state);
|
|
101
|
+
return output(ctx, queue ? { ...state, queue } : state);
|
|
41
102
|
const moved = movedLine(state, level);
|
|
42
|
-
|
|
103
|
+
const queued = queue ? `; ${queueLine(queue).replace(/^Q/, 'q')}` : '';
|
|
104
|
+
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
105
|
return 0;
|
|
44
106
|
}
|
|
45
107
|
/** 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. */
|
package/dist/handlers.js
CHANGED
|
@@ -23,4 +23,5 @@ 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
|
-
|
|
26
|
+
import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
|
|
27
|
+
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, ...MACHINE_SETUP_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS };
|
package/dist/initial-prompt.d.ts
CHANGED
|
@@ -22,5 +22,18 @@ export declare function readInitialImages(ctx: Ctx, values: Values, prompt: stri
|
|
|
22
22
|
export declare function promptWithImages(prompt: string, images: readonly InitialImage[]): string;
|
|
23
23
|
/** Writes the images into the conversation VM before the first message refers to them. */
|
|
24
24
|
export declare function uploadInitialImages(cell: Pick<CellClient, 'files'>, images: readonly InitialImage[], signal?: AbortSignal): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
export declare
|
|
25
|
+
/** The conversation inbox takes a message of at most this many characters (the API's text limit). */
|
|
26
|
+
export declare const INBOX_MAX_CHARS = 65536;
|
|
27
|
+
/**
|
|
28
|
+
* The harness's input box is the last thing that drew its prompt glyph (0.19.1+). Claude Code and Codex draw the same
|
|
29
|
+
* glyph as the selection cursor of their dialogs ("❯ 1. Yes, I trust this folder"); a prompt pasted there would pick
|
|
30
|
+
* options. The guest's own first-message delivery reads the screen the same way (coding 39+).
|
|
31
|
+
*/
|
|
32
|
+
export declare function inputBoxReady(tail: string, glyph: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay.
|
|
35
|
+
* When nothing was typed yet and the terminal never became ready, `queue` hands the prompt to the conversation's guest
|
|
36
|
+
* (its inbox, which waits for the harness and retries Enter) and the result is `queued` (0.19.1+): a first prompt is
|
|
37
|
+
* never lost to a slow start.
|
|
38
|
+
*/
|
|
39
|
+
export declare function submitInitialPrompt(connect: (signal: AbortSignal) => Promise<TerminalConnection>, prompt: string, workspaceKey: string, signal: AbortSignal, app?: 'claude' | 'codex', write?: (bytes: Uint8Array) => Promise<unknown>, queue?: () => Promise<unknown>): Promise<'submitted' | 'queued'>;
|
package/dist/initial-prompt.js
CHANGED
|
@@ -137,8 +137,26 @@ export async function uploadInitialImages(cell, images, signal) {
|
|
|
137
137
|
await cell.files.write(image.path, image.data, { mode: '0600', createParents: true });
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
/**
|
|
141
|
-
export
|
|
140
|
+
/** The conversation inbox takes a message of at most this many characters (the API's text limit). */
|
|
141
|
+
export const INBOX_MAX_CHARS = 65536;
|
|
142
|
+
/**
|
|
143
|
+
* The harness's input box is the last thing that drew its prompt glyph (0.19.1+). Claude Code and Codex draw the same
|
|
144
|
+
* glyph as the selection cursor of their dialogs ("❯ 1. Yes, I trust this folder"); a prompt pasted there would pick
|
|
145
|
+
* options. The guest's own first-message delivery reads the screen the same way (coding 39+).
|
|
146
|
+
*/
|
|
147
|
+
export function inputBoxReady(tail, glyph) {
|
|
148
|
+
// eslint-disable-next-line no-control-regex -- Native terminal escapes are removed before reading the screen text.
|
|
149
|
+
const plain = tail.replace(/\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, ' ');
|
|
150
|
+
const at = plain.lastIndexOf(glyph);
|
|
151
|
+
return at >= 0 && !/^[\s\u00a0]*\d+\./.test(plain.slice(at + glyph.length));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay.
|
|
155
|
+
* When nothing was typed yet and the terminal never became ready, `queue` hands the prompt to the conversation's guest
|
|
156
|
+
* (its inbox, which waits for the harness and retries Enter) and the result is `queued` (0.19.1+): a first prompt is
|
|
157
|
+
* never lost to a slow start.
|
|
158
|
+
*/
|
|
159
|
+
export async function submitInitialPrompt(connect, prompt, workspaceKey, signal, app = 'claude', write, queue) {
|
|
142
160
|
const bounded = AbortSignal.any([signal, AbortSignal.timeout(20000)]);
|
|
143
161
|
let connection;
|
|
144
162
|
let sent = false, pasted = false;
|
|
@@ -159,7 +177,7 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
|
|
|
159
177
|
// eslint-disable-next-line no-control-regex -- Strip native terminal ANSI before matching prompt echo.
|
|
160
178
|
const plain = tail.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/\s/g, '');
|
|
161
179
|
if (sent && bytes.length)
|
|
162
|
-
return;
|
|
180
|
+
return 'submitted';
|
|
163
181
|
if (pasted) {
|
|
164
182
|
// A long prompt overflows the input box: a 24-row terminal shows only its end, so either end confirms the paste.
|
|
165
183
|
const compact = prompt.replace(/\s/g, '');
|
|
@@ -174,7 +192,7 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
|
|
|
174
192
|
tail = '';
|
|
175
193
|
continue;
|
|
176
194
|
}
|
|
177
|
-
const ready = app === 'claude' ?
|
|
195
|
+
const ready = inputBoxReady(tail, app === 'claude' ? '❯' : '›');
|
|
178
196
|
if (!ready || !pasteEnabled)
|
|
179
197
|
continue;
|
|
180
198
|
const paste = Buffer.from(`\x1b[200~${prompt}\x1b[201~`);
|
|
@@ -188,6 +206,13 @@ export async function submitInitialPrompt(connect, prompt, workspaceKey, signal,
|
|
|
188
206
|
throw new Error('The agent terminal closed before accepting a prompt.');
|
|
189
207
|
}
|
|
190
208
|
catch {
|
|
209
|
+
if (!pasted && queue && !signal.aborted && prompt.length <= INBOX_MAX_CHARS) {
|
|
210
|
+
try {
|
|
211
|
+
await queue();
|
|
212
|
+
return 'queued';
|
|
213
|
+
}
|
|
214
|
+
catch { /* reported as not submitted below */ }
|
|
215
|
+
}
|
|
191
216
|
throw new CodedError('initial_prompt_failed', `Created ${workspaceKey}, but the initial prompt ${sent ? 'could not be confirmed' : 'was not submitted'}. Open that conversation before starting another.`, undefined, { workspace_key: workspaceKey, submitted: sent });
|
|
192
217
|
}
|
|
193
218
|
finally {
|