@shardflux/cli 0.20.0 → 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.
- package/CHANGELOG.md +214 -1
- package/README.md +215 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-login.d.ts +4 -3
- package/dist/acp-login.js +7 -5
- package/dist/acp-models.js +20 -15
- package/dist/acp.js +66 -13
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +86 -32
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.js +24 -13
- package/dist/args.d.ts +1 -1
- package/dist/args.js +94 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-forward.d.ts +2 -0
- package/dist/cli-proxy-forward.js +9 -2
- package/dist/cli-proxy-hub.d.ts +69 -25
- package/dist/cli-proxy-hub.js +231 -54
- package/dist/cli-proxy.d.ts +70 -1
- package/dist/cli-proxy.js +298 -32
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +5 -0
- package/dist/codex-cloud.js +88 -17
- package/dist/command-center-actions.js +5 -1
- package/dist/commands.js +171 -22
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +77 -13
- package/dist/conversations.d.ts +73 -1
- package/dist/conversations.js +257 -23
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +27 -1
- package/dist/errors.js +45 -5
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +108 -54
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +63 -0
- package/dist/move-worktrees.js +201 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +116 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +59 -27
- package/dist/prepared-repository.d.ts +202 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1306 -0
- package/dist/project-preparation.d.ts +48 -0
- package/dist/project-preparation.js +304 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +19 -4
- package/dist/project-sync.js +31 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.js +1 -4
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/terminal-apps.d.ts +157 -0
- package/dist/terminal-apps.js +561 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.js +11 -3
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { linuxTerminalSession } from "./linux-terminal.js";
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { attachable, openShardInTerminal, reportTerminalOpen } from "./terminal-apps.js";
|
|
2
4
|
import { randomUUID } from 'node:crypto';
|
|
3
5
|
import { AgentStateStore } from "./agent-state.js";
|
|
4
6
|
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
@@ -7,11 +9,12 @@ import { modelChoice } from "./agent-models.js";
|
|
|
7
9
|
import { terminal } from "./agent-commands.js";
|
|
8
10
|
import { cloudClaudeLogin, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
|
|
9
11
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
10
|
-
import { prepareProjectTemplate } from "./project-template-setup.js";
|
|
12
|
+
import { labelConversationSetup, prepareProjectTemplate, readConversationSetup } from "./project-template-setup.js";
|
|
11
13
|
import { projectWorkspacePath, seedAgentRepository } from "./agent-repository.js";
|
|
12
14
|
import { syncAgentConfig, syncAgentConfigAfterStart } from "./agent-config-sync.js";
|
|
13
15
|
import { savedMachine } from "./filesystem-state.js";
|
|
14
16
|
import { ensureMenubar, MenubarManager } from "./menubar.js";
|
|
17
|
+
import { ensureProjectGit } from "./project-git.js";
|
|
15
18
|
import { UsageError } from "./errors.js";
|
|
16
19
|
import { localResumeTerminal } from "./conversation-local-resume.js";
|
|
17
20
|
import { agentReachability } from "./agent-reachability.js";
|
|
@@ -24,16 +27,21 @@ import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-comma
|
|
|
24
27
|
import { openUrl } from "./browser.js";
|
|
25
28
|
import { out } from "./commands.js";
|
|
26
29
|
import { progress } from "./progress.js";
|
|
30
|
+
import { seedPhase } from "./seed-phases.js";
|
|
27
31
|
/** This computer's agent config, copied after the start; with a saved machine (which carries it) just the project memory. */
|
|
28
32
|
const afterStartConfig = (ctx, ws, machine) => () => syncAgentConfigAfterStart(ctx, ws, machine ? { memoryOnly: true } : {});
|
|
29
|
-
/**
|
|
30
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Every terminal entry point uses the durable conversation's native harness, worktree and session. A new conversation
|
|
35
|
+
* in a repository without a project template for its dependencies sets its worktree up once its agent has started
|
|
36
|
+
* (`setup`, 0.21.1+).
|
|
37
|
+
*/
|
|
38
|
+
export async function conversationTerminal(ctx, ws, c, project, values, afterStart, setup) {
|
|
31
39
|
if (c.route === 'codex-cloud')
|
|
32
40
|
return openCodexConversation(ctx, c);
|
|
33
41
|
await restoreConversationBinding(ctx, c, ws);
|
|
34
42
|
// A thread on another conversation's computer has its own PTY there, never the VM's own (0.16.0+).
|
|
35
43
|
const pty = ws.labels.shard_conversation && ws.labels.shard_conversation !== c.id ? conversationName(c) : undefined;
|
|
36
|
-
return terminal(ctx, ws, c.harness, conversationName(c), project, values,
|
|
44
|
+
return terminal(ctx, ws, c.harness, conversationName(c), project, values, setup?.hash, c.worktree_path, { id: c.id, session: c.harness_session_id, title: c.title, ...(pty ? { pty } : {}), choice: recordedChoice(c), ...(afterStart ? { afterStart } : {}), ...(setup ? { setupLabelled: setup.labelled } : {}) });
|
|
37
45
|
}
|
|
38
46
|
/**
|
|
39
47
|
* A conversation and its VM, for an app to open (Claude Desktop, claude.ai, T3 Code, the menu bar). A conversation
|
|
@@ -58,20 +66,58 @@ export async function reopenConversation(ctx, c, values) {
|
|
|
58
66
|
/**
|
|
59
67
|
* `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
|
|
60
68
|
* Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
|
|
69
|
+
* From a shell nobody types into (a coding agent's, or none) a terminal conversation opens in a new tab of the
|
|
70
|
+
* person's terminal app (0.20.1+, agents targets --terminal).
|
|
61
71
|
*/
|
|
62
72
|
export const launchProjectCommand = async (ctx, values, [path = './', explicit]) => {
|
|
63
73
|
const named = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
|
|
74
|
+
const typing = attachable(ctx.io), here = typing || ctx.json;
|
|
64
75
|
if (values.resume !== true && values.review !== true) {
|
|
65
76
|
const project = projectIdentity(path);
|
|
66
77
|
const app = named === 'claude' || named === 'codex' ? named : new AgentStateStore(ctx.store.dir).app(project.remote ?? project.folder);
|
|
67
|
-
const where = process.platform === 'linux' &&
|
|
78
|
+
const where = process.platform === 'linux' && typing ? 'terminal' : openInChoices(ctx, project.slug)[app];
|
|
68
79
|
if (where === 't3')
|
|
69
80
|
return launchT3(ctx, values, path, app);
|
|
70
81
|
if (values.on === undefined && app === 'claude' && (where === 'claude-desktop' || where === 'web'))
|
|
71
82
|
return startConversation(ctx, values, [path, ...(explicit ? [explicit] : [])], where);
|
|
83
|
+
if (!here && where === 'terminal')
|
|
84
|
+
return projectInTerminalApp(ctx, values, path, named);
|
|
72
85
|
}
|
|
86
|
+
else if (!here)
|
|
87
|
+
return projectInTerminalApp(ctx, values, path, named);
|
|
73
88
|
return launchConversationProject(ctx, values, [path, ...(explicit ? [explicit] : [])]);
|
|
74
89
|
};
|
|
90
|
+
/** Flags `shard ./` hands to the terminal tab that runs it; files become absolute paths (the tab starts in the project). */
|
|
91
|
+
const NOT_FORWARDED = new Set(['json', 'api-url', 'agent-label', 'wake', 'wake-timeout', 'help', 'version', 'timing', 'reachable', 'claude', 'codex']);
|
|
92
|
+
export function forwardedFlags(values) {
|
|
93
|
+
const flags = [];
|
|
94
|
+
for (const [key, value] of Object.entries(values)) {
|
|
95
|
+
if (NOT_FORWARDED.has(key) || value === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
for (const v of Array.isArray(value) ? value : [value]) {
|
|
98
|
+
if (v === true)
|
|
99
|
+
flags.push(`--${key}`);
|
|
100
|
+
else if (v === false)
|
|
101
|
+
flags.push(`--no-${key}`);
|
|
102
|
+
else if (typeof v === 'string') {
|
|
103
|
+
if ((key === 'prompt-file' || key === 'image') && v === '-')
|
|
104
|
+
throw new UsageError(`--${key} - reads standard input, which a new terminal tab cannot: pass a file, or start it detached with --json.`);
|
|
105
|
+
flags.push(`--${key}`, key === 'prompt-file' || key === 'image' ? resolve(v) : v);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return flags;
|
|
110
|
+
}
|
|
111
|
+
/** `shard ./` with its flags in a new tab of the person's terminal app, in the project folder: the tab owns the start,
|
|
112
|
+
* any sign-in and the attach, as if the person typed it there. */
|
|
113
|
+
async function projectInTerminalApp(ctx, values, path, app) {
|
|
114
|
+
const result = await openShardInTerminal(ctx, ['./', ...(app ? [app] : []), ...forwardedFlags(values)], { cwd: resolve(path) });
|
|
115
|
+
if (!result.opened) {
|
|
116
|
+
throw new UsageError(`shard ./ opens Claude Code or Codex in a terminal, and none opened here${result.reason ? `: ${result.reason.replace(/\.$/, '')}` : ''}.`, 'ask your user to run shard ./ in their own terminal, or start the conversation detached: shard ./ --json --prompt "<task>" (shard ./ codex --json ... for Codex)');
|
|
117
|
+
}
|
|
118
|
+
reportTerminalOpen(ctx, result);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
75
121
|
/** Select a durable conversation, then use the harness's native terminal for all interaction. */
|
|
76
122
|
export const launchConversationProject = (ctx, values, args) => startConversation(ctx, values, args);
|
|
77
123
|
async function startConversation(ctx, values, [path = './', explicit], openIn) {
|
|
@@ -114,17 +160,31 @@ async function startConversation(ctx, values, [path = './', explicit], openIn) {
|
|
|
114
160
|
}
|
|
115
161
|
if (app === 'codex' && !linuxTerminalSession(ctx, values))
|
|
116
162
|
return launchCodexCloud(ctx, values, path, images);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
163
|
+
seedPhase('cli_start');
|
|
164
|
+
// Agent credentials and the Claude sign-in are checked beside the project's local work (its Git state and snapshot),
|
|
165
|
+
// and both pass before a VM is requested (0.21.1+).
|
|
166
|
+
const signIn = (async () => {
|
|
167
|
+
await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
|
|
168
|
+
const signed = app === 'claude' ? await cloudClaudeLogin(ctx) : true;
|
|
169
|
+
if (!signed)
|
|
170
|
+
requireClaudeTerminal(ctx);
|
|
171
|
+
seedPhase('claude_login');
|
|
172
|
+
return signed;
|
|
173
|
+
})();
|
|
174
|
+
signIn.catch(() => undefined);
|
|
175
|
+
// Whether this conversation sets the project up: hashed from the files it starts from (in a Git repository, as its VM
|
|
176
|
+
// reads them), and asked while its VM starts.
|
|
177
|
+
await ensureProjectGit(ctx, project.folder);
|
|
178
|
+
const setup = readConversationSetup(ctx, project);
|
|
179
|
+
seedPhase('project_git');
|
|
121
180
|
let created;
|
|
122
181
|
try {
|
|
123
|
-
created = await createConversation(ctx, project.folder, app, 'terminal', undefined, choice);
|
|
182
|
+
created = await createConversation(ctx, project.folder, app, 'terminal', undefined, choice, undefined, setup, signIn);
|
|
124
183
|
}
|
|
125
184
|
catch (e) {
|
|
126
185
|
if (!conversationsUnavailable(e))
|
|
127
186
|
throw e;
|
|
187
|
+
const signedIn = await signIn;
|
|
128
188
|
// Optional migration: preserve the native terminal on the old control plane, without reviving main sync.
|
|
129
189
|
const name = `c-${randomUUID().slice(0, 12)}`;
|
|
130
190
|
const prepared = await prepareProjectTemplate(ctx, project, undefined);
|
|
@@ -142,6 +202,9 @@ async function startConversation(ctx, values, [path = './', explicit], openIn) {
|
|
|
142
202
|
return terminal(ctx, ws, app, name, project.remote ?? project.folder, values, prepared.setupHash);
|
|
143
203
|
}
|
|
144
204
|
const { conversation: c, workspace: ws } = created;
|
|
205
|
+
const signedIn = await signIn;
|
|
206
|
+
const setupStart = labelConversationSetup(ctx, ws, c.worktree_path, await setup);
|
|
207
|
+
seedPhase('setup_label');
|
|
145
208
|
if (values.computer === true)
|
|
146
209
|
await ws.setComputerUse(true);
|
|
147
210
|
await uploadInitialImages(ws.cell(), images, ctx.signal);
|
|
@@ -152,17 +215,18 @@ async function startConversation(ctx, values, [path = './', explicit], openIn) {
|
|
|
152
215
|
// Without a saved machine the laptop's Claude and Codex config is copied in, after the agent has its first prompt.
|
|
153
216
|
const afterStart = afterStartConfig(ctx, ws, !!(await savedMachine(ctx))?.template);
|
|
154
217
|
await ensureMenubar(ctx.store.dir, ctx.apiUrl, new MenubarManager({ home: ctx.io.env.HOME, signal: ctx.signal }));
|
|
218
|
+
seedPhase('menubar');
|
|
155
219
|
if (openIn) {
|
|
156
220
|
// The agent starts detached in its VM; the conversation then opens in Claude Desktop or claude.ai (Remote Control).
|
|
157
221
|
const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
|
|
158
|
-
await conversationTerminal(quiet, ws, c, project.remote ?? project.folder, { ...values, detach: true }, afterStart);
|
|
222
|
+
await conversationTerminal(quiet, ws, c, project.remote ?? project.folder, { ...values, detach: true }, afterStart, setupStart);
|
|
159
223
|
if (openIn === 'claude-desktop')
|
|
160
224
|
return openClaudeDesktopSession(ctx, ws, c);
|
|
161
225
|
const url = await remoteControlUrl(ctx, ws, c);
|
|
162
|
-
if (!ctx.json && !openUrl(url, ctx.io.env))
|
|
226
|
+
if (!ctx.json && !(await openUrl(url, ctx.io.env)))
|
|
163
227
|
throw new Error(`could not open ${url}`);
|
|
164
228
|
out(ctx, `${url}\n`, { target: 'web', workspace: ws.data, conversation_id: c.id, url });
|
|
165
229
|
return 0;
|
|
166
230
|
}
|
|
167
|
-
return conversationTerminal(ctx, ws, c, project.remote ?? project.folder, values, afterStart);
|
|
231
|
+
return conversationTerminal(ctx, ws, c, project.remote ?? project.folder, values, afterStart, setupStart);
|
|
168
232
|
}
|
package/dist/conversations.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CellClient, Workspace, WorkspaceView } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
|
+
import type { LocalChanges } from './agent-repository.js';
|
|
3
4
|
import type { ModelChoice } from './agent-models.js';
|
|
5
|
+
import type { PreparedApply, PreparedTransfer } from './prepared-repository.js';
|
|
4
6
|
export type Harness = 'claude' | 'codex';
|
|
5
7
|
export interface Conversation {
|
|
6
8
|
id: string;
|
|
@@ -62,6 +64,33 @@ export interface LocalConversation {
|
|
|
62
64
|
handed_off?: boolean;
|
|
63
65
|
/** The branch the conversation started from (0.16.0+), for APIs that do not return base_branch yet. */
|
|
64
66
|
base_branch?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The prepared revision this conversation starts from (0.22.0+): captured while its VM was allocated and kept until
|
|
69
|
+
* the start is done, so a start resumed after a deadline uses exactly that state. `deferred`: large files still
|
|
70
|
+
* following the first prompt.
|
|
71
|
+
*/
|
|
72
|
+
preparation?: {
|
|
73
|
+
revision: string;
|
|
74
|
+
cache: string;
|
|
75
|
+
state: 'accepted' | 'syncing' | 'deferred' | 'ready' | 'error';
|
|
76
|
+
error?: string;
|
|
77
|
+
workspace?: string;
|
|
78
|
+
transfer?: PreparedTransfer;
|
|
79
|
+
apply?: PreparedApply;
|
|
80
|
+
pending?: string[];
|
|
81
|
+
};
|
|
82
|
+
cloud_start?: {
|
|
83
|
+
prompt?: string;
|
|
84
|
+
computer: boolean;
|
|
85
|
+
attachments_pending: boolean;
|
|
86
|
+
submitted?: boolean;
|
|
87
|
+
state: 'accepted' | 'active' | 'error';
|
|
88
|
+
error?: string;
|
|
89
|
+
/** When the process carrying this start last said so (carryStart): the device helper resumes only one nobody carries. */
|
|
90
|
+
heartbeat_at?: number;
|
|
91
|
+
};
|
|
92
|
+
/** Why this start left its prepared revision for the copy seed. */
|
|
93
|
+
preparation_fallback?: string;
|
|
65
94
|
}
|
|
66
95
|
/**
|
|
67
96
|
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
@@ -95,14 +124,57 @@ export declare function restoreConversationBinding(ctx: Ctx, c: Conversation, ws
|
|
|
95
124
|
export interface MovedInSession {
|
|
96
125
|
session_id: string;
|
|
97
126
|
title?: string;
|
|
127
|
+
preserveWorktrees?: boolean;
|
|
98
128
|
}
|
|
99
|
-
|
|
129
|
+
/**
|
|
130
|
+
* `ready`: checks the caller runs beside this local work (a sign-in check, 0.21.1+); the VM is requested only after
|
|
131
|
+
* they pass. The conversation API's availability is read meanwhile too.
|
|
132
|
+
*/
|
|
133
|
+
export declare function createConversationRecord(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, allocated?: (workspace: WorkspaceView) => void, idempotencyKey?: string, choice?: ModelChoice, movedIn?: MovedInSession, ready?: Promise<unknown>): Promise<Conversation>;
|
|
134
|
+
/**
|
|
135
|
+
* Brings the conversation's accepted revision into its VM (0.22.0+): only what the project cache lacks is sent, and
|
|
136
|
+
* with `defer` large incompressible files follow the first prompt. Resumable: a later call (the device helper, or
|
|
137
|
+
* `shard agents open`) continues the same revision, never a newer checkout. False when the start has no revision.
|
|
138
|
+
*/
|
|
139
|
+
export declare function prepareAcceptedConversation(ctx: Ctx, c: Conversation, ws: Workspace, options?: {
|
|
140
|
+
defer?: boolean;
|
|
141
|
+
fallback?: boolean;
|
|
142
|
+
setup?: Promise<{
|
|
143
|
+
depsHash: string;
|
|
144
|
+
} | undefined>;
|
|
145
|
+
}): Promise<boolean>;
|
|
146
|
+
/**
|
|
147
|
+
* Sends a conversation's deferred large files and lands them in its worktree. Runs while its VM is awake (it never
|
|
148
|
+
* wakes one); the device helper retries until the start is whole.
|
|
149
|
+
*/
|
|
150
|
+
export declare function completeDeferredPreparation(ctx: Ctx, c: Conversation): Promise<boolean>;
|
|
151
|
+
/**
|
|
152
|
+
* The one-time seed of a new conversation's worktree (a conversation never watches or writes the owner's checkout):
|
|
153
|
+
* its prepared revision (0.22.0+), or the copy seed when the start has none or the VM cannot take one. Repeatable: a
|
|
154
|
+
* start resumed after a deadline continues here.
|
|
155
|
+
*/
|
|
156
|
+
export declare function seedConversation(ctx: Ctx, c: Conversation, ws: Workspace, folder: string, movedIn?: MovedInSession & {
|
|
157
|
+
seedRepository?(ws: Workspace, c: Conversation): Promise<void>;
|
|
158
|
+
}, changes?: Promise<LocalChanges>, setup?: Promise<{
|
|
159
|
+
depsHash: string;
|
|
160
|
+
} | undefined>): Promise<void>;
|
|
100
161
|
export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, choice?: ModelChoice, movedIn?: MovedInSession & {
|
|
101
162
|
beforeSeed(ws: Workspace, c: Conversation): Promise<void>;
|
|
163
|
+
seedRepository?(ws: Workspace, c: Conversation): Promise<void>;
|
|
164
|
+
}, setup?: Promise<{
|
|
165
|
+
depsHash: string;
|
|
166
|
+
} | undefined>, ready?: Promise<unknown>, startup?: {
|
|
167
|
+
accepted(c: Conversation): void;
|
|
168
|
+
beforeSeed(ws: Workspace, c: Conversation): Promise<void>;
|
|
102
169
|
}): Promise<{
|
|
103
170
|
conversation: Conversation;
|
|
104
171
|
workspace: Workspace;
|
|
105
172
|
}>;
|
|
173
|
+
/**
|
|
174
|
+
* The conversation branch, then the start state's snapshot, in one request (0.21.1+): a failed checkout fails the
|
|
175
|
+
* start, a failed snapshot only leaves it without one (recordSeedSnapshot). Its hub push stays detached in the VM.
|
|
176
|
+
*/
|
|
177
|
+
export declare function checkoutConversationBranch(ctx: Pick<Ctx, 'store' | 'signal'>, cell: Pick<CellClient, 'exec'>, c: Conversation): Promise<void>;
|
|
106
178
|
export declare function resumeConversation(ctx: Ctx, c: Conversation): Promise<Conversation>;
|
|
107
179
|
/** The model and effort a conversation's harness starts with, as its record keeps them. */
|
|
108
180
|
export declare const recordedChoice: (c: Pick<Conversation, "model" | "effort">) => ModelChoice;
|
package/dist/conversations.js
CHANGED
|
@@ -4,13 +4,19 @@ import { AgentStateStore } from "./agent-state.js";
|
|
|
4
4
|
import { gitRunner } from "./agent-git.js";
|
|
5
5
|
import { projectIdentity } from "./project-identity.js";
|
|
6
6
|
import { ensureProjectGit } from "./project-git.js";
|
|
7
|
-
import { preflightProjectSeed, seedAgentRepository } from "./agent-repository.js";
|
|
8
|
-
import { UsageError } from "./errors.js";
|
|
7
|
+
import { localChanges, preflightProjectSeed, runGitStep, seedAgentRepository } from "./agent-repository.js";
|
|
8
|
+
import { CodedError, UsageError } from "./errors.js";
|
|
9
9
|
import { conversationClient } from "./conversation-commands.js";
|
|
10
|
-
import { recordSeedSnapshot } from "./files-mirror.js";
|
|
10
|
+
import { HUB_PATH, recordSeedSnapshot, SEED_SNAPSHOT } from "./files-mirror.js";
|
|
11
11
|
import { rememberTwinSeed, sendTwinSeed, twinCheckoutArgv, twinSeed } from "./worktree-twin.js";
|
|
12
12
|
import { progress } from "./progress.js";
|
|
13
13
|
import { seedPhase, startProgress } from "./seed-phases.js";
|
|
14
|
+
import { storedTitlePrefix } from "./client-settings.js";
|
|
15
|
+
import { applyPreparedSnapshot, capturePreparedSnapshot, landDeferredFiles, preparedCachePath, preparedGitConfig, PreparedRefusal, pruneLocalPrepared, readPreparedSnapshot, releaseLocalSnapshot, transferPreparedSnapshot } from "./prepared-repository.js";
|
|
16
|
+
import { withTargetLock } from "./target-lock.js";
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { acceptedProgress } from "./seed-phases.js";
|
|
19
|
+
import { recordSetupIntent } from "./project-template-setup.js";
|
|
14
20
|
/**
|
|
15
21
|
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
16
22
|
* while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
|
|
@@ -68,7 +74,7 @@ export function rememberConversation(ctx, c, folder, handedOff = false, baseBran
|
|
|
68
74
|
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
69
75
|
const records = doc.conversations ?? {};
|
|
70
76
|
const base = baseBranch ?? records[c.id]?.base_branch;
|
|
71
|
-
records[c.id] = { api_url: ctx.apiUrl, folder, project_id: c.project_id, conversation: c, ...(handedOff ? { handed_off: true } : {}), ...(base ? { base_branch: base } : {}) };
|
|
77
|
+
records[c.id] = { ...records[c.id], api_url: ctx.apiUrl, folder, project_id: c.project_id, conversation: c, ...(handedOff ? { handed_off: true } : {}), ...(base ? { base_branch: base } : {}) };
|
|
72
78
|
doc.conversations = records;
|
|
73
79
|
});
|
|
74
80
|
}
|
|
@@ -157,58 +163,273 @@ export async function restoreConversationBinding(ctx, c, ws) {
|
|
|
157
163
|
if (c.restore.kind === 'resume')
|
|
158
164
|
progress(ctx, 'Restored');
|
|
159
165
|
}
|
|
160
|
-
|
|
166
|
+
/**
|
|
167
|
+
* `ready`: checks the caller runs beside this local work (a sign-in check, 0.21.1+); the VM is requested only after
|
|
168
|
+
* they pass. The conversation API's availability is read meanwhile too.
|
|
169
|
+
*/
|
|
170
|
+
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID(), choice = {}, movedIn, ready) {
|
|
161
171
|
const project = projectIdentity(folder);
|
|
162
172
|
await ensureProjectGit(ctx, project.folder);
|
|
163
173
|
// A start refuses before a VM is allocated, never after (0.19.0+).
|
|
164
174
|
await preflightProjectSeed(project.folder, ctx.signal);
|
|
165
|
-
// Both the conversation API and its legacy workspace fallback need a usable local HEAD.
|
|
166
|
-
|
|
175
|
+
// Both the conversation API and its legacy workspace fallback need a usable local HEAD. The API's answer (a legacy
|
|
176
|
+
// control plane refuses) is awaited before the create, after the local snapshot it runs beside.
|
|
177
|
+
const probe = conversationClient(ctx).conversations.list({ limit: 1 }).then(() => undefined, (error) => ({ error }));
|
|
167
178
|
const git = gitRunner(project.folder, { ...process.env, ...ctx.io.env }, ctx.signal);
|
|
168
179
|
const head = await git(['rev-parse', 'HEAD']);
|
|
169
180
|
const branch = await git(['symbolic-ref', '--quiet', '--short', 'HEAD']).catch(() => undefined);
|
|
170
181
|
// Uncommitted edits ride along as the twin snapshot the conversation starts from (worktree-twin.ts).
|
|
171
|
-
const seed =
|
|
182
|
+
const seed = movedIn?.preserveWorktrees ? { commit: head, snapshot: false }
|
|
183
|
+
: await twinSeed(ctx.store.dir, project.folder, head, ctx.store.profile(ctx.apiUrl).project_id).catch(() => ({ commit: head, snapshot: false }));
|
|
172
184
|
seedPhase('local_snapshot');
|
|
185
|
+
const refused = await probe;
|
|
186
|
+
if (refused)
|
|
187
|
+
throw refused.error;
|
|
188
|
+
await ready;
|
|
189
|
+
seedPhase('api_probe');
|
|
173
190
|
const commit = seed.commit;
|
|
191
|
+
// The prepared revision of that state (0.22.0+): its media and what Git does not hold (.env files, filter output),
|
|
192
|
+
// captured while the VM is requested and boots. A capture that cannot complete falls back to the copy seed, never fails
|
|
193
|
+
// the start. ACP sessions seed their own way (acp-models.ts).
|
|
194
|
+
const capture = movedIn || route === 'acp' ? Promise.resolve(undefined) : capturePreparedSnapshot(ctx.store.dir, project.folder, seed, project.remote, ctx.signal).catch((error) => {
|
|
195
|
+
if (ctx.signal.aborted)
|
|
196
|
+
throw error;
|
|
197
|
+
ctx.io.stderr.write(`Project snapshot: ${error instanceof Error ? error.message : String(error)}; copying the project instead.\n`);
|
|
198
|
+
return undefined;
|
|
199
|
+
});
|
|
200
|
+
capture.catch(() => undefined);
|
|
201
|
+
// The title marker this computer chose (`shard settings --title-prefix`, 0.20.1+); none sent means the platform's.
|
|
202
|
+
const titlePrefix = storedTitlePrefix(ctx.store.dir);
|
|
174
203
|
const response = await conversationClient(ctx).conversations.create({ repoRemote: project.remote ?? `file://${project.folder}`, harness, route, mode: 'new',
|
|
175
204
|
base: { commit, ...(branch ? { branch } : {}) }, ...(name ? { name } : {}), ...choice, projectId: ctx.store.profile(ctx.apiUrl).project_id,
|
|
205
|
+
...(titlePrefix !== undefined ? { titlePrefix } : {}),
|
|
176
206
|
...(movedIn ? { harnessSessionId: movedIn.session_id, ...(movedIn.title ? { title: movedIn.title.slice(0, 200) } : {}) } : {}) }, { idempotencyKey, signal: ctx.signal });
|
|
177
207
|
const c = validateConversation({ ...response.conversation, restore: response.restore });
|
|
178
208
|
allocated?.(response.workspace);
|
|
179
209
|
rememberTwinSeed(ctx.store.dir, c.id, project.folder, seed, branch);
|
|
180
210
|
rememberConversation(ctx, c, project.folder, false, branch);
|
|
211
|
+
// The seed awaits the capture (prepareAcceptedConversation), so it runs on while the VM boots, with the Git settings
|
|
212
|
+
// the VM repository copies from this one.
|
|
213
|
+
pendingCaptures.set(c.id, capture.then(async (prepared) => {
|
|
214
|
+
if (!prepared)
|
|
215
|
+
return undefined;
|
|
216
|
+
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
217
|
+
const record = doc.conversations[c.id];
|
|
218
|
+
if (record)
|
|
219
|
+
record.preparation = { revision: prepared.revision, cache: preparedCachePath(project.remote ?? `file://${project.folder}`), state: 'accepted' };
|
|
220
|
+
});
|
|
221
|
+
return { snapshot: prepared, git: await preparedGitConfig(project.folder, prepared.manifest, ctx.signal) };
|
|
222
|
+
}));
|
|
223
|
+
acceptedProgress(c.id, c.key);
|
|
181
224
|
return c;
|
|
182
225
|
}
|
|
183
|
-
|
|
226
|
+
/** Captures still running for conversations this process created (createConversationRecord). */
|
|
227
|
+
const pendingCaptures = new Map();
|
|
228
|
+
/** A start the copy seed took over: its prepared revision is no longer this conversation's (recorded as the reason). */
|
|
229
|
+
function dropPreparation(ctx, id, reason) {
|
|
230
|
+
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
231
|
+
const record = doc.conversations[id];
|
|
232
|
+
if (record?.preparation) {
|
|
233
|
+
delete record.preparation;
|
|
234
|
+
record.preparation_fallback = reason;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function updatePreparation(ctx, id, patch) {
|
|
239
|
+
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
240
|
+
const record = doc.conversations[id];
|
|
241
|
+
if (record?.preparation)
|
|
242
|
+
record.preparation = { ...record.preparation, ...patch };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const errorText = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 2000);
|
|
246
|
+
/** The template checkout a conversation outside it copies dependency folders from (a Codex Cloud worktree). */
|
|
247
|
+
const templateCheckout = (c) => `/home/user/${c.key.split('/')[0]}`;
|
|
248
|
+
/**
|
|
249
|
+
* Brings the conversation's accepted revision into its VM (0.22.0+): only what the project cache lacks is sent, and
|
|
250
|
+
* with `defer` large incompressible files follow the first prompt. Resumable: a later call (the device helper, or
|
|
251
|
+
* `shard agents open`) continues the same revision, never a newer checkout. False when the start has no revision.
|
|
252
|
+
*/
|
|
253
|
+
export async function prepareAcceptedConversation(ctx, c, ws, options = {}) {
|
|
254
|
+
const capturing = pendingCaptures.get(c.id);
|
|
255
|
+
let captured;
|
|
256
|
+
if (capturing) {
|
|
257
|
+
captured = await capturing.catch(() => undefined);
|
|
258
|
+
pendingCaptures.delete(c.id);
|
|
259
|
+
seedPhase('prepared_snapshot');
|
|
260
|
+
}
|
|
261
|
+
// The setup intent goes in the exec that checks the repository out, before the repository appears (0.22.0+).
|
|
262
|
+
const intent = await options.setup;
|
|
263
|
+
const local = localConversations(ctx)[c.id];
|
|
264
|
+
if (!local?.preparation)
|
|
265
|
+
return false;
|
|
266
|
+
if (local.preparation.state === 'ready' || local.preparation.state === 'deferred')
|
|
267
|
+
return true;
|
|
268
|
+
return withTargetLock(join(ctx.store.dir, 'prepared-starts', c.id), async () => {
|
|
269
|
+
const current = localConversations(ctx)[c.id]?.preparation;
|
|
270
|
+
if (!current)
|
|
271
|
+
return false;
|
|
272
|
+
if (current.state === 'ready' || current.state === 'deferred')
|
|
273
|
+
return true;
|
|
274
|
+
try {
|
|
275
|
+
updatePreparation(ctx, c.id, { state: 'syncing', error: undefined });
|
|
276
|
+
const fresh = captured?.snapshot.revision === current.revision ? captured : undefined;
|
|
277
|
+
const snapshot = fresh?.snapshot ?? await readPreparedSnapshot(ctx.store.dir, local.folder, current.revision);
|
|
278
|
+
const cell = ws.cell(), outside = c.worktree_path !== templateCheckout(c);
|
|
279
|
+
// The seed's copy to the project hub starts in the same exec as the checkout, detached (no extra round trip).
|
|
280
|
+
const target = { root: c.worktree_path, branch: c.branch, options: { signal: ctx.signal, adopt: true,
|
|
281
|
+
git: fresh?.git ?? await preparedGitConfig(local.folder, snapshot.manifest, ctx.signal), ...(outside ? { dependencySource: templateCheckout(c) } : {}),
|
|
282
|
+
after: { script: SEED_SNAPSHOT, args: [c.worktree_path, c.branch.replace(/^shard\//, ''), HUB_PATH, 'background'] },
|
|
283
|
+
...(intent ? { setupIntent: { conversation: c.id, depsHash: intent.depsHash } } : {}), workspace: ws.id } };
|
|
284
|
+
let transfer, apply;
|
|
285
|
+
try {
|
|
286
|
+
// A warm start: the probe checks the revision out in its own exec.
|
|
287
|
+
transfer = await transferPreparedSnapshot(cell, local.folder, current.cache, snapshot, { signal: ctx.signal, defer: options.defer ?? true, pin: c.id, apply: target });
|
|
288
|
+
seedPhase('prepared_transfer');
|
|
289
|
+
apply = transfer.applied ?? await applyPreparedSnapshot(cell, current.cache, current.revision, c.worktree_path, c.branch, { ...target.options, ...(transfer.complete ? { releasePin: c.id } : {}) });
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
// No agent has this worktree yet: a VM that cannot take a prepared revision (a custom template without Python
|
|
293
|
+
// 3.11, say) still starts, through the copy seed, into a clean directory.
|
|
294
|
+
if (ctx.signal.aborted || !options.fallback || (error instanceof PreparedRefusal && error.reason === 'owned'))
|
|
295
|
+
throw error;
|
|
296
|
+
ctx.io.stderr.write(`Project transfer: ${errorText(error)}; copying the project instead.\n`);
|
|
297
|
+
// The copy seed owns this start from here: nothing resumes the prepared one onto its worktree.
|
|
298
|
+
dropPreparation(ctx, c.id, errorText(error));
|
|
299
|
+
await releaseLocalSnapshot(local.folder, current.revision).catch(() => undefined);
|
|
300
|
+
// A fresh start leaves nothing at the path; a checkout taken over in place (a template's) is set aside.
|
|
301
|
+
await cell.exec.run(['sh', '-c', 'if [ -e "$1" ]; then mv "$1" "$1.unfinished-$(date +%s)"; fi', 'shard-seed', c.worktree_path], { timeoutMs: 30_000, signal: ctx.signal });
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
seedPhase('prepared_apply');
|
|
305
|
+
updatePreparation(ctx, c.id, { state: transfer.complete ? 'ready' : 'deferred', transfer, apply, pending: transfer.pending, error: undefined, workspace: ws.id });
|
|
306
|
+
if (transfer.complete)
|
|
307
|
+
await releaseLocalSnapshot(local.folder, current.revision);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
updatePreparation(ctx, c.id, { state: 'error', error: errorText(error) });
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}, ctx.signal);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Sends a conversation's deferred large files and lands them in its worktree. Runs while its VM is awake (it never
|
|
318
|
+
* wakes one); the device helper retries until the start is whole.
|
|
319
|
+
*/
|
|
320
|
+
export async function completeDeferredPreparation(ctx, c) {
|
|
321
|
+
const local = localConversations(ctx)[c.id];
|
|
322
|
+
if (local?.preparation?.state !== 'deferred' || conversationEnded(c))
|
|
323
|
+
return false;
|
|
324
|
+
return withTargetLock(join(ctx.store.dir, 'prepared-starts', c.id), async () => {
|
|
325
|
+
const current = localConversations(ctx)[c.id]?.preparation;
|
|
326
|
+
if (current?.state !== 'deferred')
|
|
327
|
+
return false;
|
|
328
|
+
let ws;
|
|
329
|
+
try {
|
|
330
|
+
ws = await conversationClient(ctx).workspaces.get(c.workspace_id);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (!(error instanceof ShardfluxApiError && error.status === 404))
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
if (!ws || ws.data.deleted_at || (current.workspace && current.workspace !== c.workspace_id)) {
|
|
337
|
+
// The VM is gone (reclaimed, moved, deleted) or the conversation continues in another one (a resume restores its
|
|
338
|
+
// worktree there): there is no worktree left to complete.
|
|
339
|
+
updatePreparation(ctx, c.id, { state: 'error', error: 'The conversation\'s VM was removed before its media arrived.', pending: undefined });
|
|
340
|
+
await releaseLocalSnapshot(local.folder, current.revision);
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if (!ws.ready)
|
|
344
|
+
return false;
|
|
345
|
+
const snapshot = await readPreparedSnapshot(ctx.store.dir, local.folder, current.revision);
|
|
346
|
+
const cell = ws.cell();
|
|
347
|
+
// Staged into the conversation's own repository and landed before this returns; the cache gets them after.
|
|
348
|
+
const transfer = await transferPreparedSnapshot(cell, local.folder, current.cache, snapshot, { signal: ctx.signal, pin: c.id,
|
|
349
|
+
apply: { root: c.worktree_path, branch: c.branch, options: { adopt: true } } });
|
|
350
|
+
// Media edited on this computer after the start never arrive as captured: the worktree goes on without them.
|
|
351
|
+
const drop = snapshot.manifest.files.filter(f => transfer.changed.includes(f.path)).map(f => f.oid);
|
|
352
|
+
const landed = await landDeferredFiles(cell, current.cache, current.revision, c.worktree_path, { signal: ctx.signal, drop });
|
|
353
|
+
if (landed.pending)
|
|
354
|
+
return false;
|
|
355
|
+
updatePreparation(ctx, c.id, { state: 'ready', transfer: { ...transfer, pending: current.pending ?? [] }, pending: undefined });
|
|
356
|
+
await releaseLocalSnapshot(local.folder, current.revision);
|
|
357
|
+
await pruneLocalPrepared(ctx.store.dir).catch(() => undefined);
|
|
358
|
+
return true;
|
|
359
|
+
}, ctx.signal);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The one-time seed of a new conversation's worktree (a conversation never watches or writes the owner's checkout):
|
|
363
|
+
* its prepared revision (0.22.0+), or the copy seed when the start has none or the VM cannot take one. Repeatable: a
|
|
364
|
+
* start resumed after a deadline continues here.
|
|
365
|
+
*/
|
|
366
|
+
export async function seedConversation(ctx, c, ws, folder, movedIn, changes, setup) {
|
|
367
|
+
// Requests the copy seed starts and nothing waits for (the history backfill's start), settled before this returns.
|
|
368
|
+
const background = [];
|
|
369
|
+
const cell = ws.cell();
|
|
370
|
+
// A conversation that sets its project up records it before its repository appears: its guest starts the agent as
|
|
371
|
+
// soon as the repository appears, then with the setup's context and toolchains (0.21.1+). A prepared start writes it
|
|
372
|
+
// in its checkout's exec; the copy seed beside its first request.
|
|
373
|
+
let recorded;
|
|
374
|
+
const recordIntent = () => recorded ??= (async () => {
|
|
375
|
+
const intent = await setup;
|
|
376
|
+
if (intent)
|
|
377
|
+
await recordSetupIntent(cell, c, intent.depsHash, ctx.signal).catch((error) => {
|
|
378
|
+
if (!ctx.signal.aborted)
|
|
379
|
+
ctx.io.stderr.write(`The agent starts without the project setup context: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
380
|
+
});
|
|
381
|
+
})();
|
|
382
|
+
if (movedIn?.seedRepository) {
|
|
383
|
+
void recordIntent();
|
|
384
|
+
await movedIn.seedRepository(ws, c);
|
|
385
|
+
}
|
|
386
|
+
else if (!await prepareAcceptedConversation(ctx, c, ws, { fallback: true, ...(setup ? { setup } : {}) })) {
|
|
387
|
+
await seedAgentRepository(ctx, ws, folder, { root: c.worktree_path, populate: true, changes: changes ?? localChanges(folder, ctx.signal),
|
|
388
|
+
background: started => background.push(started), beforeHistory: recordIntent() });
|
|
389
|
+
}
|
|
390
|
+
await recorded;
|
|
391
|
+
seedPhase('seed_done');
|
|
392
|
+
const preparation = localConversations(ctx)[c.id]?.preparation?.state, prepared = preparation === 'ready' || preparation === 'deferred';
|
|
393
|
+
if (!prepared && !movedIn?.preserveWorktrees) {
|
|
394
|
+
await sendTwinSeed(ctx.store.dir, cell, c, folder, ctx.signal);
|
|
395
|
+
seedPhase('twin_seed');
|
|
396
|
+
await checkoutConversationBranch(ctx, cell, c);
|
|
397
|
+
seedPhase('branch_checkout');
|
|
398
|
+
}
|
|
399
|
+
else if (!prepared) {
|
|
400
|
+
await recordSeedSnapshot(ws, c, ctx.signal);
|
|
401
|
+
seedPhase('seed_snapshot');
|
|
402
|
+
}
|
|
403
|
+
await Promise.all(background);
|
|
404
|
+
// Large media that follow the first prompt keep coming while this process runs; the device helper finishes them.
|
|
405
|
+
if (preparation === 'deferred')
|
|
406
|
+
void completeDeferredPreparation(ctx, c).catch(() => undefined);
|
|
407
|
+
// This computer's spool and snapshots stay bounded with no selected project to prune them (hourly at most).
|
|
408
|
+
void pruneLocalPrepared(ctx.store.dir, undefined, 3_600_000).catch(() => undefined);
|
|
409
|
+
}
|
|
410
|
+
export async function createConversation(ctx, folder, harness, route, name, choice = {}, movedIn, setup, ready, startup) {
|
|
184
411
|
const project = projectIdentity(folder);
|
|
185
412
|
seedPhase('start');
|
|
186
413
|
// The launcher's phase line (SHARDFLUX_PROGRESS_FILE, 0.19.0+): a computer starts, then the seed's own marks move it on.
|
|
187
414
|
startProgress(harness, ctx.io.env);
|
|
188
|
-
const c = await createConversationRecord(ctx, project.folder, harness, route, name, undefined, undefined, choice, movedIn);
|
|
415
|
+
const c = await createConversationRecord(ctx, project.folder, harness, route, name, undefined, undefined, choice, movedIn, ready);
|
|
416
|
+
startup?.accepted(c);
|
|
189
417
|
seedPhase('conversation_create');
|
|
418
|
+
// A start without a prepared revision (a moved-in session) reads this computer's changes while its VM starts.
|
|
419
|
+
const changes = movedIn && !movedIn.seedRepository ? localChanges(project.folder, ctx.signal) : undefined;
|
|
420
|
+
changes?.catch(() => undefined);
|
|
190
421
|
try {
|
|
191
422
|
const ws = await conversationWorkspace(ctx, c);
|
|
192
423
|
seedPhase('vm_running');
|
|
193
424
|
// A moved-in session's transcript is in place before the repository: its guest resumes it once the seed is done.
|
|
194
425
|
await movedIn?.beforeSeed(ws, c);
|
|
195
|
-
|
|
196
|
-
await
|
|
197
|
-
const cell = ws.cell();
|
|
198
|
-
seedPhase('seed_done');
|
|
199
|
-
await sendTwinSeed(ctx.store.dir, cell, c, project.folder, ctx.signal);
|
|
200
|
-
seedPhase('twin_seed');
|
|
201
|
-
const result = await cell.exec.run(twinCheckoutArgv(ctx.store.dir, c), { timeoutMs: 30_000, signal: ctx.signal });
|
|
202
|
-
if (result.exitCode !== 0)
|
|
203
|
-
throw new Error(`Conversation branch could not be prepared: ${result.stderr.trim()}`);
|
|
204
|
-
seedPhase('branch_checkout');
|
|
205
|
-
await recordSeedSnapshot(ws, c, ctx.signal);
|
|
206
|
-
seedPhase('seed_snapshot');
|
|
426
|
+
await startup?.beforeSeed(ws, c);
|
|
427
|
+
await seedConversation(ctx, c, ws, project.folder, movedIn, changes, setup);
|
|
207
428
|
return { conversation: c, workspace: ws };
|
|
208
429
|
}
|
|
209
430
|
catch (error) {
|
|
210
431
|
// A failed move leaves nothing behind: its session can move again (the API counts only a live VM's move).
|
|
211
|
-
if (
|
|
432
|
+
if (movedIn) {
|
|
212
433
|
try {
|
|
213
434
|
await ctx.cloud().workspaces.delete(c.workspace_id);
|
|
214
435
|
}
|
|
@@ -216,9 +437,22 @@ export async function createConversation(ctx, folder, harness, route, name, choi
|
|
|
216
437
|
throw new AggregateError([error, cleanup], `Cloud setup failed; remove owned workspace ${c.workspace_id} before retrying.`, { cause: cleanup });
|
|
217
438
|
}
|
|
218
439
|
}
|
|
440
|
+
if (route === 'codex-cloud')
|
|
441
|
+
throw new CodedError('project_preparation_pending', `Accepted ${c.key}; project preparation can resume. Open this conversation to continue.`, `shard agents open ${c.id}`, { conversation_id: c.id, workspace_key: c.key, workspace_id: c.workspace_id, setup_state: localConversations(ctx)[c.id]?.preparation?.state ?? 'error' });
|
|
219
442
|
throw error;
|
|
220
443
|
}
|
|
221
444
|
}
|
|
445
|
+
const shellQuote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
446
|
+
/**
|
|
447
|
+
* The conversation branch, then the start state's snapshot, in one request (0.21.1+): a failed checkout fails the
|
|
448
|
+
* start, a failed snapshot only leaves it without one (recordSeedSnapshot). Its hub push stays detached in the VM.
|
|
449
|
+
*/
|
|
450
|
+
export async function checkoutConversationBranch(ctx, cell, c) {
|
|
451
|
+
const checkout = `${twinCheckoutArgv(ctx.store.dir, c).map(shellQuote).join(' ')} || exit 3\nsh -c "$1" shard-seed "$2" "$3" "$4" background >/dev/null || true`;
|
|
452
|
+
const result = await runGitStep(cell, ['sh', '-c', checkout, 'shard-seed', SEED_SNAPSHOT, c.worktree_path, c.branch.replace(/^shard\//, ''), HUB_PATH], { timeoutMs: 120_000, maxOutputBytes: 4096, signal: ctx.signal });
|
|
453
|
+
if (result.exitCode !== 0)
|
|
454
|
+
throw new Error(`Conversation branch could not be prepared: ${result.stderr.trim()}`);
|
|
455
|
+
}
|
|
222
456
|
export async function resumeConversation(ctx, c) {
|
|
223
457
|
if (!conversationEnded(c))
|
|
224
458
|
return getConversation(ctx, c.id);
|
package/dist/device-bridge.d.ts
CHANGED
|
@@ -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;
|