@shardflux/cli 0.20.0 → 0.23.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 +312 -1
- package/README.md +242 -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-journal.js +58 -6
- package/dist/acp-login.d.ts +13 -5
- package/dist/acp-login.js +37 -8
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +29 -19
- package/dist/acp.js +120 -14
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +110 -42
- 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.d.ts +20 -1
- package/dist/agent-target-commands.js +73 -31
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +95 -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-hub.d.ts +61 -25
- package/dist/cli-proxy-hub.js +202 -54
- package/dist/cli-proxy.d.ts +44 -1
- package/dist/cli-proxy.js +177 -28
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +14 -4
- package/dist/codex-cloud.js +135 -31
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +18 -5
- package/dist/command-center-model.d.ts +2 -0
- package/dist/command-center-model.js +6 -1
- package/dist/command-center.js +3 -3
- package/dist/commands.js +173 -24
- 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 +78 -13
- package/dist/conversations.d.ts +86 -1
- package/dist/conversations.js +306 -23
- package/dist/desktop-linux.js +16 -3
- 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 +29 -2
- package/dist/errors.js +50 -7
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +133 -57
- 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/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +69 -0
- package/dist/move-worktrees.js +252 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +111 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +63 -29
- package/dist/prepared-repository.d.ts +204 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1307 -0
- package/dist/project-preparation.d.ts +62 -0
- package/dist/project-preparation.js +316 -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 +22 -4
- package/dist/project-sync.js +34 -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.d.ts +2 -1
- package/dist/t3-launch.js +12 -6
- 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/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +168 -0
- package/dist/terminal-apps.js +574 -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.d.ts +2 -0
- package/dist/update.js +43 -9
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/acp.js
CHANGED
|
@@ -6,14 +6,20 @@ import { AcpRpc, RpcError, objectValue } from "./acp-rpc.js";
|
|
|
6
6
|
import { ACP_PROBE_CATALOG, nativeCurrentModel, nativeModelOptions, withoutNativeModel } from "./acp-catalog.js";
|
|
7
7
|
import { ACP_LIFECYCLE, AcpModelCache, acpScope } from "./acp-models.js";
|
|
8
8
|
import { AcpJournal } from "./acp-journal.js";
|
|
9
|
-
import { getConversation, conversationWorkspace, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
|
|
9
|
+
import { getConversation, conversationWorkspace, preparedInFlight, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
|
|
10
|
+
import { seedEvent } from "./seed-phases.js";
|
|
10
11
|
import { projectIdentity } from "./project-identity.js";
|
|
12
|
+
import { projectSyncBoundary } from "./project-sync.js";
|
|
13
|
+
import { localManifest } from "./project-sync-manifest.js";
|
|
11
14
|
import { describeError, ownHint } from "./errors.js";
|
|
12
15
|
import { serveAcpDeviceApprovals } from "./device-bridge.js";
|
|
13
16
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
14
17
|
import { cloudClaudeLogin } from "./claude-cloud-login.js";
|
|
15
18
|
import { ACP_LOGIN_METHOD, acpSignedIn } from "./acp-login.js";
|
|
16
19
|
import { ensureMenubar, MenubarManager } from "./menubar.js";
|
|
20
|
+
import { syncAgentConfigAfterStart } from "./agent-config-sync.js";
|
|
21
|
+
import { savedMachine } from "./filesystem-state.js";
|
|
22
|
+
import { accountVmProxy, prepareVmProxy } from "./t3-proxy.js";
|
|
17
23
|
const PREFIX = 'shardflux:';
|
|
18
24
|
export function acpModel(app, id) { return `${PREFIX}${app}:${id}`; }
|
|
19
25
|
export function parseAcpModel(value) {
|
|
@@ -78,6 +84,7 @@ export class ShardfluxAcp {
|
|
|
78
84
|
#sessions = new Map();
|
|
79
85
|
#lifecycle;
|
|
80
86
|
#cache;
|
|
87
|
+
#proxied = new Map();
|
|
81
88
|
#initialized = false;
|
|
82
89
|
#probe = false;
|
|
83
90
|
constructor(ctx, client, errors = acpError, lifecycle = ACP_LIFECYCLE) { this.#ctx = ctx; this.#client = client; this.#store = new AgentStateStore(ctx.store.dir); this.#cache = new AcpModelCache(ctx.store.dir); this.#errors = errors; this.#lifecycle = lifecycle; }
|
|
@@ -196,11 +203,16 @@ export class ShardfluxAcp {
|
|
|
196
203
|
}
|
|
197
204
|
if (!record.model)
|
|
198
205
|
throw new RpcError(-32602, 'Choose an available model before sending a message.');
|
|
199
|
-
await this.#connect(session);
|
|
206
|
+
const started = await this.#connect(session);
|
|
200
207
|
if (method === 'session/load' || method === 'session/resume')
|
|
201
208
|
return this.#configuration(record.model, { ...record.setup, sessionId: record.id }, record.catalog);
|
|
202
209
|
const mapped = acpReferences(params, record, session.conversation.worktree_path, true);
|
|
210
|
+
const first = method === 'session/prompt' && !record.prompted;
|
|
203
211
|
if (method === 'session/prompt') {
|
|
212
|
+
// A connection this prompt started has just reconciled the project files, and settled them unless that pass
|
|
213
|
+
// moved some (0.21.1+).
|
|
214
|
+
if (!started || !session.files.settled())
|
|
215
|
+
await session.files.reconcile();
|
|
204
216
|
record.prompted = true;
|
|
205
217
|
record.updatedAt = new Date().toISOString();
|
|
206
218
|
this.#save(record);
|
|
@@ -209,13 +221,23 @@ export class ShardfluxAcp {
|
|
|
209
221
|
return session.transport.rpc.notify(method, mapped);
|
|
210
222
|
let result;
|
|
211
223
|
try {
|
|
212
|
-
|
|
224
|
+
const answer = session.transport.rpc.request(method, mapped, method === 'session/prompt' ? 0 : 120_000);
|
|
225
|
+
// Without a saved machine this computer's agent config is copied in once the first prompt is on its way, never
|
|
226
|
+
// before it (0.21.1+), as for a terminal conversation.
|
|
227
|
+
if (first && session.ws && !(await savedMachine(this.#ctx).catch(() => undefined))?.template)
|
|
228
|
+
void syncAgentConfigAfterStart(this.#ctx, session.ws);
|
|
229
|
+
result = objectValue(await answer);
|
|
213
230
|
}
|
|
214
231
|
catch (e) {
|
|
215
232
|
if (method === 'session/prompt')
|
|
216
233
|
await this.#rejectModel(session, parseAcpModel(record.model).id, e);
|
|
217
234
|
throw e;
|
|
218
235
|
}
|
|
236
|
+
finally {
|
|
237
|
+
// T3 snapshots the local checkout as soon as the prompt response arrives.
|
|
238
|
+
if (method === 'session/prompt' && !this.#ctx.signal.aborted && !session.stop.signal.aborted)
|
|
239
|
+
await session.files.reconcile();
|
|
240
|
+
}
|
|
219
241
|
return this.#configuration(record.model, acpReferences(result, record, session.conversation.worktree_path), record.catalog);
|
|
220
242
|
}
|
|
221
243
|
#prepare(session) {
|
|
@@ -226,8 +248,15 @@ export class ShardfluxAcp {
|
|
|
226
248
|
// The native selected adapter must not wait for the other harness's model
|
|
227
249
|
// probe. Both are guest processes; only boot/repository preparation gates it.
|
|
228
250
|
session.prepared = new Promise(resolve => { prepared = resolve; });
|
|
251
|
+
// This computer's file hashes are read while the VM starts and seeds: the first prompt's reconcile then hashes only
|
|
252
|
+
// what changed since (0.21.1+). Best effort; the reconcile reads whatever is missing.
|
|
253
|
+
if (!session.hashes) {
|
|
254
|
+
session.hashes = new Map();
|
|
255
|
+
void localManifest(record.folder, ctx.signal, session.hashes).catch(() => undefined);
|
|
256
|
+
}
|
|
229
257
|
session.preparing = (async () => {
|
|
230
258
|
await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
|
|
259
|
+
const proxy = this.#proxy();
|
|
231
260
|
const catalog = await this.#lifecycle.prepare(ctx, record.folder, `acp-${record.id.slice(3).replaceAll('-', '')}`, {
|
|
232
261
|
allocated: (c, view) => {
|
|
233
262
|
session.conversation = c;
|
|
@@ -247,6 +276,7 @@ export class ShardfluxAcp {
|
|
|
247
276
|
},
|
|
248
277
|
ready: ws => { session.ws = ws; record.scope = acpScope(ctx.apiUrl, ws.data); this.#cache.remember(ctx, record.folder, record.scope); },
|
|
249
278
|
prepared: () => { session.seedReady = true; prepared(); },
|
|
279
|
+
proxy: { env: { ...proxy.env.claude, ...proxy.env.codex }, ready: ws => this.#proxyReady(ws, proxy) },
|
|
250
280
|
});
|
|
251
281
|
ctx.signal.throwIfAborted();
|
|
252
282
|
if (!record.scope)
|
|
@@ -329,25 +359,41 @@ export class ShardfluxAcp {
|
|
|
329
359
|
}
|
|
330
360
|
return session;
|
|
331
361
|
}
|
|
362
|
+
/** True when this call started the connection (and so reconciled the project files just now). */
|
|
332
363
|
async #connect(session) {
|
|
333
364
|
await session.prepared;
|
|
334
365
|
if (session.preparationError)
|
|
335
366
|
throw session.preparationError;
|
|
336
|
-
if (session.connecting)
|
|
337
|
-
|
|
367
|
+
if (session.connecting) {
|
|
368
|
+
await session.connecting;
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
338
371
|
if (session.transport && !session.transport.rpc.closed)
|
|
339
|
-
return;
|
|
372
|
+
return false;
|
|
340
373
|
session.connecting = this.#start(session).finally(() => { session.connecting = undefined; });
|
|
341
|
-
|
|
374
|
+
await session.connecting;
|
|
375
|
+
return true;
|
|
342
376
|
}
|
|
343
377
|
async #start(session) {
|
|
344
378
|
const ctx = this.#ctx, { record } = session, app = record.app ?? parseAcpModel(record.model).app;
|
|
379
|
+
// SHARDFLUX_TRACE_SEED: when each step of the connection ends (one line each, wall clock).
|
|
380
|
+
const mark = (name) => seedEvent(`acp_${name}`, Date.now());
|
|
381
|
+
mark('connect');
|
|
345
382
|
record.app = app;
|
|
346
383
|
await session.transport?.close();
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
384
|
+
// The user's T3 Code proxy setup, read again for every connection; the agent's variables apply when its process starts.
|
|
385
|
+
const proxy = this.#proxy();
|
|
386
|
+
// The sign-in is checked beside reading the conversation and its VM; it passes before anything starts in the VM. A
|
|
387
|
+
// proxy with its own credential needs no Shardflux Claude sign-in.
|
|
388
|
+
const signIn = (async () => {
|
|
389
|
+
await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
|
|
390
|
+
if (app === 'claude' && !proxy.claudeCredential && !(await cloudClaudeLogin(ctx))) {
|
|
391
|
+
throw new RpcError(-32000, proxy.importable.includes('claude') ? 'Authentication required: connect Claude Code, or move your CLIProxyAPI into your account (shard cli-proxy import).' : 'Authentication required: connect Claude Code.', { code: 'unauthenticated', fix: SIGN_IN });
|
|
392
|
+
}
|
|
393
|
+
})();
|
|
394
|
+
signIn.catch(() => undefined);
|
|
350
395
|
if (!record.conversationId) {
|
|
396
|
+
await signIn;
|
|
351
397
|
this.#prepare(session);
|
|
352
398
|
await session.prepared;
|
|
353
399
|
if (session.preparationError)
|
|
@@ -360,11 +406,38 @@ export class ShardfluxAcp {
|
|
|
360
406
|
session.conversation = await resumeConversation(ctx, current);
|
|
361
407
|
session.ws = await conversationWorkspace(ctx, session.conversation);
|
|
362
408
|
rememberConversation(ctx, session.conversation, record.folder);
|
|
409
|
+
mark('conversation');
|
|
363
410
|
}
|
|
411
|
+
await signIn;
|
|
412
|
+
mark('signed_in');
|
|
364
413
|
let c = session.conversation;
|
|
365
414
|
const cell = session.ws.cell();
|
|
366
|
-
|
|
415
|
+
const menubar = ensureMenubar(ctx.store.dir, ctx.apiUrl, new MenubarManager({ home: ctx.io.env.HOME, signal: ctx.signal }));
|
|
367
416
|
await restoreConversationBinding(ctx, c, session.ws);
|
|
417
|
+
mark('binding');
|
|
418
|
+
// The project files are reconciled, device approvals served and the supervisor launched side by side; the native
|
|
419
|
+
// session starts once the supervisor is up, and the connection is done (the prompt goes) once the files are
|
|
420
|
+
// reconciled too (0.21.1+).
|
|
421
|
+
const files = (async () => {
|
|
422
|
+
const boundary = await projectSyncBoundary({ ...ctx, signal: AbortSignal.any([ctx.signal, session.stop.signal]) }, session.ws, projectIdentity(record.folder), {
|
|
423
|
+
root: c.worktree_path, agent: record.id.replace(/^sf-/, 'acp-'), seed: false,
|
|
424
|
+
stateKey: `acp:${ctx.apiUrl}:${record.id}:${record.folder}`, ...(session.hashes ? { cache: session.hashes } : {}),
|
|
425
|
+
// Before its first prompt the VM holds what this computer seeded it with, so edits made here since are not conflicts.
|
|
426
|
+
vmBaseline: !record.prompted,
|
|
427
|
+
// Large media of a prepared start that are still on their way land by themselves (0.22.0+).
|
|
428
|
+
inFlight: () => preparedInFlight(ctx, c.id),
|
|
429
|
+
});
|
|
430
|
+
await boundary.reconcile();
|
|
431
|
+
mark('files');
|
|
432
|
+
return boundary;
|
|
433
|
+
})();
|
|
434
|
+
// The agent starts with the proxy setup's variables, once the VM files it reads are in place.
|
|
435
|
+
const env = proxy.env[app];
|
|
436
|
+
const launch = this.#proxyReady(session.ws, proxy).then(() => cell.exec.run(['sf-agent', 'acp', app, '--conversation', c.id, '--detach'], {
|
|
437
|
+
cwd: c.worktree_path, timeoutMs: 30_000, signal: ctx.signal, maxOutputBytes: 1 << 20, ...(Object.keys(env).length ? { env } : {})
|
|
438
|
+
}));
|
|
439
|
+
files.catch(() => undefined);
|
|
440
|
+
launch.catch(() => undefined);
|
|
368
441
|
session.approvals ??= await serveAcpDeviceApprovals(ctx.store.dir, c.id, async (approval) => {
|
|
369
442
|
const result = objectValue(await this.#client.request('session/request_permission', { sessionId: record.id,
|
|
370
443
|
toolCall: { toolCallId: approval.request_id, title: approval.summary, kind: 'execute', status: 'pending' },
|
|
@@ -372,15 +445,20 @@ export class ShardfluxAcp {
|
|
|
372
445
|
const outcome = objectValue(result.outcome);
|
|
373
446
|
return outcome.outcome === 'selected' && (outcome.optionId === 'once' || outcome.optionId === 'always') ? outcome.optionId : 'deny';
|
|
374
447
|
});
|
|
375
|
-
|
|
448
|
+
mark('approvals');
|
|
449
|
+
const launched = await launch;
|
|
450
|
+
mark('launched');
|
|
451
|
+
await menubar;
|
|
376
452
|
if (launched.exitCode !== 0)
|
|
377
453
|
throw new Error(`ACP supervisor could not attach: ${launched.stderr.trim()}`);
|
|
378
|
-
|
|
454
|
+
const [claimed, statusText] = await Promise.all([getConversation(ctx, c.id), cell.files.readText(`/home/user/.local/share/shardflux/acp/${c.id}/status.json`, { length: 8192 })]);
|
|
455
|
+
c = claimed;
|
|
456
|
+
mark('status');
|
|
379
457
|
if (c.harness !== app)
|
|
380
458
|
throw new Error('The guest did not claim this conversation harness.');
|
|
381
459
|
session.conversation = c;
|
|
382
460
|
rememberConversation(ctx, c, record.folder);
|
|
383
|
-
const status = JSON.parse(
|
|
461
|
+
const status = JSON.parse(statusText);
|
|
384
462
|
if (status.conversation_id !== c.id || status.harness !== app || !Number.isSafeInteger(status.pid) || status.pid <= 0 || !status.start || status.ended)
|
|
385
463
|
throw new Error('ACP supervisor identity does not match this conversation.');
|
|
386
464
|
const owner = `${session.ws.id}:${status.pid}:${status.start}`;
|
|
@@ -423,13 +501,16 @@ export class ShardfluxAcp {
|
|
|
423
501
|
};
|
|
424
502
|
session.transport = transport;
|
|
425
503
|
await transport.start();
|
|
504
|
+
mark('journal');
|
|
426
505
|
if (initialize) {
|
|
427
506
|
const initialized = objectValue(await transport.rpc.request('initialize', { protocolVersion: 1, clientInfo: { name: 'shardflux', version: CLI_VERSION }, clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false } }));
|
|
428
507
|
if (initialized.protocolVersion !== 1)
|
|
429
508
|
throw new Error('Agent protocol version changed');
|
|
509
|
+
mark('initialized');
|
|
430
510
|
const nativeId = c.harness_session_id ?? record.nativeId;
|
|
431
511
|
const result = objectValue(await transport.rpc.request(nativeId ? 'session/load' : 'session/new', { cwd: c.worktree_path, mcpServers: [{ name: 'shardflux', command: '/usr/local/bin/sf-agent', args: ['conversation', 'mcp'], env: [{ name: 'SF_CONVERSATION_ID', value: c.id }] }], ...(nativeId ? { sessionId: nativeId } : {}) }));
|
|
432
512
|
const id = result.sessionId ?? nativeId;
|
|
513
|
+
mark('session');
|
|
433
514
|
if (typeof id !== 'string' || !id)
|
|
434
515
|
throw new Error('Agent returned no native session id');
|
|
435
516
|
record.nativeId = id;
|
|
@@ -452,6 +533,31 @@ export class ShardfluxAcp {
|
|
|
452
533
|
this.#cacheSession(session);
|
|
453
534
|
await this.#modelUpdate(session);
|
|
454
535
|
}
|
|
536
|
+
// Not connected until the files are: a failed reconcile closes the journal, so the next use connects again.
|
|
537
|
+
try {
|
|
538
|
+
session.files = await files;
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
await session.transport.close();
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
mark('files_ready');
|
|
545
|
+
}
|
|
546
|
+
/** The T3 Code proxy setup as a conversation VM follows it. Its credentials are kept out of every message. */
|
|
547
|
+
#proxy() { return accountVmProxy(this.#ctx); }
|
|
548
|
+
/** Puts the proxy's VM files in place once per VM and setup (prepareVmProxy). */
|
|
549
|
+
#proxyReady(ws, plan) {
|
|
550
|
+
if (!plan.hubInfo && !plan.codexConfig)
|
|
551
|
+
return Promise.resolve();
|
|
552
|
+
const key = JSON.stringify([plan.hubInfo, plan.codexConfig]);
|
|
553
|
+
const done = this.#proxied.get(ws.id);
|
|
554
|
+
if (done?.key === key)
|
|
555
|
+
return done.ready;
|
|
556
|
+
const ready = prepareVmProxy(ws.cell(), plan);
|
|
557
|
+
this.#proxied.set(ws.id, { key, ready });
|
|
558
|
+
ready.catch(() => { if (this.#proxied.get(ws.id)?.ready === ready)
|
|
559
|
+
this.#proxied.delete(ws.id); });
|
|
560
|
+
return ready;
|
|
455
561
|
}
|
|
456
562
|
async #close(session) {
|
|
457
563
|
this.#sessions.delete(session.record.id);
|
package/dist/agent-commands.d.ts
CHANGED
|
@@ -7,13 +7,15 @@ import type { ModelChoice } from './agent-models.js';
|
|
|
7
7
|
export declare function disambiguatedSlug(slug: string, identity: string): string;
|
|
8
8
|
export declare function projectTemplate(ctx: Ctx, remote: string | null): Promise<string>;
|
|
9
9
|
/** `place` is the repository or folder name session titles end with ("todo-app"), `task` what a new session is titled
|
|
10
|
-
* with before the guest names it from its first prompt (a first prompt line or the conversation's title)
|
|
10
|
+
* with before the guest names it from its first prompt (a first prompt line or the conversation's title), and
|
|
11
|
+
* `launch.titlePrefix` the marker titles start with when this computer chose one ("" for none; 0.20.1+). */
|
|
11
12
|
export declare function agentPty(cell: CellClient, app: 'claude' | 'codex', agent: string, approvals: boolean, term: string, signal?: AbortSignal, place?: string, setupHash?: string, cwd?: string, ptyName?: string, onLaunch?: () => void, deferSetup?: boolean, conversation?: {
|
|
12
13
|
id: string;
|
|
13
14
|
session: string | null;
|
|
14
15
|
}, task?: string, launch?: {
|
|
15
16
|
choice?: ModelChoice;
|
|
16
17
|
help?: () => Promise<string>;
|
|
18
|
+
titlePrefix?: string;
|
|
17
19
|
}): Promise<PtySession>;
|
|
18
20
|
/** The first non-empty line of a prompt or title, bounded; the guest makes it the task part of the session title. */
|
|
19
21
|
export declare function sessionTask(text: string | undefined): string | undefined;
|
|
@@ -24,6 +26,7 @@ export declare function terminal(ctx: Ctx, ws: Workspace, app: 'claude' | 'codex
|
|
|
24
26
|
title?: string;
|
|
25
27
|
choice?: ModelChoice;
|
|
26
28
|
afterStart?: () => Promise<void> | undefined;
|
|
29
|
+
setupLabelled?: Promise<void>;
|
|
27
30
|
}): Promise<number>;
|
|
28
31
|
export declare const launchProject: Handler;
|
|
29
32
|
/** The project VM launch; with `--in t3` a first message becomes a thread on the T3 server in it (SSH mode, 0.17.0+). */
|
package/dist/agent-commands.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { linuxTerminalSession
|
|
1
|
+
import { linuxTerminalSession } from "./linux-terminal.js";
|
|
2
|
+
import { attachable, currentTerminal, handedToTerminal, openShardInTerminal, referenceFolder, reportTerminalOpen, storedTerminal } from "./terminal-apps.js";
|
|
2
3
|
import { repositoryName } from "./files-mirror.js";
|
|
3
|
-
import { prepareSetupCache } from "./setup-cache.js";
|
|
4
|
+
import { prepareSetupCache, recordSetupCacheFailure } from "./setup-cache.js";
|
|
4
5
|
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
5
6
|
import { agentReachability } from "./agent-reachability.js";
|
|
6
7
|
import { configureCredentialFill, watchCredentialPrompts } from "./credential-prompts.js";
|
|
@@ -21,13 +22,15 @@ import { preflightProjectSeed, projectWorkspacePath, seedAgentRepository, worksp
|
|
|
21
22
|
import { agentName } from "./agent-git.js";
|
|
22
23
|
import { cloudClaudeLogin, ensureCloudClaude, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
|
|
23
24
|
import { progress } from "./progress.js";
|
|
25
|
+
import { seedEvent, seedPhase } from "./seed-phases.js";
|
|
24
26
|
import { rememberLaunch } from "./agent-live.js";
|
|
25
27
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
26
28
|
import { installedTargets } from "./agent-targets.js";
|
|
27
29
|
import { openInFor } from "./t3-mode.js";
|
|
30
|
+
import { storedTitlePrefix } from "./client-settings.js";
|
|
28
31
|
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
29
|
-
import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
30
|
-
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
32
|
+
import { guestFirstPrompt, INBOX_MAX_CHARS, submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
33
|
+
import { conversationForOpen, conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
31
34
|
import { conversationClient } from "./conversation-commands.js";
|
|
32
35
|
import { conversationTerminal, reopenConversation } from "./conversation-terminal.js";
|
|
33
36
|
import { CHOICE_PENDING, choiceReport, hasChoice, launchChoice, modelChoice, runFlags } from "./agent-models.js";
|
|
@@ -53,7 +56,8 @@ export async function projectTemplate(ctx, remote) {
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
/** `place` is the repository or folder name session titles end with ("todo-app"), `task` what a new session is titled
|
|
56
|
-
* with before the guest names it from its first prompt (a first prompt line or the conversation's title)
|
|
59
|
+
* with before the guest names it from its first prompt (a first prompt line or the conversation's title), and
|
|
60
|
+
* `launch.titlePrefix` the marker titles start with when this computer chose one ("" for none; 0.20.1+). */
|
|
57
61
|
export async function agentPty(cell, app, agent, approvals, term, signal, place, setupHash, cwd, ptyName, onLaunch, deferSetup = false, conversation, task, launch) {
|
|
58
62
|
const id = `shard-${ptyName || agent}-${app}`;
|
|
59
63
|
// Arrival and changed locks queue the same durable guest job even when the
|
|
@@ -81,7 +85,8 @@ export async function agentPty(cell, app, agent, approvals, term, signal, place,
|
|
|
81
85
|
}
|
|
82
86
|
// A model and effort choice reaches only a guest whose run accepts it (the caller checked).
|
|
83
87
|
const started = await cell.exec.run(['sf-agent', 'run', app, '--name', agent, '--detach', ...conversationArgs, ...(ptyName ? ['--pty-name', ptyName] : []), ...(approvals ? ['--approvals'] : []), ...(setupHash ? ['--setup', '--setup-deps-hash', setupHash] : []), ...runFlags(launch?.choice)], {
|
|
84
|
-
|
|
88
|
+
// SF_TITLE_PREFIX: the chosen marker for a launch without a conversation record (a guest before it ignores it).
|
|
89
|
+
env: { TERM: term, ...(place ? { SF_PROJECT_NAME: place, SF_REPO_NAME: place } : {}), ...(task ? { SF_TASK_TITLE: task } : {}), ...(launch?.titlePrefix !== undefined ? { SF_TITLE_PREFIX: launch.titlePrefix } : {}), ...(setupHash && deferSetup ? { SF_SETUP_DEFERRED: '1' } : {}) }, timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}),
|
|
85
90
|
});
|
|
86
91
|
if (started.exitCode !== 0)
|
|
87
92
|
throw new Error(`agent launch failed: ${started.stderr.trim() || `exit ${started.exitCode}`}`);
|
|
@@ -122,7 +127,12 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
122
127
|
if (app === 'codex' && typeof values.initialPrompt === 'string') {
|
|
123
128
|
await prepareInitialCodexProject(cell, cwd, `shard-${ptyName || agent}-${app}`);
|
|
124
129
|
}
|
|
125
|
-
|
|
130
|
+
// Credential fill serves the agent's later credential prompts: configured beside the first prompt, never before it.
|
|
131
|
+
const credentialFill = configureCredentialFill(ctx, ws).catch((error) => {
|
|
132
|
+
if (!ctx.signal.aborted)
|
|
133
|
+
ctx.io.stderr.write(`Credential fill is off for this session: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
134
|
+
return false;
|
|
135
|
+
});
|
|
126
136
|
// This command's --model/--effort, else the conversation's own (a relaunch keeps its model). A computer whose guest
|
|
127
137
|
// predates the choice starts the agent with its defaults, and this command says so once.
|
|
128
138
|
const help = runHelp(cell, app, ctx.signal), explicit = modelChoice(values, app);
|
|
@@ -131,16 +141,21 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
131
141
|
ctx.io.stderr.write(`${CHOICE_PENDING}\n`);
|
|
132
142
|
// A cold launch is remembered before it starts, so the listing can show it before the projection does (agent-live.ts).
|
|
133
143
|
const term = ctx.io.env.TERM && ctx.io.env.TERM !== 'dumb' ? ctx.io.env.TERM : 'xterm-256color';
|
|
134
|
-
const
|
|
144
|
+
const titlePrefix = storedTitlePrefix(ctx.store.dir);
|
|
145
|
+
const session = await agentPty(cell, app, agent, values.approvals === true, term, ctx.signal, repositoryName(ws.labels.repo_remote || project), setupHash, cwd, ptyName, () => { rememberLaunch(ctx, ws.id); progress(ctx, `Starting ${app === 'claude' ? 'Claude' : 'Codex'} in ${ws.key}…`); }, true, conversation && { id: conversation.id, session: conversation.session }, sessionTask(typeof values.initialPrompt === 'string' ? values.initialPrompt : conversation?.title), { ...(choice ? { choice } : {}), help, ...(titlePrefix !== undefined ? { titlePrefix } : {}) });
|
|
146
|
+
seedPhase('harness_launch');
|
|
147
|
+
// When the harness's terminal started, by the VM's clock (the guest may start it before this command asks).
|
|
148
|
+
if (session.created_at)
|
|
149
|
+
seedEvent('harness_started', Date.parse(session.created_at));
|
|
135
150
|
// A new conversation's guest can start the agent before this command reaches it, so the launch above never ran.
|
|
136
151
|
// Remember it either way: listing and `agents open` then read the guest until the projection lists the session.
|
|
137
152
|
rememberLaunch(ctx, ws.id);
|
|
138
153
|
// The real agent is running before any cache provisioning or mount wait.
|
|
139
|
-
const preparation = prepareSetupCache(ctx, ws).catch((error) => {
|
|
140
|
-
ctx.io.stderr.write(`Account setup cache unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
141
|
-
}).then(async () => {
|
|
154
|
+
const preparation = prepareSetupCache(ctx, ws).catch((error) => recordSetupCacheFailure(ctx, ws, error)).then(async () => {
|
|
142
155
|
if (!setupHash)
|
|
143
156
|
return;
|
|
157
|
+
// A new conversation's VM carries the setup labels before its setup starts (0.21.1+).
|
|
158
|
+
await conversation?.setupLabelled;
|
|
144
159
|
const queued = await cell.exec.run(['sf-agent', 'setup-start', '--deps-hash', setupHash], { cwd, timeoutMs: 30_000, maxOutputBytes: 4096, signal: ctx.signal });
|
|
145
160
|
if (queued.exitCode !== 0)
|
|
146
161
|
throw new Error(`project setup could not be queued: ${queued.stderr.trim() || `exit ${queued.exitCode}`}`);
|
|
@@ -151,12 +166,22 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
151
166
|
const text = values.initialPrompt;
|
|
152
167
|
// A conversation's guest delivers the prompt itself when this terminal never showed its input box (0.19.1+).
|
|
153
168
|
const queue = conversation ? () => conversationClient(ctx).conversations.send(conversation.id, text, { signal: ctx.signal }) : undefined;
|
|
154
|
-
|
|
169
|
+
// The VM's guest types it where it can (0.21.1+); this terminal types it for an older guest. A harness the guest
|
|
170
|
+
// waited for without seeing its input box gets it through the inbox, not a second wait here.
|
|
171
|
+
const guest = await guestFirstPrompt(cell, session.session_id, app, text, ws.key, ctx.signal);
|
|
172
|
+
if (guest === 'submitted')
|
|
173
|
+
delivery = 'submitted';
|
|
174
|
+
else if (guest === 'not_ready' && queue && text.length <= INBOX_MAX_CHARS) {
|
|
175
|
+
await queue();
|
|
176
|
+
delivery = 'queued';
|
|
177
|
+
}
|
|
178
|
+
else
|
|
179
|
+
delivery = await submitInitialPrompt(signal => cellConnector(cell, session.session_id)(0, signal), text, ws.key, ctx.signal, app, bytes => cell.pty.input(session.session_id, bytes), queue);
|
|
155
180
|
}
|
|
156
181
|
// Work that must never delay the first prompt (the laptop's agent config, 0.19.0+) starts once the agent has it.
|
|
157
182
|
const after = conversation?.afterStart?.();
|
|
158
183
|
if (!attach) {
|
|
159
|
-
await preparation;
|
|
184
|
+
await Promise.all([preparation, credentialFill]);
|
|
160
185
|
// conversation (0.16.0+): which conversation this session is, also when its computer hosts several threads.
|
|
161
186
|
// model_choice (0.17.0+): the requested model and effort, and whether this thread started with them.
|
|
162
187
|
ctx.io.stdout.write(json({ workspace: ws.data, session, ...(conversation ? { conversation: { id: conversation.id } } : {}), ...(delivery ? { initial_prompt: delivery === 'submitted' ? { submitted: true } : { submitted: false, queued: true } } : {}),
|
|
@@ -166,7 +191,7 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
166
191
|
}
|
|
167
192
|
ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
|
|
168
193
|
const attached = attachTerminal({
|
|
169
|
-
...(credentialFill ? { credentialPrompts: (ask, signal) => watchCredentialPrompts(ctx, ws, ask, signal) } : {}),
|
|
194
|
+
...(await credentialFill ? { credentialPrompts: (ask, signal) => watchCredentialPrompts(ctx, ws, ask, signal) } : {}),
|
|
170
195
|
io: ctx.io, signal: ctx.signal, connect: cellConnector(cell, session.session_id, { passive: true }),
|
|
171
196
|
wake: (signal) => ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal }),
|
|
172
197
|
asleep: () => workspaceAsleep(ws),
|
|
@@ -294,18 +319,72 @@ export async function launchProjectWith(ctx, values, [path = './', explicitApp],
|
|
|
294
319
|
await prepareAgentMachine(ctx, ws);
|
|
295
320
|
return await terminal(ctx, ws, app, agent, projectKey, values, prepared?.setupHash);
|
|
296
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Whether `agents open` hands the conversation to the person's terminal app (0.20.1+) rather than attaching here:
|
|
324
|
+
* `--in terminal` with --json (an app) or `--window`, any open from a shell nobody types into (a coding agent's, or no
|
|
325
|
+
* terminal), and `--in terminal` typed in a terminal other than the chosen one.
|
|
326
|
+
*/
|
|
327
|
+
function opensInTerminalApp(ctx, values) {
|
|
328
|
+
// A terminal this CLI opened for the conversation attaches there, never in yet another one (0.23.0+).
|
|
329
|
+
if (values.detach === true || handedToTerminal(ctx.io.env))
|
|
330
|
+
return false;
|
|
331
|
+
const here = attachable(ctx.io);
|
|
332
|
+
if (values.in === undefined)
|
|
333
|
+
return !ctx.json && !here;
|
|
334
|
+
if (values.in !== 'terminal')
|
|
335
|
+
return false;
|
|
336
|
+
if (ctx.json || !here || values.window === true)
|
|
337
|
+
return true;
|
|
338
|
+
const stored = storedTerminal(ctx.store.dir);
|
|
339
|
+
return !!stored && stored.id !== currentTerminal(ctx.io.env);
|
|
340
|
+
}
|
|
341
|
+
/** `shard agents open <ref>` (its attach, no --in) in a new tab of the person's terminal app, in the project folder. */
|
|
342
|
+
async function openInTerminalApp(ctx, ref, values, extra = {}) {
|
|
343
|
+
const words = ['agents', 'open', ref, ...(typeof values.app === 'string' ? ['--app', values.app] : [])];
|
|
344
|
+
const result = await openShardInTerminal(ctx, words, { cwd: referenceFolder(ctx.store.dir, ctx.apiUrl, ref, ctx.io.env), window: values.window === true });
|
|
345
|
+
reportTerminalOpen(ctx, result, extra);
|
|
346
|
+
return 0;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* `--in auto` for a terminal conversation: the person's terminal app (a tab there, 0.20.1+), or this terminal when it
|
|
350
|
+
* is the one they use. --json without a chosen terminal app keeps the command for an app to run (a menu bar before
|
|
351
|
+
* 0.20.1 opens Terminal with it).
|
|
352
|
+
*/
|
|
353
|
+
function terminalAuto(ctx, ws, opened, values) {
|
|
354
|
+
if (ctx.json && !storedTerminal(ctx.store.dir)) {
|
|
355
|
+
out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
|
|
356
|
+
return Promise.resolve(0);
|
|
357
|
+
}
|
|
358
|
+
const next = { ...values, in: 'terminal' };
|
|
359
|
+
if (opensInTerminalApp(ctx, next))
|
|
360
|
+
return openInTerminalApp(ctx, opened, next, { workspace: ws.data });
|
|
361
|
+
delete next.in;
|
|
362
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
|
|
363
|
+
}
|
|
297
364
|
export const AGENT_HANDLERS = {
|
|
298
365
|
async 'agents open'(ctx, values, [agent = 'main']) {
|
|
299
366
|
if (values.app !== undefined && values.app !== 'claude' && values.app !== 'codex')
|
|
300
367
|
throw new UsageError('--app must be claude or codex');
|
|
301
|
-
|
|
302
|
-
|
|
368
|
+
// A Codex Cloud conversation opens its chat in Codex, never in a terminal (0.23.0+): it is resolved before any
|
|
369
|
+
// terminal hand-off, by its id or by its computer's key, which menus before Linux 0.1.9 and menu bar 0.8.4 pass.
|
|
370
|
+
// Nimblefox (2026-10-13): the hand-off came first, and from a Codex shell each new terminal opened another one.
|
|
371
|
+
// The person's terminal app (0.20.1+): after that lookup, the new tab owns the attach, credentials and prompts; a
|
|
372
|
+
// lookup this shell cannot make (signed out here, offline) leaves the hand-off to it.
|
|
373
|
+
const handOff = opensInTerminalApp(ctx, values);
|
|
303
374
|
let resolvedConversation;
|
|
304
|
-
if (/^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
375
|
+
if (/^conv_[a-z0-9]{26}$/.test(agent) || agent.includes('/')) {
|
|
376
|
+
try {
|
|
377
|
+
resolvedConversation = await conversationForOpen(ctx, agent);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (!handOff)
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
308
383
|
}
|
|
384
|
+
if (resolvedConversation?.route === 'codex-cloud')
|
|
385
|
+
return openCodexConversation(ctx, resolvedConversation, values.original === true ? true : values.history);
|
|
386
|
+
if (handOff)
|
|
387
|
+
return openInTerminalApp(ctx, agent, values);
|
|
309
388
|
if (values.in === 'auto') {
|
|
310
389
|
// The menu and other launchers use the same CLI-owned target selection. Never put provider URLs in a client.
|
|
311
390
|
// A conversation id opens that conversation's own session, also when its computer hosts other threads.
|
|
@@ -330,20 +409,16 @@ export const AGENT_HANDLERS = {
|
|
|
330
409
|
}
|
|
331
410
|
if (chosen === 'claude-desktop' && app === 'claude')
|
|
332
411
|
return openClaudeDesktopSession(ctx, ws, resolvedConversation);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
return 0;
|
|
337
|
-
}
|
|
338
|
-
const next = { ...values };
|
|
339
|
-
delete next.in;
|
|
340
|
-
return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
|
|
341
|
-
}
|
|
412
|
+
// A terminal app attaches to the conversation's own session, Claude Code's or Codex's (Codex 0.21.1+).
|
|
413
|
+
if (chosen === 'terminal' && (app === 'claude' || app === 'codex'))
|
|
414
|
+
return terminalAuto(ctx, ws, opened, values);
|
|
342
415
|
const saved = chosen === 'codex-app' ? undefined : store.read().projects?.[project]?.target ??
|
|
343
416
|
(project.startsWith('file://') ? store.read().projects?.[new URL(project).pathname]?.target : undefined);
|
|
344
417
|
// Codex's default open implementation owns Cloud/desktop routing. PAS-XB replaces that path; do not pin
|
|
345
418
|
// the menu to the old per-VM SSH target. An explicit saved desktop target still takes precedence.
|
|
346
419
|
if ((!saved || !detected.targets.includes(saved)) && app === 'codex') {
|
|
420
|
+
if (resolvedConversation?.route === 'terminal')
|
|
421
|
+
return terminalAuto(ctx, ws, opened, values);
|
|
347
422
|
const next = { ...values };
|
|
348
423
|
delete next.in;
|
|
349
424
|
return AGENT_HANDLERS['agents open'](ctx, next, [resolvedConversation?.id ?? ws.key]);
|
|
@@ -353,21 +428,14 @@ export const AGENT_HANDLERS = {
|
|
|
353
428
|
const handler = HANDLERS[`agents open ${saved}`];
|
|
354
429
|
if (!handler)
|
|
355
430
|
throw new UsageError('Choose an installed open target with shard agents targets.');
|
|
356
|
-
return handler(ctx, { ...values, in: saved }, [
|
|
431
|
+
return handler(ctx, { ...values, in: saved }, [opened]);
|
|
357
432
|
}
|
|
358
433
|
// Claude opens where people work (0.15.1+): the conversation in Claude Desktop when it is installed, else Claude
|
|
359
434
|
// Code in a terminal. The browser (claude.ai) only when it is the chosen target (--in web).
|
|
360
435
|
if (detected.clients.includes('claude-desktop')) {
|
|
361
436
|
return openClaudeDesktopSession(ctx, ws, resolvedConversation);
|
|
362
437
|
}
|
|
363
|
-
|
|
364
|
-
// An app runs this in the person's terminal (the menu bar opens Terminal with it).
|
|
365
|
-
out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
|
|
366
|
-
return 0;
|
|
367
|
-
}
|
|
368
|
-
const next = { ...values };
|
|
369
|
-
delete next.in;
|
|
370
|
-
return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
|
|
438
|
+
return terminalAuto(ctx, ws, opened, values);
|
|
371
439
|
}
|
|
372
440
|
if (values.in !== undefined && values.in !== 'terminal') {
|
|
373
441
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -379,10 +447,10 @@ export const AGENT_HANDLERS = {
|
|
|
379
447
|
await syncCredentials(ctx);
|
|
380
448
|
let ws;
|
|
381
449
|
let conversation;
|
|
382
|
-
if (/^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
450
|
+
if (resolvedConversation || /^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
383
451
|
conversation = resolvedConversation ?? await getConversation(ctx, agent);
|
|
384
452
|
if (conversation.route === 'codex-cloud')
|
|
385
|
-
return openCodexConversation(ctx, conversation);
|
|
453
|
+
return openCodexConversation(ctx, conversation, values.original === true ? true : values.history);
|
|
386
454
|
conversation = await resumeConversation(ctx, conversation);
|
|
387
455
|
ws = await conversationWorkspace(ctx, conversation);
|
|
388
456
|
}
|
|
@@ -415,7 +483,7 @@ export const AGENT_HANDLERS = {
|
|
|
415
483
|
if (matches.length !== 1)
|
|
416
484
|
throw new UsageError('Use the conversation id.');
|
|
417
485
|
if (matches[0].route === 'codex-cloud')
|
|
418
|
-
return openCodexConversation(ctx, matches[0]);
|
|
486
|
+
return openCodexConversation(ctx, matches[0], values.original === true ? true : values.history);
|
|
419
487
|
conversation = await resumeConversation(ctx, matches[0]);
|
|
420
488
|
ws = await conversationWorkspace(ctx, conversation);
|
|
421
489
|
}
|
|
@@ -423,7 +491,7 @@ export const AGENT_HANDLERS = {
|
|
|
423
491
|
if (!conversation && ws.labels.shard_conversation) {
|
|
424
492
|
conversation = await getConversation(ctx, ws.labels.shard_conversation);
|
|
425
493
|
if (conversation.route === 'codex-cloud')
|
|
426
|
-
return openCodexConversation(ctx, conversation);
|
|
494
|
+
return openCodexConversation(ctx, conversation, values.original === true ? true : values.history);
|
|
427
495
|
conversation = await resumeConversation(ctx, conversation);
|
|
428
496
|
ws = await conversationWorkspace(ctx, conversation);
|
|
429
497
|
}
|
|
@@ -2,6 +2,8 @@ import type { CellClient } from '@shardflux/sdk';
|
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
3
|
import type { Conversation, LocalConversation } from './conversations.js';
|
|
4
4
|
export declare const RETURN_ROOT = "/shardflux/project/agent-config";
|
|
5
|
+
/** The latest agent-config line ({notice, at}), for the Linux app's top-bar panel (CLI 0.20.2+). */
|
|
6
|
+
export declare const AGENT_CONFIG_NOTICE = "agent-config-notice.json";
|
|
5
7
|
/** A machine fold waits for this much quiet after the last change, and saves the machine at most this often. */
|
|
6
8
|
export declare const FOLD_QUIET_MS = 60000;
|
|
7
9
|
export declare const FOLD_INTERVAL_MS: number;
|
|
@@ -116,8 +118,50 @@ export declare class AgentConfigReturnState {
|
|
|
116
118
|
}
|
|
117
119
|
/** How often the device helper looks for agent config to bring back: its own loop, never behind a files or transcript pass. */
|
|
118
120
|
export declare const AGENT_CONFIG_POLL_MS = 2000;
|
|
119
|
-
/**
|
|
120
|
-
export declare
|
|
121
|
+
/** With none of this user's conversations active, the full listing backs off (doubling) to this. */
|
|
122
|
+
export declare const AGENT_CONFIG_IDLE_MS = 60000;
|
|
123
|
+
/** While backed off, one cheap request (`state=working`, usually empty) this often notices a turn starting anywhere. */
|
|
124
|
+
export declare const AGENT_CONFIG_PROBE_MS = 10000;
|
|
125
|
+
/** A conversation counts as active for this long after its last activity (a turn's start or end). */
|
|
126
|
+
export declare const AGENT_CONFIG_ACTIVE_WINDOW_MS: number;
|
|
127
|
+
/**
|
|
128
|
+
* A working conversation counts as active only this long after its last activity: one left "working" by a VM that went
|
|
129
|
+
* away (staging QA FE1: 60 such, all more than a day old) must not hold every computer at the fast cadence. A longer
|
|
130
|
+
* turn still comes back, at the idle cadence.
|
|
131
|
+
*/
|
|
132
|
+
export declare const AGENT_CONFIG_WORKING_WINDOW_MS: number;
|
|
133
|
+
/** This user's conversations that are working (recently) or ended a turn within the active window. */
|
|
134
|
+
export declare function activeConversations(all: Conversation[], records: Record<string, LocalConversation>, me: string | undefined, now?: number): Conversation[];
|
|
135
|
+
export interface CadenceOptions {
|
|
136
|
+
activeMs: number;
|
|
137
|
+
idleMs: number;
|
|
138
|
+
probeMs: number;
|
|
139
|
+
}
|
|
140
|
+
export declare const CADENCE: CadenceOptions;
|
|
141
|
+
/**
|
|
142
|
+
* When the next listing is due. Active (one of this user's conversations works or just ended a turn): every 2 s.
|
|
143
|
+
* Idle: the interval doubles to 60 s, with a cheap working-only probe every 10 s and a check of this computer's own
|
|
144
|
+
* conversation records each second; either snaps it back to 2 s at once.
|
|
145
|
+
*/
|
|
146
|
+
export declare class AgentConfigCadence {
|
|
147
|
+
readonly options: CadenceOptions;
|
|
148
|
+
interval: number;
|
|
149
|
+
fullAt: number;
|
|
150
|
+
probeAt: number;
|
|
151
|
+
recordsMtime: number;
|
|
152
|
+
constructor(options?: CadenceOptions);
|
|
153
|
+
/** After a full pass: active keeps 2 s, idle doubles toward the idle ceiling. */
|
|
154
|
+
listed(active: boolean, now?: number): void;
|
|
155
|
+
snap(): void;
|
|
156
|
+
fullDue(now?: number): boolean;
|
|
157
|
+
probeDue(now?: number): boolean;
|
|
158
|
+
}
|
|
159
|
+
/** One full pass: list the account's conversations, apply what came back; true while one of this user's is active. */
|
|
160
|
+
export declare function returnAgentConfigsOnce(ctx: Ctx, state: AgentConfigReturnState): Promise<boolean>;
|
|
161
|
+
/** The cheap probe: is any of this user's conversations working right now (one request, usually an empty page)? */
|
|
162
|
+
export declare function workingProbe(ctx: Ctx, state: AgentConfigReturnState): Promise<boolean>;
|
|
163
|
+
/** The device helper's agent-config loop: activity-driven cadence, its own backoff on errors, never another loop's. */
|
|
164
|
+
export declare function runAgentConfigLoop(ctx: Ctx, state: AgentConfigReturnState, signal: AbortSignal, cadence?: AgentConfigCadence, tickMs?: number): Promise<void>;
|
|
121
165
|
/**
|
|
122
166
|
* This computer's checkout of a conversation's repository: the folder it was started from here, else the folder of
|
|
123
167
|
* another conversation of the same repository started here. A Shardflux project can hold many repositories (staging
|