@shardflux/cli 0.22.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/README.md +37 -10
  3. package/dist/acp-journal.js +58 -6
  4. package/dist/acp-login.d.ts +10 -3
  5. package/dist/acp-login.js +31 -4
  6. package/dist/acp-models.d.ts +6 -1
  7. package/dist/acp-models.js +21 -16
  8. package/dist/acp.js +60 -7
  9. package/dist/agent-commands.js +29 -15
  10. package/dist/agent-target-commands.d.ts +20 -1
  11. package/dist/agent-target-commands.js +49 -18
  12. package/dist/agent-targets.js +6 -1
  13. package/dist/args.d.ts +1 -1
  14. package/dist/args.js +8 -7
  15. package/dist/cli-proxy-forward.d.ts +0 -2
  16. package/dist/cli-proxy-forward.js +2 -9
  17. package/dist/cli-proxy-hub.d.ts +22 -30
  18. package/dist/cli-proxy-hub.js +65 -94
  19. package/dist/cli-proxy.d.ts +12 -38
  20. package/dist/cli-proxy.js +51 -168
  21. package/dist/codex-cloud.d.ts +9 -4
  22. package/dist/codex-cloud.js +47 -14
  23. package/dist/codex-linux.d.ts +14 -0
  24. package/dist/codex-linux.js +69 -0
  25. package/dist/command-center-actions.js +13 -4
  26. package/dist/command-center-model.d.ts +2 -0
  27. package/dist/command-center-model.js +6 -1
  28. package/dist/command-center.js +3 -3
  29. package/dist/commands.js +2 -2
  30. package/dist/conversation-terminal.js +3 -2
  31. package/dist/conversations.d.ts +14 -1
  32. package/dist/conversations.js +55 -6
  33. package/dist/desktop-linux.js +16 -3
  34. package/dist/errors.d.ts +2 -1
  35. package/dist/errors.js +5 -2
  36. package/dist/filesystem-commands.js +25 -3
  37. package/dist/menubar.d.ts +6 -5
  38. package/dist/menubar.js +38 -28
  39. package/dist/move-worktrees.d.ts +13 -7
  40. package/dist/move-worktrees.js +68 -17
  41. package/dist/move.js +20 -25
  42. package/dist/onboarding.js +4 -2
  43. package/dist/prepared-repository.d.ts +2 -0
  44. package/dist/prepared-repository.js +1 -1
  45. package/dist/prepared-repository.py +3 -2
  46. package/dist/project-preparation.d.ts +14 -0
  47. package/dist/project-preparation.js +16 -4
  48. package/dist/project-sync.d.ts +3 -0
  49. package/dist/project-sync.js +4 -1
  50. package/dist/t3-launch.d.ts +2 -1
  51. package/dist/t3-launch.js +11 -2
  52. package/dist/t3-proxy.d.ts +81 -0
  53. package/dist/t3-proxy.js +325 -0
  54. package/dist/terminal-apps.d.ts +11 -0
  55. package/dist/terminal-apps.js +18 -5
  56. package/dist/update.d.ts +2 -0
  57. package/dist/update.js +32 -6
  58. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,103 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.23.0 (2026-10-14)
