@shardflux/cli 0.18.0 → 0.18.1
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 +16 -1
- package/README.md +1 -1
- package/dist/agent-commands.js +7 -2
- package/dist/agent-target-commands.js +10 -3
- package/dist/args.d.ts +1 -1
- package/dist/args.js +3 -3
- package/dist/command-center-actions.js +31 -5
- package/dist/conversation-commands.js +6 -1
- package/dist/conversation-terminal.d.ts +10 -0
- package/dist/conversation-terminal.js +22 -0
- package/dist/conversations.d.ts +8 -1
- package/dist/conversations.js +35 -0
- package/dist/t3-launch.js +3 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.18.
|
|
3
|
+
## 0.18.1 (not yet published)
|
|
4
|
+
|
|
5
|
+
- `shard agents logs` takes a conversation id (`shard agents logs conv_… --json`) and follows that conversation's own
|
|
6
|
+
terminal: the PTY its computer recorded for it (a fork's inherited one included), also when you name the
|
|
7
|
+
conversation's VM by its key. `--json` adds `conversation: { id }`. Before, a conversation id read as an unknown
|
|
8
|
+
workspace key and a conversation VM's key looked for a terminal named after the VM.
|
|
9
|
+
- A finished conversation whose VM was removed opens again from every app: `shard agents open <conversation>` with
|
|
10
|
+
`--in auto` (the menu bar), `--in web`, `--in claude-desktop` or `--in t3` restores it into a fresh VM from its saved
|
|
11
|
+
branch and transcript (the same conversation, its session resumed and started) instead of answering `This
|
|
12
|
+
conversation is saved. Resume it to continue.` A restore says `Restored` once. `shard conversations send` says it
|
|
13
|
+
too when the API restored the conversation to take the message.
|
|
14
|
+
- Brings the Shardflux app for Ubuntu 0.1.1, which reads this CLI's `--json` answers whole: Settings → Agents shows
|
|
15
|
+
Connected, the top-bar panel lists your conversations, and Start opens the conversation it started. The optional
|
|
16
|
+
dependency on `@shardflux/desktop-linux-x64` / `-arm64` is exactly 0.1.1.
|
|
17
|
+
|
|
18
|
+
## 0.18.0 (2026-10-10)
|
|
4
19
|
|
|
5
20
|
- A conversation that needs you says why: `shard conversations list` adds it as a fifth column, `shard conversations
|
|
6
21
|
get` prints it after the branch, and `shard agents list` shows it as the session's LAST MESSAGE (one line, cut to
|
package/README.md
CHANGED
|
@@ -428,7 +428,7 @@ plan only when Stripe confirms the payment, so `--wait` polls until the subscrip
|
|
|
428
428
|
open). An organization that already has a subscription changes plans in the billing portal: the
|
|
429
429
|
command prints the portal URL instead (exit 0).
|
|
430
430
|
|
|
431
|
-
On the signup trial (
|
|
431
|
+
On the signup trial (30 days of Pro, no card), `billing status` shows when it ends and
|
|
432
432
|
what follows (0.6.2+): `plan trial, ends 2026-11-29 14:03:00Z, then free (trial, catalog ...)`, then
|
|
433
433
|
`keep these limits after the trial: shard billing upgrade pro` (0.13.4+). A subscription outranks the
|
|
434
434
|
trial and ends it. The trial is granted, never bought, so `billing plans` leaves it out.
|
package/dist/agent-commands.js
CHANGED
|
@@ -28,7 +28,7 @@ import { openInFor } from "./t3-mode.js";
|
|
|
28
28
|
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
29
29
|
import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
30
30
|
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
31
|
-
import { conversationTerminal } from "./conversation-terminal.js";
|
|
31
|
+
import { conversationTerminal, reopenConversation } from "./conversation-terminal.js";
|
|
32
32
|
import { CHOICE_PENDING, choiceReport, hasChoice, launchChoice, modelChoice, runFlags } from "./agent-models.js";
|
|
33
33
|
const active = (session) => session.state === 'running' || session.state === 'starting';
|
|
34
34
|
/** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
|
|
@@ -299,7 +299,12 @@ export const AGENT_HANDLERS = {
|
|
|
299
299
|
if (values.in === 'auto') {
|
|
300
300
|
// The menu and other launchers use the same CLI-owned target selection. Never put provider URLs in a client.
|
|
301
301
|
// A conversation id opens that conversation's own session, also when its computer hosts other threads.
|
|
302
|
-
|
|
302
|
+
// A conversation whose VM was removed restores into a fresh one first (0.18.1+).
|
|
303
|
+
let ws;
|
|
304
|
+
if (resolvedConversation)
|
|
305
|
+
({ conversation: resolvedConversation, ws } = await reopenConversation(ctx, resolvedConversation, values));
|
|
306
|
+
else
|
|
307
|
+
ws = await resolveWorkspace(ctx, agent);
|
|
303
308
|
const project = ws.labels.repo_remote || ws.labels.shard_project || ws.key;
|
|
304
309
|
const store = new AgentStateStore(ctx.store.dir);
|
|
305
310
|
const app = values.app ?? resolvedConversation?.harness ?? ws.labels.shard_app ?? ws.labels.app ?? store.app(project);
|
|
@@ -5,7 +5,7 @@ import { ensureCloudClaude } from "./claude-cloud-login.js";
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { Readable, Duplex } from 'node:stream';
|
|
7
7
|
import { ShardfluxApiError, WorkspaceTunnels } from '@shardflux/sdk';
|
|
8
|
-
import {
|
|
8
|
+
import { getConversation } from "./conversations.js";
|
|
9
9
|
import { out, resolveWorkspace } from "./commands.js";
|
|
10
10
|
import { MODEL_CHOICES } from "./agent-models.js";
|
|
11
11
|
import { AgentTargets, installedTargets, targetLabel } from "./agent-targets.js";
|
|
@@ -229,14 +229,21 @@ const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
|
|
|
229
229
|
if (target === 't3' && /^conv_[a-z0-9]{26}$/.test(ref))
|
|
230
230
|
return openT3Conversation(ctx, ref, values);
|
|
231
231
|
// A conversation id opens that conversation's own session on its computer (claude.ai, the desktop apps).
|
|
232
|
-
|
|
232
|
+
let conversation = /^conv_[a-z0-9]{26}$/.test(ref) ? await getConversation(ctx, ref) : undefined;
|
|
233
233
|
// A pooled thread (0.18.0+) has no provider login on its session: it opens remotely in T3 Code.
|
|
234
234
|
if (conversation && conversationPooled(conversation)) {
|
|
235
235
|
if (!detected.targets.includes('t3'))
|
|
236
236
|
throw pooledNeedsT3(ref);
|
|
237
237
|
return openT3Conversation(ctx, ref, values);
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
// A conversation whose VM was removed restores into a fresh one first (0.18.1+).
|
|
240
|
+
let ws;
|
|
241
|
+
if (conversation) {
|
|
242
|
+
const { reopenConversation } = await import("./conversation-terminal.js");
|
|
243
|
+
({ conversation, ws } = await reopenConversation(ctx, conversation, values));
|
|
244
|
+
}
|
|
245
|
+
else
|
|
246
|
+
ws = await resolveWorkspace(ctx, keyOf(ref));
|
|
240
247
|
if (values.reachable !== undefined)
|
|
241
248
|
ws = await ctx.cloud().workspaces.open({ key: ws.key, ...agentReachability(values), wait: false });
|
|
242
249
|
// T3 lists both agents and opens without Claude (it shows Claude as signed out); the Claude apps need it.
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -13,7 +13,7 @@ import { parseArgs } from 'node:util';
|
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
15
|
import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
|
|
16
|
-
export const CLI_VERSION = '0.18.
|
|
16
|
+
export const CLI_VERSION = '0.18.1';
|
|
17
17
|
export const GLOBAL_OPTIONS = {
|
|
18
18
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
19
19
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -1288,8 +1288,8 @@ const AGENT_COMMANDS = [
|
|
|
1288
1288
|
} },
|
|
1289
1289
|
{ path: ['agents', 'fork'], summary: 'Fork an agent’s memory and disk', positionals: [{ name: 'agent', required: true }, { name: 'name', required: true }], options: WAIT },
|
|
1290
1290
|
{ path: ['agents', 'park'], summary: 'Suspend an agent when idle', positionals: [{ name: 'agent', required: true }], options: { 'when-idle': { type: 'string', value: '<duration>', description: 'Idle grace before suspend (default 1s).' } } },
|
|
1291
|
-
{ path: ['agents', 'logs'], summary: 'Tail an agent
|
|
1292
|
-
app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' },
|
|
1291
|
+
{ path: ['agents', 'logs'], summary: 'Tail an agent’s terminal from its output offset: a conversation id (0.18.1+) or an agent workspace key', positionals: [{ name: 'agent', required: true }], options: {
|
|
1292
|
+
app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice; a conversation uses its own).' },
|
|
1293
1293
|
offset: { type: 'string', value: '<bytes>', description: 'Read from this output byte offset (default last 64 KiB).' },
|
|
1294
1294
|
follow: { type: 'boolean', description: 'Follow output until interrupted.' },
|
|
1295
1295
|
} },
|
|
@@ -11,9 +11,11 @@ import { agentName as gitAgentName } from "./agent-git.js";
|
|
|
11
11
|
import { randomUUID } from 'node:crypto';
|
|
12
12
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
13
13
|
import { OperationFailedError } from '@shardflux/sdk';
|
|
14
|
+
import { conversationPtyId, getConversation } from "./conversations.js";
|
|
14
15
|
import { launchConversationProject, launchProjectCommand } from "./conversation-terminal.js";
|
|
15
16
|
import { startThreadOn } from "./thread-on-computer.js";
|
|
16
17
|
import { launchT3 } from "./t3-launch.js";
|
|
18
|
+
const CONVERSATION_ID = /^conv_[a-z0-9]{26}$/;
|
|
17
19
|
/** L9 can register target-specific handlers after detecting the installed applications. */
|
|
18
20
|
export async function availableTargets(ctx) {
|
|
19
21
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -124,14 +126,38 @@ export const CENTER_HANDLERS = {
|
|
|
124
126
|
},
|
|
125
127
|
async 'agents logs'(ctx, values, [ref]) {
|
|
126
128
|
if (!ref)
|
|
127
|
-
throw new UsageError('agents logs requires an agent workspace key');
|
|
129
|
+
throw new UsageError('agents logs requires a conversation id or an agent workspace key');
|
|
128
130
|
if (values.app !== undefined && values.app !== 'claude' && values.app !== 'codex')
|
|
129
131
|
throw new UsageError('--app must be claude or codex');
|
|
130
|
-
|
|
132
|
+
// A conversation (0.18.1+): its id, or its VM's key. Its PTY is the guest's record, never derived from the VM key.
|
|
133
|
+
let conversation;
|
|
134
|
+
let ws;
|
|
135
|
+
if (CONVERSATION_ID.test(ref)) {
|
|
136
|
+
conversation = await getConversation(ctx, ref);
|
|
137
|
+
ws = await ctx.cloud().workspaces.get(conversation.workspace_id);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
ws = await resolveWorkspace(ctx, ref);
|
|
141
|
+
if (ws.labels.shard_conversation && CONVERSATION_ID.test(ws.labels.shard_conversation))
|
|
142
|
+
conversation = await getConversation(ctx, ws.labels.shard_conversation);
|
|
143
|
+
}
|
|
144
|
+
if (conversation?.route === 'codex-cloud')
|
|
145
|
+
throw new UsageError(`Codex Cloud runs this conversation; follow it with shard agents open ${conversation.id}.`);
|
|
146
|
+
if (conversation?.route === 'acp')
|
|
147
|
+
throw new UsageError('This conversation runs through its ACP client, which shows its output; it has no terminal to follow.');
|
|
148
|
+
if (conversation && values.app !== undefined && values.app !== conversation.harness)
|
|
149
|
+
throw new UsageError(`This conversation runs ${conversation.harness}; omit --app.`);
|
|
131
150
|
const project = ws.labels.repo_remote || ws.labels.shard_project || ws.key;
|
|
132
|
-
const app = str(values.app) ?? new AgentStateStore(ctx.store.dir).app(project, ws.labels.app === 'codex' ? 'codex' : 'claude');
|
|
133
|
-
const id = `shard-${ws.labels.agent_pty_name || agentName(ws.key, ws.labels)}-${app}`;
|
|
151
|
+
const app = conversation?.harness ?? str(values.app) ?? new AgentStateStore(ctx.store.dir).app(project, ws.labels.app === 'codex' ? 'codex' : 'claude');
|
|
134
152
|
const cell = ws.cell({ agentLabel: ctx.agentLabel, ...(ctx.wake ? {} : { wake: null }), transitionTimeoutMs: ctx.wakeTimeoutMs });
|
|
153
|
+
let id;
|
|
154
|
+
try {
|
|
155
|
+
id = conversation ? await conversationPtyId(cell, conversation, ctx.signal) : `shard-${ws.labels.agent_pty_name || agentName(ws.key, ws.labels)}-${app}`;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
cell.close();
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
135
161
|
const offsetText = str(values.offset);
|
|
136
162
|
if (offsetText !== undefined && (!/^\d+$/.test(offsetText) || !Number.isSafeInteger(Number(offsetText))))
|
|
137
163
|
throw new UsageError('--offset must be a nonnegative safe integer');
|
|
@@ -175,7 +201,7 @@ export const CENTER_HANDLERS = {
|
|
|
175
201
|
}
|
|
176
202
|
const remainder = decoder.decode();
|
|
177
203
|
if (ctx.json)
|
|
178
|
-
ctx.io.stdout.write(json({ pty_id: id, output: output + remainder, nextOffset: cursor.offset, session, exited }));
|
|
204
|
+
ctx.io.stdout.write(json({ pty_id: id, ...(conversation ? { conversation: { id: conversation.id } } : {}), output: output + remainder, nextOffset: cursor.offset, session, exited }));
|
|
179
205
|
else
|
|
180
206
|
ctx.io.stdout.write(remainder);
|
|
181
207
|
return 0;
|
|
@@ -22,7 +22,12 @@ export const CONVERSATION_HANDLERS = {
|
|
|
22
22
|
return 0;
|
|
23
23
|
},
|
|
24
24
|
async 'conversations get'(ctx, _values, [id]) { const c = await conversationClient(ctx).conversations.get(id); const q = questionLine(c, 200); out(ctx, `${c.title}\n${c.state}\n${c.branch}\n${q ? `${q}\n` : ''}`, c); return 0; },
|
|
25
|
-
|
|
25
|
+
// A finished conversation whose VM was removed restores into a fresh one, then takes the message (APIs that do; 0.18.1+).
|
|
26
|
+
async 'conversations send'(ctx, _values, [id, text]) {
|
|
27
|
+
const r = await conversationClient(ctx).conversations.send(id, text, { signal: ctx.signal });
|
|
28
|
+
out(ctx, `${r.restored ? 'Restored\n' : ''}Message sent (${r.seq}).\n`, r);
|
|
29
|
+
return 0;
|
|
30
|
+
},
|
|
26
31
|
async 'conversations wait'(ctx, values, [id]) {
|
|
27
32
|
const seconds = values.timeout === undefined ? 900 : Number(values.timeout);
|
|
28
33
|
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 900)
|
|
@@ -4,6 +4,16 @@ import type { Values } from './args.js';
|
|
|
4
4
|
import type { Conversation } from './conversations.js';
|
|
5
5
|
/** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
|
|
6
6
|
export declare function conversationTerminal(ctx: Ctx, ws: Workspace, c: Conversation, project: string, values: Values): Promise<number>;
|
|
7
|
+
/**
|
|
8
|
+
* A conversation and its VM, for an app to open (Claude Desktop, claude.ai, T3 Code, the menu bar). A conversation
|
|
9
|
+
* whose VM was removed after it finished restores first (0.18.1+): a fresh VM from its saved branch and transcript, the
|
|
10
|
+
* same conversation and its session resumed. A terminal conversation's session starts detached, as a new one does, so
|
|
11
|
+
* the app reaches it at once; the guest starts the others.
|
|
12
|
+
*/
|
|
13
|
+
export declare function reopenConversation(ctx: Ctx, c: Conversation, values: Values): Promise<{
|
|
14
|
+
conversation: Conversation;
|
|
15
|
+
ws: Workspace;
|
|
16
|
+
}>;
|
|
7
17
|
/**
|
|
8
18
|
* `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
|
|
9
19
|
* Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
|
|
@@ -23,6 +23,7 @@ import { launchT3 } from "./t3-launch.js";
|
|
|
23
23
|
import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-commands.js";
|
|
24
24
|
import { openUrl } from "./browser.js";
|
|
25
25
|
import { out } from "./commands.js";
|
|
26
|
+
import { progress } from "./progress.js";
|
|
26
27
|
/** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
|
|
27
28
|
export async function conversationTerminal(ctx, ws, c, project, values) {
|
|
28
29
|
if (c.route === 'codex-cloud')
|
|
@@ -32,6 +33,27 @@ export async function conversationTerminal(ctx, ws, c, project, values) {
|
|
|
32
33
|
const pty = ws.labels.shard_conversation && ws.labels.shard_conversation !== c.id ? conversationName(c) : undefined;
|
|
33
34
|
return terminal(ctx, ws, c.harness, conversationName(c), project, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id, title: c.title, ...(pty ? { pty } : {}), choice: recordedChoice(c) });
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* A conversation and its VM, for an app to open (Claude Desktop, claude.ai, T3 Code, the menu bar). A conversation
|
|
38
|
+
* whose VM was removed after it finished restores first (0.18.1+): a fresh VM from its saved branch and transcript, the
|
|
39
|
+
* same conversation and its session resumed. A terminal conversation's session starts detached, as a new one does, so
|
|
40
|
+
* the app reaches it at once; the guest starts the others.
|
|
41
|
+
*/
|
|
42
|
+
export async function reopenConversation(ctx, c, values) {
|
|
43
|
+
if (!c.ended_at || c.route === 'codex-cloud')
|
|
44
|
+
return { conversation: c, ws: await conversationWorkspace(ctx, c) };
|
|
45
|
+
const restored = await resumeConversation(ctx, c);
|
|
46
|
+
const ws = await conversationWorkspace(ctx, restored);
|
|
47
|
+
if (restored.route !== 'terminal') {
|
|
48
|
+
progress(ctx, 'Restored');
|
|
49
|
+
return { conversation: restored, ws };
|
|
50
|
+
}
|
|
51
|
+
if (!(await savedMachine(ctx))?.template)
|
|
52
|
+
await syncAgentConfig(ctx, ws);
|
|
53
|
+
const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
|
|
54
|
+
await conversationTerminal(quiet, ws, restored, ws.labels.repo_remote || ws.labels.shard_project || ws.key, { ...values, detach: true });
|
|
55
|
+
return { conversation: restored, ws };
|
|
56
|
+
}
|
|
35
57
|
/**
|
|
36
58
|
* `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
|
|
37
59
|
* Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
|
package/dist/conversations.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Workspace, WorkspaceView } from '@shardflux/sdk';
|
|
1
|
+
import type { CellClient, Workspace, WorkspaceView } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
3
|
import type { ModelChoice } from './agent-models.js';
|
|
4
4
|
export type Harness = 'claude' | 'codex';
|
|
@@ -65,6 +65,13 @@ export interface LocalConversation {
|
|
|
65
65
|
*/
|
|
66
66
|
export declare const conversationEnded: (c: Pick<Conversation, "state" | "ended_at" | "route" | "landed">) => boolean;
|
|
67
67
|
export declare const conversationName: (c: Conversation) => string;
|
|
68
|
+
/**
|
|
69
|
+
* The managed PTY a conversation's harness runs in (0.18.1+). The guest records it per conversation
|
|
70
|
+
* (sf-agent's conversation runtime record), which also keeps the PTY a fork inherited from its parent; without a
|
|
71
|
+
* record it is the one the guest names after the conversation, `shard-<name>-<harness>`. Never the VM key's name: a
|
|
72
|
+
* conversation's VM is `<project>/conv-<name>`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function conversationPtyId(cell: Pick<CellClient, 'exec'>, c: Conversation, signal?: AbortSignal): Promise<string>;
|
|
68
75
|
export declare function validateConversation(c: Conversation): Conversation;
|
|
69
76
|
export declare function localConversations(ctx: Pick<Ctx, 'store' | 'apiUrl'>): Record<string, LocalConversation>;
|
|
70
77
|
export declare function rememberConversation(ctx: Pick<Ctx, 'store' | 'apiUrl'>, c: Conversation, folder: string, handedOff?: boolean, baseBranch?: string): void;
|
package/dist/conversations.js
CHANGED
|
@@ -9,6 +9,7 @@ import { UsageError } from "./errors.js";
|
|
|
9
9
|
import { conversationClient } from "./conversation-commands.js";
|
|
10
10
|
import { recordSeedSnapshot } from "./files-mirror.js";
|
|
11
11
|
import { rememberTwinSeed, sendTwinSeed, twinCheckoutArgv, twinSeed } from "./worktree-twin.js";
|
|
12
|
+
import { progress } from "./progress.js";
|
|
12
13
|
/**
|
|
13
14
|
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
14
15
|
* while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
|
|
@@ -20,6 +21,37 @@ export const conversationName = (c) => {
|
|
|
20
21
|
throw new Error('Conversation returned an invalid branch or name');
|
|
21
22
|
return name;
|
|
22
23
|
};
|
|
24
|
+
/** The guest's managed PTY id: `shard-<name>-<app>` (services/guest-agent/internal/agents PtyID). */
|
|
25
|
+
const MANAGED_PTY = /^shard-[a-z0-9][a-z0-9._-]{0,63}-(claude|codex)$/;
|
|
26
|
+
/**
|
|
27
|
+
* The managed PTY a conversation's harness runs in (0.18.1+). The guest records it per conversation
|
|
28
|
+
* (sf-agent's conversation runtime record), which also keeps the PTY a fork inherited from its parent; without a
|
|
29
|
+
* record it is the one the guest names after the conversation, `shard-<name>-<harness>`. Never the VM key's name: a
|
|
30
|
+
* conversation's VM is `<project>/conv-<name>`.
|
|
31
|
+
*/
|
|
32
|
+
export async function conversationPtyId(cell, c, signal) {
|
|
33
|
+
const derived = `shard-${conversationName(c)}-${c.harness}`;
|
|
34
|
+
let stdout;
|
|
35
|
+
try {
|
|
36
|
+
const r = await cell.exec.run(['cat', `/home/user/.local/share/shardflux/conversation-runtime/${c.id}.json`], { timeoutMs: 10_000, maxOutputBytes: 4096, ...(signal ? { signal } : {}) });
|
|
37
|
+
if (r.exitCode !== 0)
|
|
38
|
+
return derived;
|
|
39
|
+
stdout = r.stdout;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (signal?.aborted)
|
|
43
|
+
throw error;
|
|
44
|
+
return derived;
|
|
45
|
+
}
|
|
46
|
+
let recorded;
|
|
47
|
+
try {
|
|
48
|
+
recorded = JSON.parse(stdout)?.pty_id;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return derived;
|
|
52
|
+
}
|
|
53
|
+
return typeof recorded === 'string' && MANAGED_PTY.test(recorded) && recorded.endsWith(`-${c.harness}`) ? recorded : derived;
|
|
54
|
+
}
|
|
23
55
|
export function validateConversation(c) {
|
|
24
56
|
if (!c || !/^conv_[a-z0-9]{26}$/.test(c.id) || !c.project_id || !c.workspace_id || !c.worktree_path?.startsWith('/home/user/') || /[\0\r\n]/.test(c.worktree_path) || !['claude', 'codex'].includes(c.harness))
|
|
25
57
|
throw new Error('Conversation response omitted its identity or binding');
|
|
@@ -120,6 +152,9 @@ export async function restoreConversationBinding(ctx, c, ws) {
|
|
|
120
152
|
const result = await cell.exec.run(['sf-agent', 'conversation', 'restore', '--args', '{"start":false}'], { cwd: c.worktree_path, env: { SF_CONVERSATION_ID: c.id }, timeoutMs: 120_000, signal: ctx.signal });
|
|
121
153
|
if (result.exitCode !== 0)
|
|
122
154
|
throw new Error(`Conversation restore failed: ${result.stderr.trim()}`);
|
|
155
|
+
// A conversation whose VM was removed now runs in a fresh one, from its saved branch and transcript (0.18.1+).
|
|
156
|
+
if (c.restore.kind === 'resume')
|
|
157
|
+
progress(ctx, 'Restored');
|
|
123
158
|
}
|
|
124
159
|
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID(), choice = {}) {
|
|
125
160
|
const project = projectIdentity(folder);
|
package/dist/t3-launch.js
CHANGED
|
@@ -125,7 +125,9 @@ export async function openT3Conversation(ctx, conversationId, values = {}) {
|
|
|
125
125
|
return 0;
|
|
126
126
|
}
|
|
127
127
|
// Its VM as T3's SSH environment, with the checks every `agents open t3` makes: --reachable and the cloud Claude login.
|
|
128
|
-
|
|
128
|
+
// A conversation whose VM was removed restores into a fresh one first (0.18.1+).
|
|
129
|
+
const { reopenConversation } = await import("./conversation-terminal.js");
|
|
130
|
+
let { ws } = await reopenConversation(ctx, conversation, values);
|
|
129
131
|
if (values.reachable !== undefined)
|
|
130
132
|
ws = await ctx.cloud().workspaces.open({ key: ws.key, ...agentReachability(values), wait: false });
|
|
131
133
|
// A pooled thread (0.18.0+) runs on the account hub, not on the account's cloud Claude login.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork serverless agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"typescript-eslint": "8.70.1"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@shardflux/desktop-linux-x64": "0.1.
|
|
62
|
-
"@shardflux/desktop-linux-arm64": "0.1.
|
|
61
|
+
"@shardflux/desktop-linux-x64": "0.1.1",
|
|
62
|
+
"@shardflux/desktop-linux-arm64": "0.1.1",
|
|
63
63
|
"@shardflux/menubar-darwin": "^0.6.0"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|