@shardflux/cli 0.15.2 → 0.16.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 +82 -12
- package/README.md +50 -3
- package/dist/acp-models.js +5 -1
- package/dist/agent-commands.d.ts +8 -2
- package/dist/agent-commands.js +23 -11
- package/dist/agent-target-commands.d.ts +7 -2
- package/dist/agent-target-commands.js +12 -7
- package/dist/agent-targets.d.ts +3 -0
- package/dist/agent-targets.js +4 -1
- package/dist/agent-terminal.d.ts +8 -0
- package/dist/agent-terminal.js +35 -2
- package/dist/args.d.ts +1 -1
- package/dist/args.js +26 -3
- package/dist/codex-cloud.d.ts +2 -0
- package/dist/codex-cloud.js +2 -0
- package/dist/command-center-actions.js +9 -0
- package/dist/commands.d.ts +22 -0
- package/dist/commands.js +54 -14
- package/dist/conversation-commands.js +6 -0
- package/dist/conversation-terminal.js +10 -1
- package/dist/conversations.d.ts +13 -1
- package/dist/conversations.js +41 -5
- package/dist/device-helper.js +9 -2
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +11 -1
- package/dist/files-mirror.d.ts +164 -0
- package/dist/files-mirror.js +761 -0
- package/dist/handlers.js +3 -1
- package/dist/main.js +9 -2
- package/dist/registry.d.ts +2 -42
- package/dist/registry.js +6 -90
- package/dist/thread-on-computer.d.ts +35 -0
- package/dist/thread-on-computer.js +200 -0
- package/dist/wait-status.d.ts +22 -0
- package/dist/wait-status.js +59 -0
- package/dist/worktree-twin-engine.d.ts +95 -0
- package/dist/worktree-twin-engine.js +402 -0
- package/dist/worktree-twin-land.py +331 -0
- package/dist/worktree-twin.d.ts +296 -0
- package/dist/worktree-twin.js +718 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,17 +1,87 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
3
|
+
## 0.16.0 (not yet published)
|
|
4
|
+
|
|
5
|
+
- Cloud sessions are named for the work: `shard ./` starts the agent with the repository or folder name and the first
|
|
6
|
+
line of your prompt, so claude.ai, the Claude app and its notifications show `Add CSV export · todo-app`
|
|
7
|
+
(`New conversation · todo-app` until the first prompt arrives) instead of the project slug and conversation id.
|
|
8
|
+
Claude Desktop and Codex app targets are listed as `main · acme`, without the `[SF] ` tag.
|
|
9
|
+
|
|
10
|
+
- Updates show up the moment they are published (also in 0.15.2): the latest version comes from npm's `latest` tag
|
|
11
|
+
(what `npm i -g shardflux` installs), checked at most every 12 hours with its ETag. The API still says which
|
|
12
|
+
versions it supports, and can hold back a release. Offline, nothing is printed. `shard update --check` and the menu
|
|
13
|
+
bar's update check use the same versions.
|
|
14
|
+
- The menu bar keeps up with npm by itself (also in 0.15.2): after `npm i -g shardflux`, every `shard` command compares
|
|
15
|
+
the installed app with the one the package carries (two Info.plist reads, no network) and, when that one is newer,
|
|
16
|
+
brings it in in the background: a stopped app is replaced on disk, a running one is left to update itself (menu bar
|
|
17
|
+
0.3.2+) or, for earlier apps, restarted on the new version when none of its windows is open. `shard update` swaps in
|
|
18
|
+
an app the package already carries without another install. App versions compare as semver (0.3.10 after 0.3.9).
|
|
19
|
+
|
|
20
|
+
- A new thread on an existing computer: `shard agents new --app claude|codex --on <ref>` and `shard ./ --on <ref>`
|
|
21
|
+
start Claude or Codex on the computer (VM) of an existing conversation instead of a new one. The thread works on its
|
|
22
|
+
own branch and worktree there, starting from that conversation's committed `HEAD`, and shares the computer's
|
|
23
|
+
installed packages, caches, running services, ports and files. `<ref>` is a conversation id, key or name, or the
|
|
24
|
+
computer's workspace key or id; a parked computer wakes for it. Codex threads are genuine Codex Cloud threads.
|
|
25
|
+
`--review` starts a read-only Claude reviewer of that conversation's worktree instead. `--prompt`, `--prompt-file`
|
|
26
|
+
and `--image` work as for a new conversation.
|
|
27
|
+
- `shard agents computers [--path <folder>]` lists the project's computers a thread can start on and the threads on
|
|
28
|
+
each (`--json`: `{computers: [{workspace_id, workspace_key, state, conversations: [...]}]}`).
|
|
29
|
+
- `shard agents list` shows each session's CONVERSATION and COMPUTER; `--json` rows add `conversation_id` and
|
|
30
|
+
`conversation_key`. `agents open <conversation> --in auto` opens that conversation's own Claude session when its
|
|
31
|
+
computer runs several, and the `--json` of a started Claude conversation names its `conversation.id`.
|
|
32
|
+
- A conversation's files folder starts from its own seed, also when several threads share one computer.
|
|
33
|
+
|
|
34
|
+
- Waits show where a queued start stands: while a command waits on an operation whose start is queued for capacity,
|
|
35
|
+
one line on stderr gives its place, the host being started for it and until when it holds its place
|
|
36
|
+
(`queued for capacity (position 3): a new host is starting, ready by 21:40Z; it holds its place until 21:51Z`),
|
|
37
|
+
again whenever that changes. Dim on a terminal, plain when piped, never with `--json`. A command waiting for room on
|
|
38
|
+
the workspace's host or for a free working slot of the plan says so the same way.
|
|
39
|
+
|
|
40
|
+
- **Breaking:** queued starts are seen through: without `--timeout` (or `--wake-timeout`), a wait whose start is queued for capacity
|
|
41
|
+
keeps waiting until the start's deadline instead of exiting at the default 5 minutes (or 2 minutes for a wake). An
|
|
42
|
+
explicit `--timeout`, `--wake-timeout` or `SHARDFLUX_WAKE_TIMEOUT_MS` is kept as given; its timeout error (exit 5)
|
|
43
|
+
names the queue position, and `--json` adds `last_reason`, `deadline_at` and `queue_position`.
|
|
44
|
+
|
|
45
|
+
- Commands wait for room instead of failing (SDK 0.22.0): a call refused before it ran while the workspace's host makes
|
|
46
|
+
room (503 `host_capacity`) or while every workspace the plan lets work at once is busy (429 `quota_exceeded`) is sent
|
|
47
|
+
again after each `Retry-After` for up to 2 minutes, `ws exec` starts and `ws open` included.
|
|
48
|
+
|
|
49
|
+
- The agent terminal (`shard ./`, `shard agents open --in terminal`) waits `Retry-After` when the workspace's host is
|
|
50
|
+
making room, says `waiting for room on the workspace's host (position N)` once, and after a drop that follows a
|
|
51
|
+
working attach reconnects from the shortest backoff again.
|
|
52
|
+
|
|
53
|
+
- Every conversation's committed files on your Mac: while the menu bar app runs, its device helper keeps
|
|
54
|
+
`~/Shardflux/<repository>/<base branch>/<conversation>/` in step with what each conversation of your account
|
|
55
|
+
committed, read-only and without prompts. A folder holds the files that conversation changed or added since it
|
|
56
|
+
started (`.shardflux-manifest.json` lists them, deletions included); the base branch moving, a rebase, and the
|
|
57
|
+
uncommitted edits your worktree had when the conversation started are left out. All worktrees of one repository are
|
|
58
|
+
one `<repository>` folder. Files above 50 MB, and files past 2 GB per conversation, are listed as
|
|
59
|
+
`<file>.placeholder.txt`; set `max_file_mb` and `max_conversation_mb` in the repository's `.shardflux/files.yaml`.
|
|
60
|
+
A landed conversation's folder is marked `landed` and stops updating; a discarded one is removed. Your own
|
|
61
|
+
checkouts are never written.
|
|
62
|
+
|
|
63
|
+
- `shard files pull <conversation> <path>` downloads one listed file in place of its placeholder, whatever its size.
|
|
64
|
+
|
|
65
|
+
- New conversations record the working tree they start from (a seed snapshot made in the conversation's VM, never on
|
|
66
|
+
your Mac) and the base branch they start on.
|
|
67
|
+
|
|
68
|
+
- Finished conversations land in your local checkout with no input: while the menu bar app runs, a conversation
|
|
69
|
+
whose turn ends with its work committed lands into the branch it started from. The branch moves to its commits
|
|
70
|
+
(fast-forward, or its commits replayed onto your newer ones) and the files it changed are written atomically in the
|
|
71
|
+
worktree where that branch is checked out, three-way merged with your uncommitted edits (your text wins where both
|
|
72
|
+
changed the same lines). The index follows, so `git status` shows only your own edits. A file is written only after
|
|
73
|
+
2 seconds without a change, never while your own merge, rebase, cherry-pick or bisect runs, and every rewritten file
|
|
74
|
+
is backed up first (`shard conversations landing backups`, `restore <backup>`; kept 14 days, up to 1 GiB). When the
|
|
75
|
+
conversation's commits conflict with your newer commits, its agent is asked in that conversation to merge your
|
|
76
|
+
branch and resolve; the result lands when it finishes. `shard conversations landing pr` opens a pull request
|
|
77
|
+
instead; `off` keeps conversations in the cloud. Conversations that finish while your computer is offline or asleep
|
|
78
|
+
land the moment it is back, one after another in the order they finished, also when their VM has been reclaimed.
|
|
79
|
+
|
|
80
|
+
- A conversation started from a checkout with uncommitted edits starts from them: its base is a snapshot commit of
|
|
81
|
+
your working tree (kept as `refs/shard/twin/<id>` in your repository; your index, HEAD and branches are not
|
|
82
|
+
touched), so its own commits hold only its work and your edits stay yours.
|
|
83
|
+
|
|
84
|
+
- Includes SDK 0.22.0.
|
|
15
85
|
|
|
16
86
|
## 0.15.1
|
|
17
87
|
|
package/README.md
CHANGED
|
@@ -80,6 +80,20 @@ settings: T3 runs `shard acp --login` in its own terminal, which creates or sign
|
|
|
80
80
|
Codex login, or your email) and connects Claude Code and Codex, skipping every step that is already done (0.15.0+).
|
|
81
81
|
With T3 Code 0.0.45, `shard agents new --in t3` adds the project as an SSH environment instead.
|
|
82
82
|
|
|
83
|
+
Start another thread on a computer that is already set up (0.16.0+): it shares that conversation's installed
|
|
84
|
+
packages, caches, running services, ports and files, and works on its own branch and worktree.
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
shard agents computers # the project's computers and the threads on each
|
|
88
|
+
shard ./ --on fix-login # Claude (or Codex) joins the computer of conversation fix-login
|
|
89
|
+
shard agents new --app codex --on fix-login --prompt "Load-test the API it is running"
|
|
90
|
+
shard ./ --on fix-login --review # a read-only Claude reviewer of its worktree
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`--on` takes a conversation's id, key or name, or the computer's workspace key or id. A parked computer wakes for
|
|
94
|
+
the new thread. Codex threads are Codex Cloud threads, on the web and your phone too. `shard agents list` shows each
|
|
95
|
+
thread's conversation and computer.
|
|
96
|
+
|
|
83
97
|
Git fetch and push use the coding guest’s platform transport when available, preserving Git’s binary packets (0.14.0+). Older guests retain their existing transport.
|
|
84
98
|
|
|
85
99
|
To try a conversation on your Mac:
|
|
@@ -94,6 +108,26 @@ Device tools ask for approval on your Mac before preparing the conversation’s
|
|
|
94
108
|
`~/.config/shardflux/conversations/`; ended sessions enter Claude Code’s or Codex’s native history. Transcript files up to 64 MiB are supported (0.14.0+).
|
|
95
109
|
From 0.14.3, transcript copying runs independently of device approvals. Unknown entries are skipped, and failed copies retry with backoff; diagnostics name the conversation and affected entry.
|
|
96
110
|
|
|
111
|
+
Every conversation's committed files are on your Mac too (0.16.0+, with the menu bar app running):
|
|
112
|
+
`~/Shardflux/<repository>/<base branch>/<conversation>/` holds what each conversation changed or added, read-only,
|
|
113
|
+
and updates when its branch moves. Large files are listed as `<file>.placeholder.txt`; download one with the menu
|
|
114
|
+
bar's **Download** or:
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
shard files pull hype-video out/hype.mp4
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Finished conversations land in your checkout by themselves (0.16.0+, with the menu bar app running). When a
|
|
121
|
+
conversation's turn ends with its work committed, the branch it started from moves to its commits and the changed
|
|
122
|
+
files are written in place, next to your own uncommitted edits, which stay as they are (where both touched the same
|
|
123
|
+
lines, your text is kept). `git status` keeps showing only your edits. A conversation started from a checkout with
|
|
124
|
+
uncommitted edits starts from them too. Every rewritten file is backed up first:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
shard conversations landing # auto (default), pr or off for this project
|
|
128
|
+
shard conversations landing backups # then: shard conversations landing restore <backup>
|
|
129
|
+
```
|
|
130
|
+
|
|
97
131
|
For application workspace commands, `shard signup` creates an account and saves a project key, or set
|
|
98
132
|
`SHARDFLUX_API_KEY` to a key from the console and run `shard login`. `shard --help` lists every command.
|
|
99
133
|
|
|
@@ -692,7 +726,11 @@ without waking it (the state at suspension); `files search` then notes it on std
|
|
|
692
726
|
- `--wake-timeout <ms>` (or `SHARDFLUX_WAKE_TIMEOUT_MS`, default `120000`) bounds the wait. Past
|
|
693
727
|
it, `shard` exits 5 and names the operation and its state (for example `Operation <id> (open)
|
|
694
728
|
is still queued after 120000 ms`). The operation continues server side:
|
|
695
|
-
`shard operations wait <id>` keeps waiting.
|
|
729
|
+
`shard operations wait <id>` keeps waiting. Without either (0.16.0+), a resume queued for capacity is waited for
|
|
730
|
+
until its deadline, its place shown on stderr (see [Start deadlines](#start-deadlines)).
|
|
731
|
+
- A call refused while the workspace's host makes room to wake it (503 `host_capacity`) or while every workspace the
|
|
732
|
+
plan lets work at once is busy (429 `quota_exceeded`) did not run: `shard` sends it again after each `Retry-After`,
|
|
733
|
+
for up to 2 minutes (0.16.0+), and stderr says `waiting for room on the workspace's host (position 2)` once.
|
|
696
734
|
- A resume or open that fails exits 6.
|
|
697
735
|
- `--no-wake` (or `SHARDFLUX_NO_WAKE=1`) turns this off. A workspace that is not running is then
|
|
698
736
|
refused at once (exit 1, `workspace_not_running`).
|
|
@@ -875,7 +913,16 @@ that screenshot, default 250) and `--format jpeg --quality N` for smaller images
|
|
|
875
913
|
## Start deadlines
|
|
876
914
|
|
|
877
915
|
A queued open, resume or fork (`capacity_pending`) has a deadline 15 minutes after it was created.
|
|
878
|
-
|
|
916
|
+
While `shard` waits for it, stderr says where it stands (0.16.0+), again whenever that changes (dim on a terminal,
|
|
917
|
+
never with `--json`):
|
|
918
|
+
|
|
919
|
+
```text
|
|
920
|
+
queued for capacity (position 3): a new host is starting, ready by 21:40Z; it holds its place until 21:51Z
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
Without `--timeout`, a queued start is waited for until its deadline (0.16.0+). An explicit `--timeout` is kept: if it
|
|
924
|
+
ends first, `shard` exits 5 and the timeout error names the deadline and the queue position (`--json`:
|
|
925
|
+
`deadline_at`, `queue_position`, `last_reason`). A start still pending at the
|
|
879
926
|
deadline fails with `capacity_unavailable`: nothing was started, and a suspended workspace stays
|
|
880
927
|
suspended. `shard` exits 6 as for any failed operation, and the error says `retryable=true` (with
|
|
881
928
|
`--json`, `"retryable": true`) with a hint to retry:
|
|
@@ -1047,7 +1094,7 @@ printf %s "$NEW_KEY" | shard secrets rotate OPENAI_API_KEY
|
|
|
1047
1094
|
| 2 | usage error: bad arguments, or input the API rejected as `validation_failed` |
|
|
1048
1095
|
| 3 | not found: workspace, operation or route |
|
|
1049
1096
|
| 4 | authentication or authorization: missing, malformed, revoked or insufficient key or session; `step_up_required` (run `shard auth step-up`), `mfa_required` (a sign-in waiting for its code), `email_unverified`, `invalid_credentials`; `access_denied` or `expired_token` from `auth login --browser` (0.15.0+) |
|
|
1050
|
-
| 5 | timeout: waiting gave up (`--timeout`, or `--wake-timeout` while waking a workspace); the operation continues server side (`shard operations wait <id>`), up to a queued start's
|
|
1097
|
+
| 5 | timeout: waiting gave up (`--timeout`, or `--wake-timeout` while waking a workspace); the operation continues server side (`shard operations wait <id>`), up to a queued start's deadline (`deadline_at`). Without `--timeout`, a wait on a start queued for capacity lasts until that deadline (0.16.0+). `executions get` (0.5.0+): the execution is still queued or running. `ws exec-read` (0.8.0+): the background command still runs. `billing upgrade --wait` (0.5.0+): nobody paid in time (the checkout stays open) |
|
|
1051
1098
|
| 6 | the awaited operation ended `failed` or `canceled` (also a resume or open that failed while waking a workspace). `retryable=true` in the error means a retry can succeed (`capacity_unavailable`). Also a file-first execution that ended `failed` or `lost` (0.5.0+): nothing was published. `exec` (0.5.1+): the command could not start (`exec_failed_to_start`). `billing upgrade --wait` (0.5.0+): the checkout expired or was canceled (`checkout_ended`) |
|
|
1052
1099
|
| 130 | interrupted (Ctrl-C). Waits stop at once, and exec sends a cancel to the session. A file-first execution runs to completion, and the error names the `shard executions get` command for its result. |
|
|
1053
1100
|
| exec, executions get, ws exec-read | the command's own code: 124 if it timed out, 128+N if killed by signal N. Failures before the command ran use the codes above; use `--json` to tell them apart. |
|
package/dist/acp-models.js
CHANGED
|
@@ -7,6 +7,8 @@ import { createConversationRecord, conversationWorkspace } from "./conversations
|
|
|
7
7
|
import { AgentStateStore } from "./agent-state.js";
|
|
8
8
|
import { objectValue } from "./acp-rpc.js";
|
|
9
9
|
import { nativeCurrentModel, nativeModelOptions } from "./acp-catalog.js";
|
|
10
|
+
import { recordSeedSnapshot } from "./files-mirror.js";
|
|
11
|
+
import { sendTwinSeed, twinCheckoutArgv } from "./worktree-twin.js";
|
|
10
12
|
/** No prompts, login, logout or credential writes. Every child process group is
|
|
11
13
|
* terminated, including initialization failures and interrupted clients. */
|
|
12
14
|
export const ACP_MODELS_SCRIPT = String.raw `
|
|
@@ -143,9 +145,11 @@ export const ACP_LIFECYCLE = { async prepare(ctx, folder, name, callbacks) {
|
|
|
143
145
|
const seeded = (async () => {
|
|
144
146
|
await seedAgentRepository(ctx, ws, folder, { root: c.worktree_path, populate: true,
|
|
145
147
|
initialFiles: ['.claude/settings.json', '.claude/settings.local.json', '.codex/config.toml', '.mcp.json'], onInitialFiles: initialReady });
|
|
146
|
-
|
|
148
|
+
await sendTwinSeed(ctx.store.dir, ws.cell(), c, folder, ctx.signal);
|
|
149
|
+
const result = await ws.cell().exec.run(twinCheckoutArgv(ctx.store.dir, c), { timeoutMs: 30_000, signal: ctx.signal });
|
|
147
150
|
if (result.exitCode !== 0)
|
|
148
151
|
throw new Error(`Conversation branch could not be prepared: ${result.stderr.trim()}`);
|
|
152
|
+
await recordSeedSnapshot(ws, c, ctx.signal);
|
|
149
153
|
callbacks.prepared();
|
|
150
154
|
})();
|
|
151
155
|
void seeded.catch(initialFailed);
|
package/dist/agent-commands.d.ts
CHANGED
|
@@ -4,13 +4,19 @@ import type { Values } from './args.js';
|
|
|
4
4
|
/** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
|
|
5
5
|
export declare function disambiguatedSlug(slug: string, identity: string): string;
|
|
6
6
|
export declare function projectTemplate(ctx: Ctx, remote: string | null): Promise<string>;
|
|
7
|
-
|
|
7
|
+
/** `place` is the repository or folder name session titles end with ("todo-app"), `task` what a new session is titled
|
|
8
|
+
* with before the guest names it from its first prompt (a first prompt line or the conversation's title). */
|
|
9
|
+
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?: {
|
|
8
10
|
id: string;
|
|
9
11
|
session: string | null;
|
|
10
|
-
}): Promise<PtySession>;
|
|
12
|
+
}, task?: string): Promise<PtySession>;
|
|
13
|
+
/** The first non-empty line of a prompt or title, bounded; the guest makes it the task part of the session title. */
|
|
14
|
+
export declare function sessionTask(text: string | undefined): string | undefined;
|
|
11
15
|
export declare function terminal(ctx: Ctx, ws: Workspace, app: 'claude' | 'codex', agent: string, project: string, values: Values, setupHash?: string, cwd?: string, conversation?: {
|
|
12
16
|
id: string;
|
|
13
17
|
session: string | null;
|
|
18
|
+
pty?: string;
|
|
19
|
+
title?: string;
|
|
14
20
|
}): Promise<number>;
|
|
15
21
|
export declare const launchProject: Handler;
|
|
16
22
|
export declare const AGENT_HANDLERS: Record<string, Handler>;
|
package/dist/agent-commands.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { repositoryName } from "./files-mirror.js";
|
|
1
2
|
import { prepareSetupCache } from "./setup-cache.js";
|
|
2
3
|
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
3
4
|
import { agentReachability } from "./agent-reachability.js";
|
|
@@ -47,7 +48,9 @@ export async function projectTemplate(ctx, remote) {
|
|
|
47
48
|
throw error;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
+
/** `place` is the repository or folder name session titles end with ("todo-app"), `task` what a new session is titled
|
|
52
|
+
* with before the guest names it from its first prompt (a first prompt line or the conversation's title). */
|
|
53
|
+
export async function agentPty(cell, app, agent, approvals, term, signal, place, setupHash, cwd, ptyName, onLaunch, deferSetup = false, conversation, task) {
|
|
51
54
|
const id = `shard-${ptyName || agent}-${app}`;
|
|
52
55
|
// Arrival and changed locks queue the same durable guest job even when the
|
|
53
56
|
// native agent is already alive. Setup never becomes its first user turn.
|
|
@@ -73,7 +76,7 @@ export async function agentPty(cell, app, agent, approvals, term, signal, projec
|
|
|
73
76
|
conversationArgs = ['--conversation', conversation.id, ...(conversation.session ? ['--resume-session', conversation.session] : [])];
|
|
74
77
|
}
|
|
75
78
|
const launch = await cell.exec.run(['sf-agent', 'run', app, '--name', agent, '--detach', ...conversationArgs, ...(ptyName ? ['--pty-name', ptyName] : []), ...(approvals ? ['--approvals'] : []), ...(setupHash ? ['--setup', '--setup-deps-hash', setupHash] : [])], {
|
|
76
|
-
env: { TERM: term, ...(
|
|
79
|
+
env: { TERM: term, ...(place ? { SF_PROJECT_NAME: place, SF_REPO_NAME: place } : {}), ...(task ? { SF_TASK_TITLE: task } : {}), ...(setupHash && deferSetup ? { SF_SETUP_DEFERRED: '1' } : {}) }, timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}),
|
|
77
80
|
});
|
|
78
81
|
if (launch.exitCode !== 0)
|
|
79
82
|
throw new Error(`agent launch failed: ${launch.stderr.trim() || `exit ${launch.exitCode}`}`);
|
|
@@ -91,7 +94,13 @@ export async function agentPty(cell, app, agent, approvals, term, signal, projec
|
|
|
91
94
|
throw new Error(`agent exited before attach: ${session.state}`);
|
|
92
95
|
return session;
|
|
93
96
|
}
|
|
97
|
+
/** The first non-empty line of a prompt or title, bounded; the guest makes it the task part of the session title. */
|
|
98
|
+
export function sessionTask(text) {
|
|
99
|
+
const line = text?.split(/\r?\n/).map(l => l.replace(/[\s\p{Cc}]+/gu, ' ').trim()).find(Boolean);
|
|
100
|
+
return line ? [...line].slice(0, 200).join('') : undefined;
|
|
101
|
+
}
|
|
94
102
|
export async function terminal(ctx, ws, app, agent, project, values, setupHash, cwd = workspaceRepositoryPath(ws), conversation) {
|
|
103
|
+
const ptyName = conversation?.pty ?? ws.labels.agent_pty_name;
|
|
95
104
|
const store = new AgentStateStore(ctx.store.dir);
|
|
96
105
|
if (!ws.ready)
|
|
97
106
|
await ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
|
|
@@ -101,12 +110,12 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
101
110
|
const tunnels = attach ? await startAgentConfigTunnels(ctx, ws) : null;
|
|
102
111
|
try {
|
|
103
112
|
if (app === 'codex' && typeof values.initialPrompt === 'string') {
|
|
104
|
-
await prepareInitialCodexProject(cell, cwd, `shard-${
|
|
113
|
+
await prepareInitialCodexProject(cell, cwd, `shard-${ptyName || agent}-${app}`);
|
|
105
114
|
}
|
|
106
115
|
const credentialFill = await configureCredentialFill(ctx, ws);
|
|
107
116
|
// A cold launch is remembered before it starts, so the listing can show it before the projection does (agent-live.ts).
|
|
108
117
|
const term = ctx.io.env.TERM && ctx.io.env.TERM !== 'dumb' ? ctx.io.env.TERM : 'xterm-256color';
|
|
109
|
-
const session = await agentPty(cell, app, agent, values.approvals === true, term, ctx.signal, ws.labels.
|
|
118
|
+
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));
|
|
110
119
|
// A new conversation's guest can start the agent before this command reaches it, so the launch above never ran.
|
|
111
120
|
// Remember it either way: listing and `agents open` then read the guest until the projection lists the session.
|
|
112
121
|
rememberLaunch(ctx, ws.id);
|
|
@@ -126,7 +135,8 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
126
135
|
}
|
|
127
136
|
if (!attach) {
|
|
128
137
|
await preparation;
|
|
129
|
-
|
|
138
|
+
// conversation (0.16.0+): which conversation this session is, also when its computer hosts several threads.
|
|
139
|
+
ctx.io.stdout.write(json({ workspace: ws.data, session, ...(conversation ? { conversation: { id: conversation.id } } : {}), ...(typeof values.initialPrompt === 'string' ? { initial_prompt: { submitted: true } } : {}) }));
|
|
130
140
|
return 0;
|
|
131
141
|
}
|
|
132
142
|
ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
|
|
@@ -258,10 +268,11 @@ export const AGENT_HANDLERS = {
|
|
|
258
268
|
}
|
|
259
269
|
if (values.in === 'auto') {
|
|
260
270
|
// The menu and other launchers use the same CLI-owned target selection. Never put provider URLs in a client.
|
|
261
|
-
|
|
271
|
+
// A conversation id opens that conversation's own session, also when its computer hosts other threads.
|
|
272
|
+
const ws = resolvedConversation ? await conversationWorkspace(ctx, resolvedConversation) : await resolveWorkspace(ctx, agent);
|
|
262
273
|
const project = ws.labels.repo_remote || ws.labels.shard_project || ws.key;
|
|
263
274
|
const store = new AgentStateStore(ctx.store.dir);
|
|
264
|
-
const app = values.app ?? ws.labels.shard_app ?? ws.labels.app ?? store.app(project);
|
|
275
|
+
const app = values.app ?? resolvedConversation?.harness ?? ws.labels.shard_app ?? ws.labels.app ?? store.app(project);
|
|
265
276
|
const detected = installedTargets(ctx.io.env);
|
|
266
277
|
const saved = store.read().projects?.[project]?.target ??
|
|
267
278
|
(project.startsWith('file://') ? store.read().projects?.[new URL(project).pathname]?.target : undefined);
|
|
@@ -270,7 +281,7 @@ export const AGENT_HANDLERS = {
|
|
|
270
281
|
if ((!saved || !detected.targets.includes(saved)) && app === 'codex') {
|
|
271
282
|
const next = { ...values };
|
|
272
283
|
delete next.in;
|
|
273
|
-
return AGENT_HANDLERS['agents open'](ctx, next, [ws.key]);
|
|
284
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [resolvedConversation?.id ?? ws.key]);
|
|
274
285
|
}
|
|
275
286
|
if (saved && detected.targets.includes(saved)) {
|
|
276
287
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -282,16 +293,17 @@ export const AGENT_HANDLERS = {
|
|
|
282
293
|
// Claude opens where people work (0.15.1+): the conversation in Claude Desktop when it is installed, else Claude
|
|
283
294
|
// Code in a terminal. The browser (claude.ai) only when it is the chosen target (--in web).
|
|
284
295
|
if (detected.clients.includes('claude-desktop')) {
|
|
285
|
-
return openClaudeDesktopSession(ctx, ws);
|
|
296
|
+
return openClaudeDesktopSession(ctx, ws, resolvedConversation);
|
|
286
297
|
}
|
|
298
|
+
const opened = resolvedConversation?.id ?? ws.key;
|
|
287
299
|
if (ctx.json) {
|
|
288
300
|
// An app runs this in the person's terminal (the menu bar opens Terminal with it).
|
|
289
|
-
out(ctx, `shard agents open ${
|
|
301
|
+
out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
|
|
290
302
|
return 0;
|
|
291
303
|
}
|
|
292
304
|
const next = { ...values };
|
|
293
305
|
delete next.in;
|
|
294
|
-
return AGENT_HANDLERS['agents open'](ctx, next, [
|
|
306
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
|
|
295
307
|
}
|
|
296
308
|
if (values.in !== undefined && values.in !== 'terminal') {
|
|
297
309
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Duplex } from 'node:stream';
|
|
2
2
|
import type { CellClient, Workspace } from '@shardflux/sdk';
|
|
3
|
+
import type { Conversation } from './conversations.js';
|
|
3
4
|
import type { Ctx, Handler, Io } from './commands.js';
|
|
4
5
|
import { AgentTargets } from './agent-targets.js';
|
|
5
6
|
import type { DesktopTarget } from './agent-targets.js';
|
|
@@ -13,7 +14,11 @@ export declare function proxyStream(io: Io, ended?: () => void, outputGate?: Pro
|
|
|
13
14
|
* this computer just started can accept its first prompt before Remote Control registers, so an open of our recent
|
|
14
15
|
* launch probes the guest for up to 30 s; history browsing stays passive.
|
|
15
16
|
*/
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The Claude Remote Control URL of a session in `ws`. With several threads on one computer (0.16.0+) a conversation's
|
|
19
|
+
* own session is chosen by its harness session id; a thread that has not reported one yet is unambiguous only alone.
|
|
20
|
+
*/
|
|
21
|
+
export declare function remoteControlUrl(ctx: Ctx, ws: Workspace, conversation?: Pick<Conversation, 'harness_session_id'>): Promise<string>;
|
|
17
22
|
/** Claude Desktop's own link to a Remote Control session: claude://code/<session id> opens it in its Code tab. */
|
|
18
23
|
export declare function claudeDesktopLink(remoteControl: string): string;
|
|
19
24
|
export declare const CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
@@ -21,7 +26,7 @@ export declare const CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
|
21
26
|
* Opens the conversation itself (its live Remote Control session) in Claude Desktop (0.15.1+), the way `--in auto`
|
|
22
27
|
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
23
28
|
*/
|
|
24
|
-
export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace): Promise<number>;
|
|
29
|
+
export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace, conversation?: Pick<Conversation, 'harness_session_id'>): Promise<number>;
|
|
25
30
|
export declare function openDesktopWorkspace(ctx: Ctx, ws: Workspace, target: DesktopTarget, t3Version?: string, prewarmed?: boolean): Promise<number>;
|
|
26
31
|
export declare function queueT3Prewarm(ctx: Ctx, ws: Workspace, version: string): Promise<void>;
|
|
27
32
|
export declare const TARGET_HANDLERS: Record<string, Handler>;
|
|
@@ -6,7 +6,7 @@ import { spawn } from 'node:child_process';
|
|
|
6
6
|
import { Readable, Duplex } from 'node:stream';
|
|
7
7
|
import { ShardfluxApiError, WorkspaceTunnels } from '@shardflux/sdk';
|
|
8
8
|
import { out, resolveWorkspace } from "./commands.js";
|
|
9
|
-
import { AgentTargets, installedTargets } from "./agent-targets.js";
|
|
9
|
+
import { AgentTargets, installedTargets, targetLabel } from "./agent-targets.js";
|
|
10
10
|
import { join, relative } from 'node:path';
|
|
11
11
|
import { projectIdentity } from "./project-identity.js";
|
|
12
12
|
import { projectWorkspacePath } from "./agent-repository.js";
|
|
@@ -137,12 +137,18 @@ export function proxyStream(io, ended, outputGate) {
|
|
|
137
137
|
* this computer just started can accept its first prompt before Remote Control registers, so an open of our recent
|
|
138
138
|
* launch probes the guest for up to 30 s; history browsing stays passive.
|
|
139
139
|
*/
|
|
140
|
-
|
|
140
|
+
/**
|
|
141
|
+
* The Claude Remote Control URL of a session in `ws`. With several threads on one computer (0.16.0+) a conversation's
|
|
142
|
+
* own session is chosen by its harness session id; a thread that has not reported one yet is unambiguous only alone.
|
|
143
|
+
*/
|
|
144
|
+
export async function remoteControlUrl(ctx, ws, conversation) {
|
|
141
145
|
let cursor;
|
|
142
146
|
let url;
|
|
147
|
+
const session = conversation?.harness_session_id ?? undefined;
|
|
148
|
+
const mine = (id) => session === undefined || id === session;
|
|
143
149
|
do {
|
|
144
150
|
const page = await ctx.cloud().agents.list({ limit: 200, ...(cursor ? { cursor } : {}) });
|
|
145
|
-
url = page.data.find(a => a.workspace_id === ws.id && a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
151
|
+
url = page.data.find(a => a.workspace_id === ws.id && a.app === 'claude' && a.state !== 'exited' && a.remote_control_url && mine(a.id.slice(a.workspace_id.length + 1)))?.remote_control_url;
|
|
146
152
|
cursor = page.nextCursor ?? undefined;
|
|
147
153
|
} while (!url && cursor && !ctx.signal.aborted);
|
|
148
154
|
// A new Claude process can accept its first prompt before Remote Control registers.
|
|
@@ -158,7 +164,7 @@ export async function remoteControlUrl(ctx, ws) {
|
|
|
158
164
|
});
|
|
159
165
|
if (result.exitCode !== 0)
|
|
160
166
|
break;
|
|
161
|
-
url = guestSessions(result.stdout).find(a => a.app === 'claude' && a.state !== 'exited' && a.remote_control_url)?.remote_control_url;
|
|
167
|
+
url = guestSessions(result.stdout).find(a => a.app === 'claude' && a.state !== 'exited' && a.remote_control_url && mine(a.session_id))?.remote_control_url;
|
|
162
168
|
if (!url && Date.now() < deadline)
|
|
163
169
|
await delay(Math.min(250, Math.max(1, deadline - Date.now())), undefined, { signal: ctx.signal });
|
|
164
170
|
}
|
|
@@ -184,9 +190,9 @@ export const CLAUDE_DESKTOP_BUNDLE = 'com.anthropic.claudefordesktop';
|
|
|
184
190
|
* Opens the conversation itself (its live Remote Control session) in Claude Desktop (0.15.1+), the way `--in auto`
|
|
185
191
|
* opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
|
|
186
192
|
*/
|
|
187
|
-
export async function openClaudeDesktopSession(ctx, ws) {
|
|
193
|
+
export async function openClaudeDesktopSession(ctx, ws, conversation) {
|
|
188
194
|
await ensureCloudClaude(ctx, ws);
|
|
189
|
-
const session = await remoteControlUrl(ctx, ws);
|
|
195
|
+
const session = await remoteControlUrl(ctx, ws, conversation);
|
|
190
196
|
const url = claudeDesktopLink(session);
|
|
191
197
|
if (!ctx.json)
|
|
192
198
|
await openLink(url, ctx.io.env);
|
|
@@ -251,7 +257,6 @@ export async function queueT3Prewarm(ctx, ws, version) {
|
|
|
251
257
|
}
|
|
252
258
|
}
|
|
253
259
|
const tilde = (targets, file) => file.startsWith(`${targets.home}/`) ? `~/${relative(targets.home, file)}` : file;
|
|
254
|
-
const targetLabel = (e) => e.agent === 'router' ? `Shardflux · ${e.project}` : `[SF] ${e.project} · ${e.agent}`;
|
|
255
260
|
/** Where each managed connection of an entry lives: the SSH host block (and its Include), then each desktop app's file. */
|
|
256
261
|
function configuredTargets(targets, e, t3 = false) {
|
|
257
262
|
return [
|
package/dist/agent-targets.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export interface TargetEntry {
|
|
|
7
7
|
agent: string;
|
|
8
8
|
targets: DesktopTarget[];
|
|
9
9
|
}
|
|
10
|
+
/** What Claude Desktop and the Codex app list a target as: the project's router "Shardflux · <project>", an agent
|
|
11
|
+
* "<agent> · <project>" (the thing, then the place, as session titles are; no tag). */
|
|
12
|
+
export declare const targetLabel: (e: Pick<TargetEntry, "agent" | "project">) => string;
|
|
10
13
|
export declare function targetIdentity(key: string): Pick<TargetEntry, 'host' | 'workspace' | 'project' | 'agent'>;
|
|
11
14
|
/** Replace only a marked block, retaining every byte outside it. Put Includes before user Host blocks. */
|
|
12
15
|
export declare function managedBlock(text: string, body: string): string;
|
package/dist/agent-targets.js
CHANGED
|
@@ -15,6 +15,9 @@ const shellQuote = (s) => "'" + s.replaceAll("'", "'\\''").replaceAll('%', '%%')
|
|
|
15
15
|
const quote = (s) => '"' + s.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%') + '"';
|
|
16
16
|
// Some SSH config readers (T3 Code's host discovery) split Include on whitespace and keep quotes; quote only when needed.
|
|
17
17
|
const includePath = (s) => /^[^\s"'\\#%]+$/.test(s) ? s : quote(s);
|
|
18
|
+
/** What Claude Desktop and the Codex app list a target as: the project's router "Shardflux · <project>", an agent
|
|
19
|
+
* "<agent> · <project>" (the thing, then the place, as session titles are; no tag). */
|
|
20
|
+
export const targetLabel = (e) => e.agent === 'router' ? `Shardflux · ${e.project}` : `${e.agent} · ${e.project}`;
|
|
18
21
|
export function targetIdentity(key) {
|
|
19
22
|
const parts = key.split('/');
|
|
20
23
|
if (parts.length !== 2 || parts.some(p => !/^[a-z0-9][a-z0-9._-]{0,95}$/.test(p)))
|
|
@@ -280,7 +283,7 @@ export class AgentTargets {
|
|
|
280
283
|
if (!(prop in doc) && !manifest.created[file].includes(prop))
|
|
281
284
|
manifest.created[file].push(prop);
|
|
282
285
|
doc[prop] = [...kept, ...entries.filter(e => e.targets.includes(target)).map(e => {
|
|
283
|
-
const label = e
|
|
286
|
+
const label = targetLabel(e);
|
|
284
287
|
return target === 'claude-desktop' ? { id: e.host, name: label, sshHost: e.host, startDirectory: projectWorkspacePath({ slug: e.project }) } : { sshAlias: e.host, projects: [{ remotePath: projectWorkspacePath({ slug: e.project }), label }] };
|
|
285
288
|
})];
|
|
286
289
|
if (!doc[prop].length && manifest.created[file].includes(prop))
|
package/dist/agent-terminal.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ export interface TerminalConnection {
|
|
|
8
8
|
}
|
|
9
9
|
/** The workspace (or the VM under this terminal) is asleep: the user's next keystroke wakes it. */
|
|
10
10
|
export declare function sleepingAttach(error: unknown): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The wait before the next attach (0.16.0+): an attach refused with a Retry-After (503 `host_capacity` while the
|
|
13
|
+
* workspace's host makes room, 429 `quota_exceeded`) waits it with up to 20 % jitter; others back off from 100 ms to
|
|
14
|
+
* 2 s by consecutive failures, which reset once an attach delivers.
|
|
15
|
+
*/
|
|
16
|
+
export declare function reattachDelayMs(error: unknown, failures: number, random?: () => number): number;
|
|
17
|
+
/** The line an attach that waits for room prints (0.16.0+), or null for any other refusal. */
|
|
18
|
+
export declare function admissionNotice(error: unknown): string | null;
|
|
11
19
|
export declare function retryableAttach(error: unknown): boolean;
|
|
12
20
|
/** Offsets address bytes, including escape sequences and UTF-8 split across frames. */
|
|
13
21
|
export declare class PtyOutputCursor {
|
package/dist/agent-terminal.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { StringDecoder } from 'node:string_decoder';
|
|
2
|
-
import { PtyConnection, ShardfluxApiError, ShardfluxProtocolError, exitCodePosix } from '@shardflux/sdk';
|
|
2
|
+
import { PtyConnection, ShardfluxApiError, ShardfluxProtocolError, exitCodePosix, isAdmissionRefusal } from '@shardflux/sdk';
|
|
3
3
|
import { UsageError } from "./errors.js";
|
|
4
4
|
/** The workspace (or the VM under this terminal) is asleep: the user's next keystroke wakes it. */
|
|
5
5
|
export function sleepingAttach(error) {
|
|
6
6
|
return error instanceof ShardfluxApiError && error.code === 'workspace_not_running';
|
|
7
7
|
}
|
|
8
8
|
const ASLEEP_NOTICE = '\r\n\x1b[2m· asleep. Type to wake it.\x1b[0m\r\n';
|
|
9
|
+
/**
|
|
10
|
+
* The wait before the next attach (0.16.0+): an attach refused with a Retry-After (503 `host_capacity` while the
|
|
11
|
+
* workspace's host makes room, 429 `quota_exceeded`) waits it with up to 20 % jitter; others back off from 100 ms to
|
|
12
|
+
* 2 s by consecutive failures, which reset once an attach delivers.
|
|
13
|
+
*/
|
|
14
|
+
export function reattachDelayMs(error, failures, random = Math.random) {
|
|
15
|
+
const hinted = error instanceof ShardfluxApiError ? error.retryAfterSeconds : undefined;
|
|
16
|
+
if (hinted !== undefined && hinted > 0)
|
|
17
|
+
return Math.floor(hinted * 1000 * (1 + 0.2 * random()));
|
|
18
|
+
return Math.min(2_000, 100 * 2 ** Math.min(failures, 5));
|
|
19
|
+
}
|
|
20
|
+
/** The line an attach that waits for room prints (0.16.0+), or null for any other refusal. */
|
|
21
|
+
export function admissionNotice(error) {
|
|
22
|
+
if (!isAdmissionRefusal(error))
|
|
23
|
+
return null;
|
|
24
|
+
const position = typeof error.details?.queue_position === 'number' ? ` (position ${error.details.queue_position})` : '';
|
|
25
|
+
return error.reason === 'host_capacity' ? `waiting for room on the workspace's host${position}` : 'waiting for a free working slot: every workspace the plan lets work at once is busy';
|
|
26
|
+
}
|
|
9
27
|
export function retryableAttach(error) {
|
|
10
28
|
if (error instanceof ShardfluxProtocolError && /invalid|byte gap|omitted/.test(error.message))
|
|
11
29
|
return false;
|
|
@@ -164,6 +182,10 @@ export async function attachTerminal(options) {
|
|
|
164
182
|
controller.abort();
|
|
165
183
|
} });
|
|
166
184
|
let reconnects = 0;
|
|
185
|
+
// Consecutive failed attaches: the backoff's exponent, back to 0 once an attach delivers a message.
|
|
186
|
+
let failures = 0;
|
|
187
|
+
let lastError;
|
|
188
|
+
let notice = null;
|
|
167
189
|
let asleep = false;
|
|
168
190
|
try {
|
|
169
191
|
while (!signal.aborted) {
|
|
@@ -193,6 +215,9 @@ export async function attachTerminal(options) {
|
|
|
193
215
|
for await (const message of connection.messages()) {
|
|
194
216
|
if (signal.aborted)
|
|
195
217
|
break;
|
|
218
|
+
// The attach delivers: a later drop starts the backoff over, and the next wait for room is said again.
|
|
219
|
+
failures = 0;
|
|
220
|
+
notice = null;
|
|
196
221
|
const bytes = cursor.consume(message);
|
|
197
222
|
if (bytes?.length)
|
|
198
223
|
io.stdout.write(bytes);
|
|
@@ -212,6 +237,7 @@ export async function attachTerminal(options) {
|
|
|
212
237
|
asleep = true;
|
|
213
238
|
continue;
|
|
214
239
|
}
|
|
240
|
+
lastError = error;
|
|
215
241
|
}
|
|
216
242
|
finally {
|
|
217
243
|
const previous = connection;
|
|
@@ -220,7 +246,14 @@ export async function attachTerminal(options) {
|
|
|
220
246
|
await previous?.close();
|
|
221
247
|
}
|
|
222
248
|
reconnects += 1;
|
|
223
|
-
|
|
249
|
+
failures += 1;
|
|
250
|
+
// Waiting for room on the workspace's host (or a working slot): said once, and again when the position changes.
|
|
251
|
+
const waiting = admissionNotice(lastError);
|
|
252
|
+
if (waiting && waiting !== notice)
|
|
253
|
+
io.stderr.write(`\r\n\x1b[2m· ${waiting}\x1b[0m\r\n`);
|
|
254
|
+
notice = waiting;
|
|
255
|
+
await options.sleep(reattachDelayMs(lastError, failures), signal);
|
|
256
|
+
lastError = undefined;
|
|
224
257
|
}
|
|
225
258
|
if (inputFailure)
|
|
226
259
|
throw inputFailure;
|
package/dist/args.d.ts
CHANGED