4
+
5
+ - Codex Cloud enable and setup report when a ChatGPT sign-in is required, with `codex login` and `shard setup` as the next steps. Setup offers the sign-in action.
6
+
7
+ - On macOS, the installed CLI records a matching stable Node.js path for the menu bar, so Homebrew upgrades and
8
+ cleanup keep its actions available.
9
+ - On Linux, the desktop records the installed Node.js and CLI paths for its actions and existing autostart entry,
10
+ keeping them available after the launching shell's fnm multishell links expire.
11
+ - Opening a moved Claude or Codex conversation by its full key resolves the conversation from any folder, including
12
+ `--in auto` in the desktop menus and explicit app/web targets. A computer key opens its bound conversation's session.
13
+ - Move to cloud moves the session's checkout, uncommitted edits included, in any repository: also one with other
14
+ local branches, other worktrees or a worktree whose folder is gone. Those stay on this computer, so `shard move`,
15
+ the menu bar's and the Linux app's **Move to cloud** and the MCP tool go straight ahead without a scope choice
16
+ (`--this-worktree-only` is now the default). When the session also worked in another worktree, the move names it
17
+ in one line and `--json` lists it as `other_worktrees`. `--preserve-all-worktrees` skips registrations whose folder
18
+ is gone (`stale_registrations` in `--plan`); their branches move with the other local refs.
19
+ - Open Codex Cloud chats in the browser on every platform. `agents open --original` opens the original history; `--history <thread-id>` opens any earlier linked chat. JSON includes all previous history links.
20
+
21
+ - Opening a conversation from a coding agent's shell (Codex, Claude Code, Cursor) opens exactly one terminal, and the
22
+ conversation attaches there. The terminal no longer inherits the agent shell's variables, and a terminal Shardflux
23
+ opened never opens another, so it no longer reopens terminals in a loop that steals focus. A Codex Cloud conversation
24
+ opens in Codex before any terminal is considered, also when given its computer's key.
25
+ - Inherits completed forward-helper verification reuse from SDK 0.27.0 on a reused workspace client; each ownership epoch and helper version is verified again.
26
+ - Historical template file-list GETs wait for checksum-verified cache reconstruction, honoring Retry-After within the SDK admission deadline. File history and contents remain stored; POSTs never replay for file_list_indexing.
27
+
28
+ - Plan-derived API budgets (0.23.0+, SDK 0.27.0): `--json` error output includes `rate_limit` and `retry_after_seconds`; the text hint names the shared scope and delay. API request pacing follows the SDK admission timeout and preserves the request body. Reuse workspace credentials and reduce status polling when the returned budget asks for a delay.
29
+
30
+ - T3 Code conversations on Shardflux follow your T3 Code proxy setup (CLIProxyAPI, or any Claude- or
31
+ OpenAI-compatible proxy). What your T3 Code Claude provider runs with (its environment variables, and Claude Code's
32
+ settings.json `env`: `ANTHROPIC_BASE_URL`, its token, models and headers) reaches the conversation's Claude Code, and
33
+ your Codex `model_provider` (its `base_url`, `wire_api` and the key `env_key` names) reaches its Codex. CLIProxyAPI
34
+ on this computer (`127.0.0.1:8317`, or the port it had before `shard cli-proxy import`) is reached through your
35
+ hosted CLIProxyAPI, as on this computer; a proxy at another address is used as written. A Claude Code conversation
36
+ that goes through your proxy with its own token starts without a Shardflux Claude sign-in. T3 Code's default
37
+ provider is the one followed while it is enabled.
38
+ - `shard agents new --in t3` and `shard agents open t3` give the VM's own T3 Code (SSH and the T3 app) the same Claude
39
+ Code and Codex provider settings, with tokens in its secret store.
40
+ - Sign in on the Shardflux provider in T3 Code (`shard acp --login`) offers to move the CLIProxyAPI your T3 Code setup
41
+ uses into your account, so cloud conversations use it too. Claude Code that goes through it is connected then.
42
+ - Your hosted CLIProxyAPI routes requests across your logins exactly as its config says (round-robin, fill-first,
43
+ session affinity): `shard cli-proxy import` brings every login as it is on your computer, enabled or disabled, and
44
+ nothing changes them afterwards. `shard cli-proxy use <login>` serves a provider with that one login; `shard
45
+ cli-proxy use --all` uses every login again. `--use` on import is the same choice for a login you move.
46
+ - `ws exec-cancel` displays pending/unverified kernel drain and the follow command; `exec-read` keeps the real foreground state and reason.
47
+ - `shard template save` and `shard template` on your computer use the newest conversation this checkout started whose
48
+ VM is still there (a restored one too), else the project's workspace from before conversations. They no longer
49
+ refuse with "Run shard ./ for this project" after `shard ./`.
50
+ - `shard agents logs <conversation>` of a Codex Cloud conversation says its output is in its Codex thread and names
51
+ the commands that show it: `shard agents open <id>` opens the thread, `shard c wait <id>` waits for its reply and
52
+ `shard c get <id>` shows it.
53
+ - `shard agents new --in terminal --prompt <text>` (or `--prompt-file`, `--image`) starts a new conversation with that
54
+ first message, as without `--in`; a new Codex conversation says it opens in Codex Cloud. Before, it was refused
55
+ with a message that recommended the same command. A first message with `--name`, `--like`, an agent argument or
56
+ another `--in` target names the option it cannot be combined with.
57
+ - `shard projects sync add <path> --wait` reports a project setup that stopped as `project_setup_failed` (it was
58
+ `internal_error`): the message names the step and what it needs, such as a lockfile to commit,
59
+ `details.step` and `details.error_code` identify it, and the hint gives the way forward.
60
+ - `shard agents open` (and Open in the menus) of a Codex Cloud chat that switching the active Codex account or turning
61
+ Codex Cloud off and on stopped continues it in a new Codex chat of the same conversation, on its computer and branch,
62
+ instead of reporting that its executor was removed.
63
+ - ACP journal followers retry temporary lifecycle interruptions with bounded backoff and their saved sequence, without resubmitting a turn; permanent loss stops promptly.
64
+ - SSH inherits the SDK cold helper race repair.
65
+ - Inherits SDK 0.27.0 safe replay of truncated JSON lifecycle and exec answers.
66
+ - Uses SDK 0.27.0: workspace exec retries a lost start response with the same session ID.
67
+
68
+ - A T3 Code (`shard acp`) conversation starts the way a terminal one does: from its prepared revision, sending only
69
+ what the project cache lacks (nothing for a returning project), so its session is ready ~2.5–3 s sooner. Large files
70
+ still on their way stay out of the first file reconcile and land by themselves.
71
+ - A T3 Code (`shard acp`) conversation's first prompt reaches the agent ~1 s sooner: the agent's session starts while
72
+ the project files are reconciled, instead of after. `SHARDFLUX_TRACE_SEED=1` also marks each step of that
73
+ connection (`acp_*`). Each message to the agent up to 192 KiB (every one but a long prompt) now takes one request
74
+ to your VM instead of four.
75
+ - `shard agents open t3` and `shard agents new --in t3` with T3 Code 0.0.45 bring T3 up only once the workspace's T3
76
+ Connect link is linked: T3 lists the project only then. A link in progress is waited for (at most 30 s, within
77
+ `--wake-timeout`). A refused link is the `t3_link_failed` error with the API's remedy (for T3 Connect's environment
78
+ limit, Settings → Connections, with `details.recovery`); a link still in progress is `t3_link_pending`. Both carry
79
+ `workspace_key`, `conversation_id` and any started `t3_thread_id`, so the menu bar and the Linux app offer Open for
80
+ what exists rather than a new start. Before, `--json` answered `opened: true` with `t3_link.status: linking`.
81
+ - `shard agents open t3 <conversation>` for a Codex Cloud conversation opens its existing Codex chat, as
82
+ `shard agents open` does, instead of setting its VM up in T3 Code, which cannot show that chat. It neither wakes the
83
+ VM nor needs T3 Code.
84
+ - The Codex app on Linux: OpenAI's ChatGPT app for Linux (`chatgpt.desktop`) or a community build, found as the desktop
85
+ entry that handles codex://. `shard agents open <conversation>` and `shard agents open codex-app <conversation>` open
86
+ a Codex Cloud conversation's existing thread in it, through that entry, and `agents open codex-app` of a Cloud
87
+ conversation opens its thread on macOS too (it set up an SSH environment before). Its SSH environments stay a macOS
88
+ target.
89
+ - On a computer without the Codex app, opening or starting a Codex Cloud conversation says where its chat is,
90
+ whether it is waiting for your answer, and how to answer and follow it from here (`shard conversations send <id>
91
+ "<answer>"`, `shard conversations get <id>`), instead of failing to open its codex:// link. `--json` adds
92
+ `codex_app` and `reply_command`.
93
+ - The command center (`shard` with no arguments) lists a Codex Cloud chat as **needs you** or **working** from Codex
94
+ even while its computer is parked, and Enter opens it in Codex without waking that computer. Includes the Linux
95
+ desktop 0.1.10 payloads: Codex Cloud chats in the top-bar panel.
96
+ - `shard menubar start`, `stop` and `status` act on the Shardflux app for this config directory wherever it runs from
97
+ (menu bar 0.8.4+, one app per config directory), and an update stops that app before the new one starts, so no
98
+ older app stays behind. A start that races another one for the same directory hands over without opening the
99
+ launcher.
100
+
3
101
  ## 0.22.0 (2026-10-13)
