remote-codex 0.11.36 → 0.11.38

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/README.md CHANGED
@@ -5,9 +5,19 @@
5
5
 
6
6
  ## English
7
7
 
8
- Remote Codex is a web, Android, and iOS supervisor for Codex workspaces and
9
- threads. It can run locally, over Tailscale/LAN, or through a public relay so a
10
- private machine can connect outward without opening inbound ports.
8
+ Remote Codex is a self-hosted control plane for long-running coding agents. It
9
+ lets you open the same workspace and continue the same thread from a browser,
10
+ Android phone, iPhone, tablet, or another computer without moving the actual
11
+ agent process away from the machine that owns your code.
12
+
13
+ Start a task at your desk, check its progress from your phone, answer an
14
+ approval request while away, and continue from another computer later. The
15
+ thread remains one continuous session with the same history, workspace, files,
16
+ runtime state, and pending actions.
17
+
18
+ Remote Codex can run locally, over Tailscale or LAN, or through a public relay.
19
+ In relay mode, private machines connect outward, so you do not need to expose a
20
+ home or office workstation through an inbound port.
11
21
 
12
22
  ### Downloads
13
23
 
@@ -15,13 +25,98 @@ private machine can connect outward without opening inbound ports.
15
25
  - iOS IPA: [RemoteCodex.ipa](https://github.com/dufangshi/remoteCodex/releases/latest/download/RemoteCodex.ipa)
16
26
  - npm CLI: `npm install -g remote-codex`
17
27
 
28
+ ### Why Remote Codex
29
+
30
+ | Capability | What it means in practice |
31
+ | --- | --- |
32
+ | One thread on every screen | Move between desktop, web, Android, and iOS without restarting the task or losing conversation history. |
33
+ | Client disconnects do not cancel work | The agent runs on the connected device that owns the workspace. A phone losing signal, a browser refresh, or a closed laptop client does not stop the task, as long as the execution device and its agent runtime remain online. |
34
+ | Your code stays on your machine | The supervisor works against the real local workspace. Relay mode forwards authorized traffic instead of copying the entire development environment into a hosted editor. |
35
+ | Share only what is needed | Share a thread, selected workspaces, or a device with a friend. Choose read or control access and revoke it when collaboration is finished. |
36
+ | Built for active agent work | Follow streaming output, answer approval questions, inspect tool activity, upload context, browse files, and recover from errors from the same interface. |
37
+ | Multiple ways to connect | Use local mode, private LAN or Tailscale access, or a public relay with outbound-only device connections. |
38
+
39
+ ### Continue Anywhere, Keep Running
40
+
41
+ The frontend is a control surface, not the place where the agent process lives.
42
+ Closing or disconnecting a client does not send a stop command to the thread.
43
+
44
+ ```mermaid
45
+ flowchart LR
46
+ A["Desktop browser"] --> R["Remote Codex relay"]
47
+ B["Android or iOS"] --> R
48
+ C["Another computer"] --> R
49
+ R --> D["Connected execution device"]
50
+ D --> T["Agent runtime and thread"]
51
+ D --> W["Local workspace and files"]
52
+ ```
53
+
54
+ If a mobile client enters a tunnel, changes networks, or temporarily goes
55
+ offline, the execution device can keep the thread running. Reconnect from any
56
+ client to load the latest transcript and continue from the current state.
57
+
18
58
  ### Features
19
59
 
20
- - Relay mode for web/mobile access to private workspaces.
21
- - Device-scoped routes, shared sessions, and per-device workspace/thread lists.
22
- - Codex thread chat with streaming steps, attachments, exports, and workspace explorer.
23
- - Workspace file browsing, preview, upload/download, edit, and image preview.
24
- - Android, iOS, web, Docker, and npm CLI entrypoints.
60
+ #### Threads and agent control
61
+
62
+ - Create, resume, rename, pin, fork, archive, and delete threads.
63
+ - Follow streamed assistant output, reasoning summaries, tool calls, status,
64
+ token usage, and completion state.
65
+ - Send follow-up instructions while a turn is active and answer pending
66
+ approvals or structured questions remotely.
67
+ - Select agent backend, model, reasoning effort, collaboration mode, and
68
+ approval policy when starting work.
69
+ - Work with Codex, Claude, and OpenCode backends through one supervisor surface.
70
+ - Upload attachments and export transcripts as PDF or HTML.
71
+
72
+ #### Workspaces and files
73
+
74
+ - Register trusted project roots and keep threads grouped by workspace.
75
+ - Browse the workspace tree beside the conversation.
76
+ - Preview text, Markdown, code, images, PDFs, and other supported artifacts.
77
+ - Upload, download, edit, move, and delete workspace files where access permits.
78
+ - Use thread-aware file views to inspect the exact project the agent is changing.
79
+
80
+ #### Devices, relay, and collaboration
81
+
82
+ - Connect private workstations to a relay using an outbound WebSocket tunnel.
83
+ - Keep separate workspace and thread inventories for each connected device.
84
+ - Share a single thread for focused review, selected workspaces for ongoing
85
+ collaboration, or a device when broader access is appropriate.
86
+ - Choose read-only or control access, inspect share activity, and revoke access.
87
+ - Sign in with relay accounts, with optional Google or GitHub OAuth when enabled
88
+ by the relay administrator.
89
+ - Manage users, registrations, devices, shares, and hosted-device state from the
90
+ relay administration surface.
91
+
92
+ #### Clients and deployment
93
+
94
+ - Responsive web client plus native Android and iOS applications.
95
+ - Local CLI mode for a private workstation.
96
+ - Docker and GHCR deployment for the relay server.
97
+ - Tailscale/LAN access for fully private networks.
98
+ - Public relay access without opening inbound ports on execution devices.
99
+
100
+ ### Product Screenshot
101
+
102
+ The thread timeline, composer, workspace tree, and file preview stay together so
103
+ you can discuss a change and inspect the affected files without switching tools.
104
+
105
+ ![Remote Codex thread timeline with workspace explorer](docs/images/remote-codex-thread-workspace.png)
106
+
107
+ ### What Keeps Running
108
+
109
+ Remote Codex separates three concerns:
110
+
111
+ 1. **Client:** the browser or mobile app used to view and control work.
112
+ 2. **Relay:** the optional authenticated routing layer between clients and
113
+ private devices.
114
+ 3. **Execution device:** the machine that owns the workspace and runs the agent.
115
+
116
+ A client connection can come and go. Work continues while the execution device,
117
+ its supervisor, and the underlying agent runtime remain available. Explicitly
118
+ stopping a turn, stopping the supervisor, rebooting the execution device, or
119
+ losing its network connection can still interrupt availability.
25
120
 
26
121
  ### Run A Relay Server
27
122
 
@@ -156,7 +251,7 @@ pnpm test
156
251
  After building an APK and IPA locally:
157
252
 
158
253
  ```bash
159
- pnpm release:mobile -- --tag v0.11.23 \
254
+ pnpm release:mobile -- --tag v0.11.36 \
160
255
  --apk apps/android/app/build/outputs/apk/release/app-release.apk \
161
256
  --ipa apps/ios/build/RemoteCodex.ipa
162
257
  ```
@@ -168,9 +263,15 @@ The uploaded asset names stay stable:
168
263
 
169
264
  ## 中文
170
265
 
171
- Remote Codex Codex workspace 和 thread 的 Web、Android、iOS 控制台。它可以
172
- 本地运行,也可以通过 Tailscale/LAN 访问;relay 模式下,私有机器只需要主动连到
173
- 公网 relay,不需要开放入站端口。
266
+ Remote Codex 是面向长时间运行编程 Agent 的自托管控制平台。你可以从浏览器、
267
+ Android、iPhone、平板或另一台电脑打开同一个 workspace,并继续同一个 thread,
268
+ 而真正的 Agent 进程仍运行在拥有代码和开发环境的那台设备上。
269
+
270
+ 你可以在电脑前启动任务,离开后用手机查看进度、处理授权请求,再从另一台电脑继续。
271
+ 整个过程仍是同一个 session,保留完整对话、workspace、文件、运行状态和待处理操作。
272
+
273
+ Remote Codex 支持本机、Tailscale/LAN 和公网 Relay 三种访问方式。Relay 模式下,
274
+ 私有设备主动向外建立连接,不需要给家里或办公室的开发机开放入站端口。
174
275
 
175
276
  ### 下载
176
277
 
@@ -178,13 +279,89 @@ Remote Codex 是 Codex workspace 和 thread 的 Web、Android、iOS 控制台。
178
279
  - iOS IPA: [RemoteCodex.ipa](https://github.com/dufangshi/remoteCodex/releases/latest/download/RemoteCodex.ipa)
179
280
  - npm CLI: `npm install -g remote-codex`
180
281
 
282
+ ### 为什么使用 Remote Codex
283
+
284
+ | 能力 | 实际价值 |
285
+ | --- | --- |
286
+ | 一个 Thread,多端无缝续接 | 在电脑、网页、Android 和 iOS 之间切换,不需要重新启动任务,也不会丢失上下文。 |
287
+ | 前端断网不等于任务中断 | Agent 在真正拥有 workspace 的执行设备上运行。手机弱网、浏览器刷新或客户端电脑合盖不会自动停止任务,只要执行设备及其 Agent runtime 仍在线。 |
288
+ | 代码保留在自己的设备 | Supervisor 直接操作真实的本地 workspace。Relay 只转发经过授权的请求,不要求把整个开发环境迁移到云端编辑器。 |
289
+ | 精细分享,更高效协作 | 可以只分享一个 thread、指定 workspace,或在确实需要时分享整个 device;支持只读、控制和随时撤销。 |
290
+ | 针对 Agent 工作流设计 | 在一个界面中查看流式输出、处理授权、检查工具调用、上传资料、浏览文件和恢复错误。 |
291
+ | 部署方式灵活 | 可选择本机模式、Tailscale/LAN 私网模式,或让私有设备主动连接公网 Relay。 |
292
+
293
+ ### 随处继续,任务持续运行
294
+
295
+ 前端客户端只是控制界面,并不是 Agent 进程实际运行的位置。关闭客户端或临时断线,
296
+ 不会自动向 thread 发送停止命令。
297
+
298
+ ```mermaid
299
+ flowchart LR
300
+ A["电脑浏览器"] --> R["Remote Codex Relay"]
301
+ B["Android 或 iOS"] --> R
302
+ C["另一台电脑"] --> R
303
+ R --> D["在线执行设备"]
304
+ D --> T["Agent Runtime 与 Thread"]
305
+ D --> W["本地 Workspace 与文件"]
306
+ ```
307
+
308
+ 手机进入隧道、切换 Wi-Fi/蜂窝网络或短暂离线时,只要执行设备仍保持网络连接,
309
+ 任务就可以继续运行。任意客户端重新连接后,会加载最新 transcript,并从当前状态继续。
310
+
181
311
  ### 功能
182
312
 
183
- - Relay 模式:网页和移动端访问私有 workspace。
184
- - 按设备隔离 URL、workspace、thread,并支持 session 分享。
185
- - Codex thread 聊天、流式步骤、图片/附件、导出和 workspace explorer
186
- - 文件浏览、预览、上传/下载、编辑和图片预览。
187
- - Web、Android、iOS、Docker、npm CLI 多入口。
313
+ #### Thread Agent 控制
314
+
315
+ - 创建、继续、重命名、置顶、fork、归档和删除 thread。
316
+ - 查看流式回答、reasoning 摘要、工具调用、运行状态、token 使用和完成状态。
317
+ - Agent 运行期间继续发送指令,并远程处理授权请求或结构化问题。
318
+ - 创建任务时选择 Agent backend、模型、reasoning effort、协作模式和授权策略。
319
+ - 在同一个 Supervisor 界面使用 Codex、Claude 和 OpenCode backend。
320
+ - 上传附件,并将 transcript 导出为 PDF 或 HTML。
321
+
322
+ #### Workspace 与文件
323
+
324
+ - 注册可信项目目录,并按 workspace 管理相关 thread。
325
+ - 在对话旁直接浏览 workspace 文件树。
326
+ - 预览文本、Markdown、代码、图片、PDF 和其他受支持的 artifact。
327
+ - 在权限允许时上传、下载、编辑、移动和删除文件。
328
+ - 使用 thread 关联的文件视图,快速核对 Agent 正在修改的真实项目。
329
+
330
+ #### Device、Relay 与协作
331
+
332
+ - 通过主动向外建立的 WebSocket tunnel,把私有工作站连接到 Relay。
333
+ - 每个 device 独立管理自己的 workspace 和 thread 列表。
334
+ - 分享单个 thread 做定向 review,分享指定 workspace 做持续协作,或按需分享 device。
335
+ - 配置只读或控制权限,查看访问记录,并随时撤销分享。
336
+ - 使用 Relay 账号登录;管理员配置后可启用 Google 或 GitHub OAuth。
337
+ - 在 Relay 管理界面管理用户、注册、设备、分享和 hosted device 状态。
338
+
339
+ #### 客户端与部署
340
+
341
+ - 响应式 Web 客户端,以及原生 Android、iOS 应用。
342
+ - 面向私有工作站的本地 CLI 模式。
343
+ - 支持 Docker 和 GHCR 镜像部署 Relay。
344
+ - 支持完全私有的 Tailscale/LAN 访问。
345
+ - 使用公网 Relay 时,执行设备不需要开放入站端口。
346
+
347
+ ### 产品截图
348
+
349
+ Thread timeline、输入区、workspace 文件树和文件预览位于同一工作界面,讨论改动时可以
350
+ 立即检查相关文件,无需在多个工具之间来回切换。
351
+
352
+ ![Remote Codex Thread 与 Workspace Explorer](docs/images/remote-codex-thread-workspace.png)
353
+
354
+ ### 哪些组件必须保持在线
355
+
356
+ Remote Codex 将系统分成三个部分:
357
+
358
+ 1. **客户端:** 用于查看和控制任务的浏览器或移动应用。
359
+ 2. **Relay:** 可选的鉴权和路由层,连接客户端与私有设备。
360
+ 3. **执行设备:** 真正保存 workspace 并运行 Agent 的机器。
361
+
362
+ 客户端可以随时连接或断开。只要执行设备、Supervisor 和底层 Agent runtime 仍可用,
363
+ 任务就会继续。显式停止 turn、停止 Supervisor、重启执行设备,或执行设备本身断网,
364
+ 仍可能中断可访问性或任务执行。
188
365
 
189
366
  ### 启动 Relay 服务器
190
367
 
@@ -315,7 +492,7 @@ pnpm test
315
492
  本地构建好 APK 和 IPA 后:
316
493
 
317
494
  ```bash
318
- pnpm release:mobile -- --tag v0.11.23 \
495
+ pnpm release:mobile -- --tag v0.11.36 \
319
496
  --apk apps/android/app/build/outputs/apk/release/app-release.apk \
320
497
  --ipa apps/ios/build/RemoteCodex.ipa
321
498
  ```
@@ -6900,6 +6900,13 @@ function createThreadPendingSteerRecord(db, input) {
6900
6900
  function deleteThreadPendingSteerRecordById(db, id) {
6901
6901
  db.delete(threadPendingSteers).where(eq(threadPendingSteers.id, id)).run();
6902
6902
  }
6903
+ function updateThreadPendingSteerRecordDelivery(db, id, delivery, turnId) {
6904
+ db.update(threadPendingSteers).set({
6905
+ delivery,
6906
+ turnId,
6907
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
6908
+ }).where(eq(threadPendingSteers.id, id)).run();
6909
+ }
6903
6910
  function deleteThreadPendingSteerRecordsByThreadId(db, threadId) {
6904
6911
  db.delete(threadPendingSteers).where(eq(threadPendingSteers.threadId, threadId)).run();
6905
6912
  }
@@ -10403,7 +10410,46 @@ function mapModel2(model) {
10403
10410
  };
10404
10411
  }
10405
10412
  function mapTurn2(turn) {
10406
- return codexTurnToAgentTurn(turn);
10413
+ const mapped = codexTurnToAgentTurn(turn);
10414
+ return turn.error ? {
10415
+ ...mapped,
10416
+ error: { message: codexTurnErrorMessage(turn.error) }
10417
+ } : mapped;
10418
+ }
10419
+ function nestedUpstreamErrorMessage(value) {
10420
+ if (!value) {
10421
+ return null;
10422
+ }
10423
+ try {
10424
+ const parsed = JSON.parse(value);
10425
+ if (typeof parsed !== "object" || parsed === null) {
10426
+ return null;
10427
+ }
10428
+ const error = "error" in parsed ? parsed.error : parsed;
10429
+ if (typeof error !== "object" || error === null || !("message" in error)) {
10430
+ return null;
10431
+ }
10432
+ return typeof error.message === "string" && error.message.trim() ? error.message.trim() : null;
10433
+ } catch {
10434
+ return null;
10435
+ }
10436
+ }
10437
+ function codexTurnErrorMessage(error) {
10438
+ const message = error.message?.trim() || "Turn failed unexpectedly.";
10439
+ const details = error.additionalDetails?.trim();
10440
+ if (!details) {
10441
+ return message;
10442
+ }
10443
+ const upstreamMessage = nestedUpstreamErrorMessage(details);
10444
+ if (upstreamMessage && !message.includes(upstreamMessage)) {
10445
+ return upstreamMessage;
10446
+ }
10447
+ if (message.includes(details)) {
10448
+ return message;
10449
+ }
10450
+ return `${message}
10451
+
10452
+ ${details}`;
10407
10453
  }
10408
10454
  function mapGoal(goal) {
10409
10455
  return {
@@ -10610,12 +10656,15 @@ function mapCodexNotification(event) {
10610
10656
  }
10611
10657
  case "error": {
10612
10658
  const params = event.params;
10659
+ if (params.willRetry) {
10660
+ return null;
10661
+ }
10613
10662
  return {
10614
10663
  type: "turn.failed",
10615
10664
  provider: "codex",
10616
10665
  providerSessionId: params.threadId,
10617
10666
  providerTurnId: params.turnId,
10618
- error: params.error.message ?? "Turn failed unexpectedly.",
10667
+ error: codexTurnErrorMessage(params.error),
10619
10668
  willRetry: params.willRetry
10620
10669
  };
10621
10670
  }
@@ -16321,6 +16370,11 @@ var ThreadAuxiliaryStateStore = class {
16321
16370
  (record) => record.turnId === turnId
16322
16371
  );
16323
16372
  }
16373
+ listQueuedContinuationRecords(localThreadId) {
16374
+ return listThreadPendingSteerRecordsByThreadId(this.db, localThreadId).filter(
16375
+ (record) => record.delivery === "continuation"
16376
+ );
16377
+ }
16324
16378
  findPendingSteerRecord(localThreadId, id) {
16325
16379
  return listThreadPendingSteerRecordsByThreadId(this.db, localThreadId).find(
16326
16380
  (record) => record.id === id
@@ -16334,6 +16388,9 @@ var ThreadAuxiliaryStateStore = class {
16334
16388
  (record) => record.delivery === "continuation"
16335
16389
  );
16336
16390
  }
16391
+ hasQueuedContinuations(localThreadId) {
16392
+ return this.listQueuedContinuationRecords(localThreadId).length > 0;
16393
+ }
16337
16394
  deletePendingSteerRecord(localThreadId, id, turnId) {
16338
16395
  deleteThreadPendingSteerRecordById(this.db, id);
16339
16396
  this.callbacks.invalidateThreadDetailCache(localThreadId);
@@ -17848,12 +17905,14 @@ var ThreadRuntimeEventProjector = class {
17848
17905
  if (!record) {
17849
17906
  return;
17850
17907
  }
17851
- updateThreadRecord(db, record.id, {
17852
- status: event.status
17853
- });
17908
+ const status = event.status === "idle" && record.providerTurnId ? "running" : event.status;
17909
+ updateThreadRecord(db, record.id, { status });
17854
17910
  callbacks.emitThreadEvent("thread.updated", record.id, {
17855
- status: getThreadRecordById(db, record.id)?.status ?? record.status
17911
+ status: getThreadRecordById(db, record.id)?.status ?? status
17856
17912
  });
17913
+ if (status === "idle") {
17914
+ callbacks.scheduleQueuedContinuationDrain(record.id);
17915
+ }
17857
17916
  return;
17858
17917
  }
17859
17918
  case "session.title.updated": {
@@ -18131,12 +18190,21 @@ var ThreadRuntimeEventProjector = class {
18131
18190
  const rawTurnId = event.turn.providerTurnId;
18132
18191
  const turnId = liveState.displayTurnIdForRuntimeTurn(record.id, rawTurnId) ?? rawTurnId;
18133
18192
  const turnItems = event.turn.items;
18193
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
18194
+ if (event.turn.status === "failed") {
18195
+ this.persistTurnFailureItem(
18196
+ record.id,
18197
+ turnId,
18198
+ event.turn.error?.message ?? "Unable to complete the turn.",
18199
+ completedAt
18200
+ );
18201
+ }
18134
18202
  updateThreadRecord(db, record.id, {
18135
18203
  providerTurnId: null,
18136
18204
  activeTurnCollaborationMode: null,
18137
18205
  status: event.turn.status === "failed" ? "failed" : event.turn.status === "interrupted" ? "interrupted" : "idle",
18138
18206
  lastError: event.turn.error?.message ?? null,
18139
- lastTurnCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
18207
+ lastTurnCompletedAt: completedAt
18140
18208
  });
18141
18209
  callbacks.persistFinalTurnOrderingHints(record.id, turnId, turnItems);
18142
18210
  callbacks.persistRuntimeTurnItemsAsDisplayTurn(record.id, rawTurnId, turnId, turnItems);
@@ -18174,7 +18242,7 @@ var ThreadRuntimeEventProjector = class {
18174
18242
  );
18175
18243
  liveState.clearRuntimeDisplayTurnMapping(record.id, rawTurnId);
18176
18244
  if (preservePendingSteers) {
18177
- callbacks.scheduleQueuedContinuationDrain(record.id, turnId);
18245
+ callbacks.scheduleQueuedContinuationDrain(record.id);
18178
18246
  }
18179
18247
  return;
18180
18248
  }
@@ -18187,12 +18255,14 @@ var ThreadRuntimeEventProjector = class {
18187
18255
  return;
18188
18256
  }
18189
18257
  const turnId = liveState.displayTurnIdForRuntimeTurn(record.id, event.providerTurnId) ?? event.providerTurnId;
18258
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
18259
+ this.persistTurnFailureItem(record.id, turnId, event.error, failedAt);
18190
18260
  updateThreadRecord(db, record.id, {
18191
18261
  providerTurnId: null,
18192
18262
  activeTurnCollaborationMode: null,
18193
18263
  status: "failed",
18194
18264
  lastError: event.error,
18195
- lastTurnCompletedAt: (/* @__PURE__ */ new Date()).toISOString()
18265
+ lastTurnCompletedAt: failedAt
18196
18266
  });
18197
18267
  liveState.setLivePlan(record.id, null);
18198
18268
  liveState.setLiveItems(record.id, null);
@@ -18208,7 +18278,9 @@ var ThreadRuntimeEventProjector = class {
18208
18278
  error: event.error,
18209
18279
  ...event.willRetry !== void 0 ? { willRetry: event.willRetry } : {}
18210
18280
  });
18211
- liveState.clearRuntimeDisplayTurnMapping(record.id, event.providerTurnId);
18281
+ if (event.provider !== "codex") {
18282
+ liveState.clearRuntimeDisplayTurnMapping(record.id, event.providerTurnId);
18283
+ }
18212
18284
  return;
18213
18285
  }
18214
18286
  }
@@ -18220,6 +18292,25 @@ var ThreadRuntimeEventProjector = class {
18220
18292
  providerSessionId
18221
18293
  );
18222
18294
  }
18295
+ persistTurnFailureItem(localThreadId, turnId, error, createdAt) {
18296
+ const itemId = `${turnId}:runtime-error`;
18297
+ const item = {
18298
+ id: itemId,
18299
+ kind: "agentMessage",
18300
+ text: error,
18301
+ status: "failed",
18302
+ createdAt,
18303
+ sequence: this.input.liveState.recordTurnItemOrder(localThreadId, turnId, itemId),
18304
+ sourceTurnId: turnId
18305
+ };
18306
+ upsertThreadHistoryItemRecord(this.input.db, {
18307
+ threadId: localThreadId,
18308
+ turnId,
18309
+ itemId,
18310
+ itemJson: JSON.stringify(item)
18311
+ });
18312
+ this.input.liveState.upsertLiveItem(localThreadId, turnId, item);
18313
+ }
18223
18314
  isAutoGeneratedTitle(title) {
18224
18315
  const normalized = title?.trim();
18225
18316
  return !normalized || normalized === "Untitled thread" || normalized === "Thread";
@@ -19703,7 +19794,7 @@ var ThreadPromptTurnCoordinator = class {
19703
19794
  collaborationMode: input.collaborationMode,
19704
19795
  sandboxMode: input.sandboxMode,
19705
19796
  performanceMode: input.performanceMode,
19706
- startNewTurn: input.collaborationMode !== (record.activeTurnCollaborationMode === "plan" ? "plan" : "default")
19797
+ startNewTurn: true
19707
19798
  })
19708
19799
  });