4
102
 
5
103
  - **Starts send only what changed.** A new conversation's project travels as Git packs into a project cache its VMs
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @shardflux/cli: `shard`
2
2
 
3
+ Historical template file lists: reads queue checksum-verified reconstruction when a cold list is absent from the SQL cache. TypeScript SDK 0.27.0+ / Python 0.23.0+ wait full Retry-After for retryable API 409 file_list_indexing on GET/HEAD within the admission timeout. Template objects and version history stay available.
4
+
5
+ Plan-derived API budgets (0.23.0+, SDK 0.27.0): `--json` error output includes `rate_limit` and `retry_after_seconds`; the text hint names the shared scope and delay. API request pacing follows the SDK admission timeout and preserves the request body. Reuse workspace credentials and reduce status polling when the returned budget asks for a delay.
6
+
7
+ Exec survives a dropped connection (0.23.0+): a lost start response is retried with the same session ID and reattaches to the command that already started, and a cut-off JSON answer is retried within the same safe retry boundary.
8
+
3
9
  Suspension controls arrive in 0.22.0 (next coordinated release). `shard operations progress <operation-id>` reads the actual phase, host deadline, cancellation window and safe next action. `shard operations cancel <operation-id>` requests cancellation; `--wait --timeout 5m` waits for confirmed `suspend_canceled` recovery of the original workspace. Asynchronous acceptance is labeled separately. The owning host refuses cancellation outside validated preparation; after capture, wait for the outcome and resume if it succeeded. These commands never reset, replace or launch a provider turn. `--json` preserves the reported values; unavailable progress is explicit.
4
10
 
5
11
  The command line for [Shardflux](https://shardflux.dev): serverless VMs for AI agents. Open a
@@ -114,9 +120,13 @@ and Codex, skipping every step that is already done (0.15.0+). Editors that laun
114
120
  run the same sign-in as `shard --login` (0.22.0+).
115
121
  With T3 Code 0.0.45, `shard agents new --in t3` adds the project as an SSH environment instead.
116
122
 
123
+ ACP journal read reattachment uses bounded backoff through temporary lifecycle interruptions (0.23.0+). It keeps the saved sequence and never resubmits an uncertain turn.
124
+
117
125
  In CLI 0.22.0+, `shard setup --inspect` shows a stopped T3 Connect workspace link and its remedy without restarting
118
- the link or replacing your approved sign-in. A full T3 environment allowance needs a free slot: remove an unused
119
- environment in T3 Connect, then run `shard agents open t3 <workspace>` to request another link.
126
+ the link or replacing your approved sign-in. `shard agents open t3` brings T3 Code up once the workspace is linked
127
+ (0.23.0+): a refused link answers `t3_link_failed` with what to do (for T3 Connect's environment limit, remove an
128
+ environment in T3 Code → Settings → Connections, `details.recovery`), and a link still in progress after 30 seconds
129
+ answers `t3_link_pending`. Both name the workspace and conversation: open it again with `shard agents open t3`.
120
130
 
121
131
  Start another thread on a computer that is already set up (0.16.0+): it shares that conversation's installed
122
132
  packages, caches, running services, ports and files, and works on its own branch and worktree.
@@ -155,18 +165,20 @@ shard move --list # the project's sessions, and where moved one
155
165
  shard move --session 3d085e35-aa66-4866-9a53-f09719d6767c --detach --json
156
166
  ```
157
167
 
158
- CLI 0.22.0+ detects registered worktrees before migration. Inspect the exact paths and file bytes with
159
- `shard move --last --plan --json`, then choose `--preserve-all-worktrees` to include every registered worktree,
160
- all local refs, indices and ignored files, or `--this-worktree-only` to leave the others here explicitly.
161
- Preserve-all includes ignored dependency/build caches; tar headers add to the displayed file bytes and the VM
168
+ The session's checkout moves, uncommitted edits included; the repository's other worktrees and local branches stay
169
+ on this computer (0.23.0+; `--this-worktree-only` says so explicitly). When the session also worked in another
170
+ worktree, the move names it in one line (`other_worktrees` in `--json`). To bring every registered worktree, all
171
+ local refs, indices and ignored files too, inspect the exact paths and file bytes with
172
+ `shard move --last --plan --json`, then move with `--preserve-all-worktrees` (0.22.0+). Preserve-all includes ignored dependency/build caches; tar headers add to the displayed file bytes and the VM
162
173
  and project space need free disk for the copies. It never follows symlinks into unrelated sibling folders.
163
174
  The source checkout stays untouched. Cloud worktrees keep their staged/unstaged state; the selected tree runs on
164
175
  the conversation's branch while its original branch and HEAD remain in the recovery archive. Git location
165
176
  pointers are repaired for the VM. The result's `preserved.snapshot` names an immutable migration-time snapshot
166
177
  in the project space. Its `restore.py manifest.json <empty-folder>` command restores the original repository
167
178
  state into a new folder and refuses to overwrite continued work. It is a recovery snapshot, not an ongoing
168
- backup of later edits in other worktrees. Missing registrations, external object alternates, nested repositories
169
- and special files need resolution before preserve-all can upload.
179
+ backup of later edits in other worktrees. External object alternates, nested repositories and special files need
180
+ resolution before preserve-all can upload; a registration whose folder is gone has nothing to copy and is listed as
181
+ `stale_registrations`.
170
182
 
171
183
  A running turn finishes here first. Uncommitted edits come along, and your working tree, index and branch stay as
172
184
  they are. The cloud work comes back to your checkout by itself, and typing in the local Claude Code copy shows one line
@@ -187,7 +199,8 @@ The first message becomes a T3 thread and T3 comes to the front. With the Shardf
187
199
  cloud VM; with T3 Code 0.0.45 it runs on the T3 server in the project's VM, or with `--on` on that computer's.
188
200
  `--image` attaches images. `shard agents targets --json` shows T3 Code's mode, where each app's new conversations
189
201
  open, and every place each app can open (`open_in_targets`, 0.22.0+); `shard agents open t3 <conversation>` brings its
190
- thread back. Codex opens in a terminal app on macOS too (0.22.0+): `shard agents targets --open-in codex=terminal`.
202
+ thread back, and a Codex Cloud conversation's chat opens in your browser (0.23.0+). Answer a Codex Cloud
203
+ conversation with `shard conversations send <id> "<answer>"` (0.23.0+ names it when you open one). Codex opens in a terminal app on macOS too (0.22.0+): `shard agents targets --open-in codex=terminal`.
191
204
 
192
205
  `shard settings --title-prefix <text|none|default>` (0.22.0+) sets the marker your Claude conversations' titles start
193
206
  with in claude.ai and the Claude app (default `☁`: "☁ Fix login bug · todo-app"); `shard settings` shows it with where
@@ -317,7 +330,8 @@ answer) or their product (it runs VMs for its customers through the API, SDK or
317
330
 
318
331
  2. **Connect Claude and Codex.** Ask which they use (Claude Code and Codex / Claude Code / Codex). `shard agents
319
332
  credentials login claude --json` opens the claude.ai sign-in and a page for its code in their browser (0.22.0+);
320
- Codex uses this computer's login (`codex login`). `shard setup --inspect --connect --json --apps claude,codex`
333
+ Codex Cloud uses this computer's ChatGPT sign-in (`codex login`); copy it with `shard setup`.
334
+ `shard setup --inspect --connect --json --apps claude,codex`
321
335
  (0.15.0+) checks both live, copies the Codex login and starts the account's Codex remote device.
322
336
  3. **Set up the machine** every conversation starts from: ask "Set up your cloud machine from this computer?" (Yes /
323
337
  Not now), then `shard machine setup --from-this-computer --json` (0.20.0+): this computer's Claude Code or Codex
@@ -1596,3 +1610,16 @@ Explicit `agents new --in terminal`, `--in t3`, and `--like` launches keep their
1596
1610
  Save progress (0.22.0+): Machine and template saves show source size and an estimated duration, then queued/build state and phase updates immediately. With `--json`, progress events go to stderr and the final stdout document includes `preflight` and `phases`. Failure and timeout output retain the latest counters and `details.last_progress`, with the build id and recovery action. If an older API cannot expose the build, JSON stderr emits `save_started` with the version/build id and leaves the phase unknown. Missing final host reports preserve the last observed progress.
1597
1611
 
1598
1612
  Desktop launch cards (0.22.0+): the bundled desktop apps clear and close the launcher on submission, then show each pending start in a layered card stack near the top of the display. **Show all launches** expands individual statuses and recovery.
1613
+
1614
+
1615
+ ### Pending execution cancellation (0.23.0+)
1616
+
1617
+ On guests advertising `exec_cancel_pending.v1`, successful cancellation can return the actual `running` state
1618
+ with `canceled: true` while the kernel holds a killed process. The execution's `error` explains pending drain,
1619
+ and becomes unverified after 30 seconds without claiming an exit or freeing ownership. Read/follow that
1620
+ session until its real final status; repeat cancellation is idempotent. An already exited foreground keeps
1621
+ its result while descendant drain can remain pending. TS/Python agent tools and MCP expose the reason as
1622
+ `status_reason` and keep the exit code null while active. CLI prints the reason and follow command.
1623
+ Older clients read the same session response fields.
1624
+
1625
+ Cloud history (0.23.0+): `shard agents open <conversation-id> --original` opens the original chat; `--history <thread-id>` opens an earlier linked chat. `--json` includes `previous_threads` with each history URL. Sign in with the Codex account that owns the selected chat.
@@ -1,6 +1,18 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { StringDecoder } from 'node:string_decoder';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { ShardfluxApiError, ShardfluxProtocolError } from '@shardflux/sdk';
3
5
  import { AcpRpc } from "./acp-rpc.js";
6
+ import { reattachDelayMs } from "./agent-terminal.js";
7
+ const RECONNECT_BUDGET_MS = 120_000;
8
+ function retryableRead(error) {
9
+ if (error instanceof ShardfluxApiError)
10
+ return error.retryable || ['stale_epoch', 'token_expired', 'workspace_not_running', 'workspace_busy'].includes(error.code);
11
+ return error instanceof TypeError || error instanceof ShardfluxProtocolError && error.status === 0;
12
+ }
13
+ // An input this small (every RPC but a long prompt) travels in its command's start request: one request instead of a
14
+ // start, a stdin frame, an output stream and a cancel (0.23.0+).
15
+ const ONE_SHOT_INPUT = 192 << 10;
4
16
  // InputACP's private, same-uid socket receipt protocol. Stdin avoids Linux's 128 KiB per-argument limit.
5
17
  export const ACP_INPUT_SCRIPT = `import json, os, socket, sys
6
18
  payload = sys.stdin.buffer.readline((2 << 20) + 1024)
@@ -70,6 +82,19 @@ export class AcpJournal {
70
82
  async #deliver(inputId, message) {
71
83
  const dir = `/home/user/.local/share/shardflux/acp/${this.#conversation}`;
72
84
  const payload = Buffer.from(`{"mode":"input","input_id":${JSON.stringify(inputId)},"message":${message}}\n`);
85
+ if (payload.length <= ONE_SHOT_INPUT) {
86
+ const r = await this.#cell.exec.run(['python3', '-c', ACP_INPUT_SCRIPT, dir], { cwd: this.#cwd, stdin: payload, timeoutMs: 15_000, maxOutputBytes: 65536, signal: this.#signal });
87
+ let receipt;
88
+ try {
89
+ receipt = JSON.parse(r.stdout);
90
+ }
91
+ catch {
92
+ receipt = undefined;
93
+ }
94
+ if (r.exitCode !== 0 || receipt?.accepted !== true)
95
+ throw new Error('ACP delivery is unverified; load this session before continuing.');
96
+ return;
97
+ }
73
98
  const session = await this.#cell.exec.start({ argv: ['python3', '-c', ACP_INPUT_SCRIPT, dir], cwd: this.#cwd, stdin_open: true, timeout_ms: 15_000 }, this.#signal);
74
99
  let offset = 0, stdout = '';
75
100
  try {
@@ -167,17 +192,22 @@ export class AcpJournal {
167
192
  const signal = AbortSignal.any([this.#signal, reader.signal]);
168
193
  try {
169
194
  let failures = 0;
195
+ let outageStarted;
170
196
  while (this.#wanted() && !signal.aborted) {
171
197
  const decoder = new StringDecoder('utf8');
172
198
  let tail = '', offset = 0;
199
+ let interruption;
173
200
  try {
174
201
  const session = await this.#cell.exec.start({ argv: ['sf-agent', 'acp', 'attach', '--conversation', this.#conversation, '--from-seq', String(this.#cursor.from_seq), '--follow'], cwd: this.#cwd, timeout_ms: 0 }, signal);
175
202
  this.#follower = session.session_id;
176
203
  for await (const event of await this.#cell.exec.output(session.session_id, { follow: true, signal })) {
177
- if (event.type === 'error')
178
- throw new Error(event.error?.error.message ?? 'ACP journal failed');
204
+ if (event.type === 'error') {
205
+ if (event.error)
206
+ throw new ShardfluxApiError(502, event.error, 'cell');
207
+ throw new Error('ACP journal failed without an error envelope');
208
+ }
179
209
  if (event.type === 'exit')
180
- throw new Error('ACP journal detached');
210
+ throw new ShardfluxProtocolError('ACP journal detached', 0, 'cell');
181
211
  if (event.type !== 'output' || event.stream !== 'stdout' || !event.data)
182
212
  continue;
183
213
  const bytes = Buffer.from(event.data, 'base64'), at = event.offset ?? offset;
@@ -189,8 +219,12 @@ export class AcpJournal {
189
219
  for (let newline; (newline = tail.indexOf('\n')) >= 0;) {
190
220
  const line = tail.slice(0, newline);
191
221
  tail = tail.slice(newline + 1);
222
+ const before = this.#cursor.from_seq;
192
223
  await this.#entry(line);
193
- failures = 0;
224
+ if (this.#cursor.from_seq > before) {
225
+ failures = 0;
226
+ outageStarted = undefined;
227
+ }
194
228
  }
195
229
  if (Buffer.byteLength(tail) > 4 << 20)
196
230
  throw new Error('ACP journal frame exceeds 4 MiB');
@@ -198,17 +232,35 @@ export class AcpJournal {
198
232
  return;
199
233
  }
200
234
  if (!signal.aborted)
201
- throw new Error('ACP journal connection ended');
235
+ throw new ShardfluxProtocolError('ACP journal connection ended', 0, 'cell');
202
236
  }
203
237
  catch (error) {
204
238
  if (signal.aborted)
205
239
  break;
206
- if (++failures > 3)
240
+ if (!retryableRead(error))
207
241
  throw error;
242
+ outageStarted ??= performance.now();
243
+ if (performance.now() - outageStarted >= RECONNECT_BUDGET_MS)
244
+ throw new Error('ACP journal reconnect timed out; load this session to continue.', { cause: error });
245
+ failures++;
246
+ interruption = error;
208
247
  }
209
248
  finally {
210
249
  await this.#detach();
211
250
  }
251
+ // Retrying only the read follower cannot submit a second turn. Keep
252
+ // the original sequence/prompt cursor and stop waits on editor exit.
253
+ // A stopped reader (pause, idle) ends the wait as it ends an attach:
254
+ // the loop exits without closing the editor's connection.
255
+ if (this.#wanted() && !signal.aborted && interruption !== undefined) {
256
+ try {
257
+ await delay(Math.min(reattachDelayMs(interruption, failures), RECONNECT_BUDGET_MS - (performance.now() - outageStarted)), undefined, { signal });
258
+ }
259
+ catch (error) {
260
+ if (!signal.aborted)
261
+ throw error;
262
+ }
263
+ }
212
264
  }
213
265
  }
214
266
  catch (error) {
@@ -1,4 +1,5 @@
1
1
  import type { Ctx } from './commands.js';
2
+ import type { ImportResult, LocalCliProxy } from './cli-proxy.js';
2
3
  /**
3
4
  * The ACP terminal sign-in method. An editor runs it in its own terminal when the user chooses it, then reconnects:
4
5
  * T3 Code appends these args to the configured `shard acp` (`shard acp --login`); ACP registry clients run the agent
@@ -13,9 +14,15 @@ export declare const ACP_LOGIN_METHOD: {
13
14
  };
14
15
  /** Whether an API key is configured (no request; a revoked key surfaces as auth_required on its first call). */
15
16
  export declare function acpSignedIn(ctx: Ctx): boolean;
17
+ /** How the sign-in finds and moves this computer's CLIProxyAPI (tests replace both). */
18
+ export interface AcpLoginDeps {
19
+ detect?: (env: Record<string, string | undefined>) => Promise<LocalCliProxy | null>;
20
+ move?: (ctx: Ctx) => Promise<ImportResult>;
21
+ }
16
22
  /**
17
23
  * `shard acp --login`: everything a first conversation needs, each step skipped when it is already done: the
18
- * account (signed up or in with this computer's Codex login, else an email), its API key, the Codex login copy and the
19
- * cloud Claude sign-in.
24
+ * account (signed up or in with this computer's Codex login, else an email), its API key, the Codex login copy, the
25
+ * user's T3 Code proxy setup (a CLIProxyAPI on this computer, moved into the account when they say so) and the cloud
26
+ * Claude sign-in (not needed when Claude Code goes through that proxy with its own token).
20
27
  */
21
- export declare function runAcpLogin(ctx: Ctx): Promise<number>;
28
+ export declare function runAcpLogin(ctx: Ctx, deps?: AcpLoginDeps): Promise<number>;
package/dist/acp-login.js CHANGED
@@ -6,6 +6,8 @@ import { describeError } from "./errors.js";
6
6
  import { cloudClaudeLogin, loginClaudeInWorkspace } from "./claude-cloud-login.js";
7
7
  import { syncAgentCredentials } from "./agent-credentials.js";
8
8
  import { thisComputer } from "./this-computer.js";
9
+ import { detectLocalCliProxy, importCliProxy } from "./cli-proxy.js";
10
+ import { accountVmProxy, readT3ProxySetup } from "./t3-proxy.js";
9
11
  /**
10
12
  * The ACP terminal sign-in method. An editor runs it in its own terminal when the user chooses it, then reconnects:
11
13
  * T3 Code appends these args to the configured `shard acp` (`shard acp --login`); ACP registry clients run the agent
@@ -46,10 +48,11 @@ async function accountReady(ctx) {
46
48
  const say = (ctx, text) => { ctx.io.stderr.write(text); };
47
49
  /**
48
50
  * `shard acp --login`: everything a first conversation needs, each step skipped when it is already done: the
49
- * account (signed up or in with this computer's Codex login, else an email), its API key, the Codex login copy and the
50
- * cloud Claude sign-in.
51
+ * account (signed up or in with this computer's Codex login, else an email), its API key, the Codex login copy, the
52
+ * user's T3 Code proxy setup (a CLIProxyAPI on this computer, moved into the account when they say so) and the cloud
53
+ * Claude sign-in (not needed when Claude Code goes through that proxy with its own token).
51
54
  */
52
- export async function runAcpLogin(ctx) {
55
+ export async function runAcpLogin(ctx, deps = {}) {
53
56
  const { HANDLERS } = await import("./handlers.js");
54
57
  const home = ctx.io.env.HOME ?? homedir();
55
58
  const codex = existsSync(join(home, '.codex', 'auth.json'));
@@ -99,7 +102,31 @@ export async function runAcpLogin(ctx) {
99
102
  await syncAgentCredentials(ctx.cloud(), { home, redact: (value) => ctx.addSecret(value) });
100
103
  say(ctx, `Codex: connected with the login on ${thisComputer()}.\n`);
101
104
  }
102
- if (await cloudClaudeLogin(ctx))
105
+ // T3 Code's own providers go through CLIProxyAPI on this computer: cloud threads follow once it is in the account.
106
+ let plan = accountVmProxy(ctx);
107
+ if (plan.importable.length) {
108
+ const local = await (deps.detect ?? ((env) => detectLocalCliProxy({ env })))(ctx.io.env).catch(() => null);
109
+ if (local?.logins.length) {
110
+ const answer = (await ctx.input.answer('\nYour T3 Code setup uses CLIProxyAPI on this computer. Move it into your Shardflux account so cloud conversations use it too? Your logins and routing come along, and this computer keeps http://127.0.0.1:8317. [Y/n] '))?.trim().toLowerCase() ?? 'n';
111
+ if (answer === '' || answer.startsWith('y')) {
112
+ try {
113
+ // The answer above is the consent to stop the local copy too.
114
+ const moved = await (deps.move ?? ((c) => importCliProxy(c, { values: { yes: true } })))(ctx);
115
+ plan = accountVmProxy(ctx);
116
+ say(ctx, `CLIProxyAPI: in your account, ${moved.logins.active.length} login${moved.logins.active.length === 1 ? '' : 's'} serving. Cloud conversations use it.\n`);
117
+ }
118
+ catch (error) {
119
+ say(ctx, `CLIProxyAPI: ${describeError(error).message} Run: shard cli-proxy import\n`);
120
+ }
121
+ }
122
+ else
123
+ say(ctx, 'CLIProxyAPI: stays on this computer; cloud conversations use your Shardflux sign-ins. Move it any time: shard cli-proxy import\n');
124
+ }
125
+ }
126
+ const claude = readT3ProxySetup({ env: ctx.io.env }).claude;
127
+ if (plan.claudeCredential && claude)
128
+ say(ctx, `Claude Code: connected through ${claude.local ? 'your CLIProxyAPI' : new URL(claude.url).host}.\n`);
129
+ else if (await cloudClaudeLogin(ctx))
103
130
  say(ctx, 'Claude Code: connected.\n');
104
131
  else {
105
132
  const answer = (await ctx.input.answer('\nConnect Claude Code now? It opens a Claude sign-in page. [Y/n] '))?.trim().toLowerCase() ?? 'n';
@@ -24,11 +24,16 @@ export declare class AcpModelCache {
24
24
  read(scope: AcpScope): AcpCatalog;
25
25
  write(scope: AcpScope, catalog: AcpCatalog): void;
26
26
  }
27
- export declare function discoverAcpCatalog(ctx: Ctx, ws: Workspace, root: string): Promise<AcpCatalog>;
27
+ export declare function discoverAcpCatalog(ctx: Ctx, ws: Workspace, root: string, env?: Record<string, string>): Promise<AcpCatalog>;
28
28
  export interface AcpPreparation {
29
29
  allocated(conversation: Conversation, workspace: WorkspaceView): void;
30
30
  ready(workspace: Workspace): void;
31
31
  prepared(): void;
32
+ /** The user's T3 Code proxy setup (t3-proxy.ts): the adapters' variables, and the VM files they read, placed first. */
33
+ proxy?: {
34
+ env: Record<string, string>;
35
+ ready(workspace: Workspace): Promise<void>;
36
+ };
32
37
  }
33
38
  export interface AcpLifecycle {
34
39
  prepare(ctx: Ctx, folder: string, name: string, callbacks: AcpPreparation): Promise<AcpCatalog>;
@@ -1,13 +1,13 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { join, posix } from 'node:path';
4
4
  import { projectIdentity } from "./project-identity.js";
5
- import { localChanges, seedAgentRepository } from "./agent-repository.js";
6
- import { checkoutConversationBranch, createConversationRecord, conversationWorkspace } from "./conversations.js";
5
+ import { localChanges } from "./agent-repository.js";
6
+ import { createConversationRecord, conversationWorkspace, seedConversation } from "./conversations.js";
7
+ import { seedPhase } from "./seed-phases.js";
7
8
  import { AgentStateStore } from "./agent-state.js";
8
9
  import { objectValue } from "./acp-rpc.js";
9
10
  import { nativeCurrentModel, nativeModelOptions } from "./acp-catalog.js";
10
- import { sendTwinSeed } from "./worktree-twin.js";
11
11
  /** No prompts, login, logout or credential writes. Every child process group is
12
12
  * terminated, including initialization failures and interrupted clients. */
13
13
  export const ACP_MODELS_SCRIPT = String.raw `
@@ -115,9 +115,9 @@ export class AcpModelCache {
115
115
  });
116
116
  }
117
117
  }
118
- export async function discoverAcpCatalog(ctx, ws, root) {
118
+ export async function discoverAcpCatalog(ctx, ws, root, env = {}) {
119
119
  const result = await ws.cell().exec.run(['node', '-e', ACP_MODELS_SCRIPT], { cwd: root, timeoutMs: 55_000,
120
- signal: ctx.signal, maxOutputBytes: 1 << 20, env: { SF_ACP_MODEL_DISCOVERY: '1' } });
120
+ signal: ctx.signal, maxOutputBytes: 1 << 20, env: { ...env, SF_ACP_MODEL_DISCOVERY: '1' } });
121
121
  if (result.exitCode !== 0)
122
122
  throw new Error(`Guest model discovery failed: ${result.stderr.trim()}`);
123
123
  const discovered = JSON.parse(result.stdout), catalog = {};
@@ -140,25 +140,30 @@ export const ACP_LIFECYCLE = { async prepare(ctx, folder, name, callbacks) {
140
140
  const changes = localChanges(folder, ctx.signal);
141
141
  changes.catch(() => undefined);
142
142
  const ws = await conversationWorkspace(ctx, c);
143
+ seedPhase('vm_running');
143
144
  callbacks.ready(ws);
144
- await ws.cell().files.mkdir(c.worktree_path, { parents: true });
145
- const background = [];
146
145
  let initialReady, initialFailed;
147
146
  const initialFiles = new Promise((resolve, reject) => { initialReady = resolve; initialFailed = reject; });
148
147
  // The adapters read the project's own agent settings when it has any: discovery then waits for those files. A
149
- // project without them is discovered while it seeds (0.21.1+): the catalog depends on the account, not the files.
148
+ // project without them is discovered while it seeds (0.21.1+), beside the worktree rather than in it (a prepared
149
+ // worktree is moved into place whole): the catalog depends on the account, not the files.
150
150
  const projectSettings = ACP_INITIAL_FILES.some(path => existsSync(join(folder, path)));
151
+ // The same seed as a terminal start (0.22.0+): the prepared revision, only what the project cache lacks, or the copy
152
+ // seed. Media land before the reply and those still on their way stay out of the first file reconcile (acp.ts).
151
153
  const seeded = (async () => {
152
- await seedAgentRepository(ctx, ws, folder, { root: c.worktree_path, populate: true, changes, background: started => background.push(started),
153
- initialFiles: ACP_INITIAL_FILES, onInitialFiles: initialReady });
154
- await sendTwinSeed(ctx.store.dir, ws.cell(), c, folder, ctx.signal);
155
- await checkoutConversationBranch(ctx, ws.cell(), c);
156
- await Promise.all(background);
154
+ await seedConversation(ctx, c, ws, folder, undefined, changes, undefined, { initialFiles: ACP_INITIAL_FILES, onInitialFiles: initialReady, landNow: true });
157
155
  callbacks.prepared();
158
156
  })();
159
157
  void seeded.catch(initialFailed);
160
- const models = (async () => { if (projectSettings)
161
- await initialFiles; return discoverAcpCatalog(ctx, ws, c.worktree_path); })();
158
+ const proxied = callbacks.proxy?.ready(ws) ?? Promise.resolve();
159
+ const models = (async () => {
160
+ await proxied;
161
+ if (projectSettings) {
162
+ await initialFiles;
163
+ return discoverAcpCatalog(ctx, ws, c.worktree_path, callbacks.proxy?.env);
164
+ }
165
+ return discoverAcpCatalog(ctx, ws, posix.dirname(c.worktree_path), callbacks.proxy?.env);
166
+ })();
162
167
  // Observe both failures immediately. Never start a second VM, and never delete
163
168
  // the user's conversation VM as catalog cleanup.
164
169
  const results = await Promise.allSettled([seeded, models]);