19709
19800
  this.callbacks.invalidateThreadDetailCache(localThreadId);
@@ -22193,7 +22284,7 @@ var ThreadService = class {
22193
22284
  ...turnId ? { turnId } : {}
22194
22285
  }),
22195
22286
  invalidateThreadDetailCache: (localThreadId) => this.invalidateThreadDetailCache(localThreadId),
22196
- shouldPreserveCompletedPendingSteer: (localThreadId, turnId) => this.shouldPreserveCompletedPendingSteer(localThreadId, turnId),
22287
+ shouldPreserveCompletedPendingSteer: (localThreadId) => this.shouldPreserveCompletedPendingSteer(localThreadId),
22197
22288
  shouldPreserveMissingPendingSteer: (localThreadId, turnId) => this.shouldPreserveMissingPendingSteer(localThreadId, turnId)
22198
22289
  });
22199
22290
  this.requestCoordinator = new ProviderRequestCoordinator({
@@ -22268,8 +22359,8 @@ var ThreadService = class {
22268
22359
  normalizeCollaborationMode,
22269
22360
  normalizeReasoningEffort: normalizeReasoningEffort2,
22270
22361
  normalizeThreadGoalStatusForThread: (goal, record) => this.goalCoordinator.normalizeThreadGoalStatusForThread(goal, record),
22271
- shouldPreservePendingSteersForCompletedTurn: (record, turnId) => this.shouldPreserveCompletedPendingSteer(record.id, turnId),
22272
- scheduleQueuedContinuationDrain: (localThreadId, turnId) => this.scheduleQueuedContinuationDrain(localThreadId, turnId),
22362
+ shouldPreservePendingSteersForCompletedTurn: (record) => this.shouldPreserveCompletedPendingSteer(record.id),
22363
+ scheduleQueuedContinuationDrain: (localThreadId) => this.scheduleQueuedContinuationDrain(localThreadId),
22273
22364
  persistLiveHistoryItem: (localThreadId, turnId, item) => this.historyPersistence.persistLiveHistoryItem(localThreadId, turnId, item),
22274
22365
  persistFinalTurnOrderingHints: (localThreadId, turnId, items) => this.historyPersistence.persistFinalTurnOrderingHints(localThreadId, turnId, items),
22275
22366
  persistRuntimeTurnItemsAsDisplayTurn: (localThreadId, runtimeTurnId, displayTurnId, items) => this.historyPersistence.persistRuntimeTurnItemsAsDisplayTurn(
@@ -22737,27 +22828,7 @@ var ThreadService = class {
22737
22828
  };
22738
22829
  const hasActiveProviderTurn = Boolean(record.providerTurnId) && (record.status === "running" || !turnConfig.supportsRunningTurnInput && !record.lastTurnCompletedAt) && record.status !== "failed" && record.status !== "interrupted";
22739
22830
  if (hasActiveProviderTurn && record.providerTurnId) {
22740
- const activeTurnCollaborationMode = normalizeCollaborationMode(
22741
- record.activeTurnCollaborationMode ?? record.collaborationMode
22742
- );
22743
- if (!turnConfig.supportsRunningTurnInput || activeTurnCollaborationMode !== turnConfig.collaborationMode) {
22744
- return this.promptTurnCoordinator.queueContinuationPromptTurn(localThreadId, {
22745
- ...connectedRecord,
22746
- providerTurnId: record.providerTurnId
22747
- }, {
22748
- prompt,
22749
- displayPrompt,
22750
- developerInstructions,
22751
- clientRequestId: input.clientRequestId ?? null,
22752
- effectiveModel: turnConfig.effectiveModel,
22753
- normalizedReasoning: turnConfig.normalizedReasoning,
22754
- collaborationMode: turnConfig.collaborationMode,
22755
- sandboxMode: turnConfig.sandboxMode,
22756
- performanceMode: turnConfig.performanceMode,
22757
- workspacePath: workspace.absPath
22758
- });
22759
- }
22760
- return this.promptTurnCoordinator.steerOrStartPromptTurn(localThreadId, {
22831
+ return this.promptTurnCoordinator.queueContinuationPromptTurn(localThreadId, {
22761
22832
  ...connectedRecord,
22762
22833
  providerTurnId: record.providerTurnId
22763
22834
  }, {
@@ -22989,6 +23060,58 @@ var ThreadService = class {
22989
23060
  );
22990
23061
  return this.getThreadDetail(localThreadId);
22991
23062
  }
23063
+ async steerPendingPrompt(localThreadId, pendingSteerId) {
23064
+ const record = this.requireThreadRecord(localThreadId);
23065
+ const pending = this.auxiliaryState.findPendingSteerRecord(
23066
+ localThreadId,
23067
+ pendingSteerId
23068
+ );
23069
+ if (!pending) {
23070
+ throw new HttpError(404, {
23071
+ code: "not_found",
23072
+ message: "Pending queued prompt was not found."
23073
+ });
23074
+ }
23075
+ if (pending.delivery !== "continuation") {
23076
+ throw new HttpError(409, {
23077
+ code: "conflict",
23078
+ message: "This prompt has already been steered."
23079
+ });
23080
+ }
23081
+ if (!record.providerTurnId || record.status !== "running") {
23082
+ throw new HttpError(409, {
23083
+ code: "conflict",
23084
+ message: "The active turn finished before this prompt could be steered."
23085
+ });
23086
+ }
23087
+ const providerSessionId = this.requireProviderSessionId(record);
23088
+ const runtime = this.runtimeForProvider(record.provider);
23089
+ if (!runtime.sendInput) {
23090
+ throw new HttpError(409, {
23091
+ code: "conflict",
23092
+ message: "This backend does not support steering an active turn."
23093
+ });
23094
+ }
23095
+ const workspace = this.requireWorkspaceForThread(record);
23096
+ await runtime.sendInput({
23097
+ providerSessionId,
23098
+ providerTurnId: record.providerTurnId,
23099
+ prompt: pending.submittedPrompt,
23100
+ workspacePath: workspace.absPath
23101
+ });
23102
+ updateThreadPendingSteerRecordDelivery(
23103
+ this.db,
23104
+ pending.id,
23105
+ "steer",
23106
+ record.providerTurnId
23107
+ );
23108
+ this.invalidateThreadDetailCache(localThreadId);
23109
+ this.emitThreadEvent("thread.updated", localThreadId, {
23110
+ reason: "pending_steer_updated",
23111
+ turnId: record.providerTurnId
23112
+ });
23113
+ return this.getThreadDetail(localThreadId);
23114
+ }
22992
23115
  async respondToRequest(localThreadId, requestId, input) {
22993
23116
  const record = this.requireThreadRecord(localThreadId);
22994
23117
  const requestResponse = this.requestCoordinator.respondToRequest(
@@ -23079,12 +23202,12 @@ var ThreadService = class {
23079
23202
  const runtime = this.runtimeForProvider(provider2);
23080
23203
  return Boolean(runtime.sendInput && runtime.capabilities.turns.steer);
23081
23204
  }
23082
- shouldPreserveCompletedPendingSteer(localThreadId, turnId) {
23205
+ shouldPreserveCompletedPendingSteer(localThreadId) {
23083
23206
  const record = getThreadRecordById(this.db, localThreadId);
23084
23207
  if (!record) {
23085
23208
  return false;
23086
23209
  }
23087
- return this.auxiliaryState.hasQueuedContinuationsForTurn(localThreadId, turnId);
23210
+ return this.auxiliaryState.hasQueuedContinuations(localThreadId);
23088
23211
  }
23089
23212
  shouldPreserveMissingPendingSteer(localThreadId, turnId) {
23090
23213
  const record = getThreadRecordById(this.db, localThreadId);
@@ -23100,14 +23223,14 @@ var ThreadService = class {
23100
23223
  );
23101
23224
  return (record.providerTurnId === turnId || activeDisplayTurnId === turnId) && this.auxiliaryState.hasQueuedContinuationsForTurn(localThreadId, turnId);
23102
23225
  }
23103
- scheduleQueuedContinuationDrain(localThreadId, turnId) {
23104
- const key = `${localThreadId}:${turnId}`;
23226
+ scheduleQueuedContinuationDrain(localThreadId) {
23227
+ const key = localThreadId;
23105
23228
  if (this.queuedContinuationDrains.has(key)) {
23106
23229
  return;
23107
23230
  }
23108
23231
  this.queuedContinuationDrains.add(key);
23109
23232
  queueMicrotask(() => {
23110
- void this.drainQueuedContinuation(localThreadId, turnId).catch((error) => {
23233
+ void this.drainQueuedContinuation(localThreadId).catch((error) => {
23111
23234
  const message = error instanceof Error ? error.message : "Failed to run queued prompt.";
23112
23235
  updateThreadRecord(this.db, localThreadId, {
23113
23236
  lastError: message
@@ -23118,11 +23241,8 @@ var ThreadService = class {
23118
23241
  });
23119
23242
  });
23120
23243
  }
23121
- async drainQueuedContinuation(localThreadId, turnId) {
23122
- const pending = this.auxiliaryState.listPendingSteerRecordsForTurn(
23123
- localThreadId,
23124
- turnId
23125
- ).find((entry) => entry.delivery === "continuation");
23244
+ async drainQueuedContinuation(localThreadId) {
23245
+ const pending = this.auxiliaryState.listQueuedContinuationRecords(localThreadId)[0];
23126
23246
  if (!pending) {
23127
23247
  return;
23128
23248
  }
@@ -23152,16 +23272,6 @@ var ThreadService = class {
23152
23272
  approvalMode: record.approvalMode ?? "yolo",
23153
23273
  promptInput: {}
23154
23274
  });
23155
- if (!queuedConfig?.startNewTurn) {
23156
- const queuedUserItemId = `queued-continuation:${pending.id}:user`;
23157
- this.historyPersistence.persistProjectedHistoryItem(localThreadId, turnId, {
23158
- id: queuedUserItemId,
23159
- kind: "userMessage",
23160
- text: pending.displayPrompt,
23161
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
23162
- sequence: this.liveState.recordTurnItemOrder(localThreadId, turnId, queuedUserItemId)
23163
- });
23164
- }
23165
23275
  await this.promptTurnCoordinator.startPromptTurn(localThreadId, {
23166
23276
  ...record,
23167
23277
  providerSessionId
@@ -23174,10 +23284,9 @@ var ThreadService = class {
23174
23284
  collaborationMode: turnConfig.collaborationMode,
23175
23285
  sandboxMode: turnConfig.sandboxMode,
23176
23286
  performanceMode: turnConfig.performanceMode,
23177
- workspacePath: workspace.absPath,
23178
- ...queuedConfig?.startNewTurn ? {} : { hidden: true, displayTurnId: turnId }
23287
+ workspacePath: workspace.absPath
23179
23288
  });
23180
- this.auxiliaryState.deletePendingSteerRecord(localThreadId, pending.id, turnId);
23289
+ this.auxiliaryState.deletePendingSteerRecord(localThreadId, pending.id, pending.turnId);
23181
23290
  }
23182
23291
  async handleProviderRuntimeRequest(request) {
23183
23292
  this.requestCoordinator.handleProviderRuntimeRequest(request);
@@ -24516,6 +24625,19 @@ async function registerThreadRoutes(app2) {
24516
24625
  params.pendingSteerId
24517
24626
  );
24518
24627
  });
24628
+ app2.post(
24629
+ "/api/threads/:id/pending-steers/:pendingSteerId/steer",
24630
+ async (request) => {
24631
+ const params = z5.object({
24632
+ id: z5.string().uuid(),
24633
+ pendingSteerId: z5.string().uuid()
24634
+ }).parse(request.params);
24635
+ return app2.services.threadService.steerPendingPrompt(
24636
+ params.id,
24637
+ params.pendingSteerId
24638
+ );
24639
+ }
24640
+ );
24519
24641
  app2.post("/api/threads/:id/interrupt", async (request) => {
24520
24642
  const params = z5.object({ id: z5.string().uuid() }).parse(request.params);
24521
24643
  const body = interruptSchema.parse(request.body ?? {});