remote-codex 0.11.36 → 0.11.37
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 +195 -18
- package/apps/supervisor-api/dist/index.js +152 -26
- package/apps/supervisor-web/dist/assets/index-P0J5du_C.css +1 -0
- package/apps/supervisor-web/dist/assets/{index-Da1X2d2c.js → index-pkf-_mec.js} +7 -7
- package/apps/supervisor-web/dist/assets/{thread-ui-C1THdVD0.js → thread-ui-xWvswa2v.js} +57 -24
- package/apps/supervisor-web/dist/index.html +3 -3
- package/package.json +1 -1
- package/packages/codex/src/runtimeAdapter.test.ts +57 -0
- package/packages/codex/src/runtimeAdapter.ts +52 -3
- package/packages/codex/src/types.ts +8 -2
- package/packages/db/src/repositories.ts +16 -0
- package/apps/supervisor-web/dist/assets/index-Cc1qhcG9.css +0 -1
package/README.md
CHANGED
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
## English
|
|
7
7
|
|
|
8
|
-
Remote Codex is a
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
|
|
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
|
+

|
|
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.
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
-
|
|
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
|
+

|
|
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.
|
|
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
|
-
|
|
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
|
|
10667
|
+
error: codexTurnErrorMessage(params.error),
|
|
10619
10668
|
willRetry: params.willRetry
|
|
10620
10669
|
};
|
|
10621
10670
|
}
|
|
@@ -18131,12 +18180,21 @@ var ThreadRuntimeEventProjector = class {
|
|
|
18131
18180
|
const rawTurnId = event.turn.providerTurnId;
|
|
18132
18181
|
const turnId = liveState.displayTurnIdForRuntimeTurn(record.id, rawTurnId) ?? rawTurnId;
|
|
18133
18182
|
const turnItems = event.turn.items;
|
|
18183
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18184
|
+
if (event.turn.status === "failed") {
|
|
18185
|
+
this.persistTurnFailureItem(
|
|
18186
|
+
record.id,
|
|
18187
|
+
turnId,
|
|
18188
|
+
event.turn.error?.message ?? "Unable to complete the turn.",
|
|
18189
|
+
completedAt
|
|
18190
|
+
);
|
|
18191
|
+
}
|
|
18134
18192
|
updateThreadRecord(db, record.id, {
|
|
18135
18193
|
providerTurnId: null,
|
|
18136
18194
|
activeTurnCollaborationMode: null,
|
|
18137
18195
|
status: event.turn.status === "failed" ? "failed" : event.turn.status === "interrupted" ? "interrupted" : "idle",
|
|
18138
18196
|
lastError: event.turn.error?.message ?? null,
|
|
18139
|
-
lastTurnCompletedAt:
|
|
18197
|
+
lastTurnCompletedAt: completedAt
|
|
18140
18198
|
});
|
|
18141
18199
|
callbacks.persistFinalTurnOrderingHints(record.id, turnId, turnItems);
|
|
18142
18200
|
callbacks.persistRuntimeTurnItemsAsDisplayTurn(record.id, rawTurnId, turnId, turnItems);
|
|
@@ -18187,12 +18245,14 @@ var ThreadRuntimeEventProjector = class {
|
|
|
18187
18245
|
return;
|
|
18188
18246
|
}
|
|
18189
18247
|
const turnId = liveState.displayTurnIdForRuntimeTurn(record.id, event.providerTurnId) ?? event.providerTurnId;
|
|
18248
|
+
const failedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
18249
|
+
this.persistTurnFailureItem(record.id, turnId, event.error, failedAt);
|
|
18190
18250
|
updateThreadRecord(db, record.id, {
|
|
18191
18251
|
providerTurnId: null,
|
|
18192
18252
|
activeTurnCollaborationMode: null,
|
|
18193
18253
|
status: "failed",
|
|
18194
18254
|
lastError: event.error,
|
|
18195
|
-
lastTurnCompletedAt:
|
|
18255
|
+
lastTurnCompletedAt: failedAt
|
|
18196
18256
|
});
|
|
18197
18257
|
liveState.setLivePlan(record.id, null);
|
|
18198
18258
|
liveState.setLiveItems(record.id, null);
|
|
@@ -18208,7 +18268,9 @@ var ThreadRuntimeEventProjector = class {
|
|
|
18208
18268
|
error: event.error,
|
|
18209
18269
|
...event.willRetry !== void 0 ? { willRetry: event.willRetry } : {}
|
|
18210
18270
|
});
|
|
18211
|
-
|
|
18271
|
+
if (event.provider !== "codex") {
|
|
18272
|
+
liveState.clearRuntimeDisplayTurnMapping(record.id, event.providerTurnId);
|
|
18273
|
+
}
|
|
18212
18274
|
return;
|
|
18213
18275
|
}
|
|
18214
18276
|
}
|
|
@@ -18220,6 +18282,25 @@ var ThreadRuntimeEventProjector = class {
|
|
|
18220
18282
|
providerSessionId
|
|
18221
18283
|
);
|
|
18222
18284
|
}
|
|
18285
|
+
persistTurnFailureItem(localThreadId, turnId, error, createdAt) {
|
|
18286
|
+
const itemId = `${turnId}:runtime-error`;
|
|
18287
|
+
const item = {
|
|
18288
|
+
id: itemId,
|
|
18289
|
+
kind: "agentMessage",
|
|
18290
|
+
text: error,
|
|
18291
|
+
status: "failed",
|
|
18292
|
+
createdAt,
|
|
18293
|
+
sequence: this.input.liveState.recordTurnItemOrder(localThreadId, turnId, itemId),
|
|
18294
|
+
sourceTurnId: turnId
|
|
18295
|
+
};
|
|
18296
|
+
upsertThreadHistoryItemRecord(this.input.db, {
|
|
18297
|
+
threadId: localThreadId,
|
|
18298
|
+
turnId,
|
|
18299
|
+
itemId,
|
|
18300
|
+
itemJson: JSON.stringify(item)
|
|
18301
|
+
});
|
|
18302
|
+
this.input.liveState.upsertLiveItem(localThreadId, turnId, item);
|
|
18303
|
+
}
|
|
18223
18304
|
isAutoGeneratedTitle(title) {
|
|
18224
18305
|
const normalized = title?.trim();
|
|
18225
18306
|
return !normalized || normalized === "Untitled thread" || normalized === "Thread";
|
|
@@ -22737,27 +22818,7 @@ var ThreadService = class {
|
|
|
22737
22818
|
};
|
|
22738
22819
|
const hasActiveProviderTurn = Boolean(record.providerTurnId) && (record.status === "running" || !turnConfig.supportsRunningTurnInput && !record.lastTurnCompletedAt) && record.status !== "failed" && record.status !== "interrupted";
|
|
22739
22820
|
if (hasActiveProviderTurn && record.providerTurnId) {
|
|
22740
|
-
|
|
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, {
|
|
22821
|
+
return this.promptTurnCoordinator.queueContinuationPromptTurn(localThreadId, {
|
|
22761
22822
|
...connectedRecord,
|
|
22762
22823
|
providerTurnId: record.providerTurnId
|
|
22763
22824
|
}, {
|
|
@@ -22989,6 +23050,58 @@ var ThreadService = class {
|
|
|
22989
23050
|
);
|
|
22990
23051
|
return this.getThreadDetail(localThreadId);
|
|
22991
23052
|
}
|
|
23053
|
+
async steerPendingPrompt(localThreadId, pendingSteerId) {
|
|
23054
|
+
const record = this.requireThreadRecord(localThreadId);
|
|
23055
|
+
const pending = this.auxiliaryState.findPendingSteerRecord(
|
|
23056
|
+
localThreadId,
|
|
23057
|
+
pendingSteerId
|
|
23058
|
+
);
|
|
23059
|
+
if (!pending) {
|
|
23060
|
+
throw new HttpError(404, {
|
|
23061
|
+
code: "not_found",
|
|
23062
|
+
message: "Pending queued prompt was not found."
|
|
23063
|
+
});
|
|
23064
|
+
}
|
|
23065
|
+
if (pending.delivery !== "continuation") {
|
|
23066
|
+
throw new HttpError(409, {
|
|
23067
|
+
code: "conflict",
|
|
23068
|
+
message: "This prompt has already been steered."
|
|
23069
|
+
});
|
|
23070
|
+
}
|
|
23071
|
+
if (!record.providerTurnId || record.status !== "running") {
|
|
23072
|
+
throw new HttpError(409, {
|
|
23073
|
+
code: "conflict",
|
|
23074
|
+
message: "The active turn finished before this prompt could be steered."
|
|
23075
|
+
});
|
|
23076
|
+
}
|
|
23077
|
+
const providerSessionId = this.requireProviderSessionId(record);
|
|
23078
|
+
const runtime = this.runtimeForProvider(record.provider);
|
|
23079
|
+
if (!runtime.sendInput) {
|
|
23080
|
+
throw new HttpError(409, {
|
|
23081
|
+
code: "conflict",
|
|
23082
|
+
message: "This backend does not support steering an active turn."
|
|
23083
|
+
});
|
|
23084
|
+
}
|
|
23085
|
+
const workspace = this.requireWorkspaceForThread(record);
|
|
23086
|
+
await runtime.sendInput({
|
|
23087
|
+
providerSessionId,
|
|
23088
|
+
providerTurnId: record.providerTurnId,
|
|
23089
|
+
prompt: pending.submittedPrompt,
|
|
23090
|
+
workspacePath: workspace.absPath
|
|
23091
|
+
});
|
|
23092
|
+
updateThreadPendingSteerRecordDelivery(
|
|
23093
|
+
this.db,
|
|
23094
|
+
pending.id,
|
|
23095
|
+
"steer",
|
|
23096
|
+
record.providerTurnId
|
|
23097
|
+
);
|
|
23098
|
+
this.invalidateThreadDetailCache(localThreadId);
|
|
23099
|
+
this.emitThreadEvent("thread.updated", localThreadId, {
|
|
23100
|
+
reason: "pending_steer_updated",
|
|
23101
|
+
turnId: record.providerTurnId
|
|
23102
|
+
});
|
|
23103
|
+
return this.getThreadDetail(localThreadId);
|
|
23104
|
+
}
|
|
22992
23105
|
async respondToRequest(localThreadId, requestId, input) {
|
|
22993
23106
|
const record = this.requireThreadRecord(localThreadId);
|
|
22994
23107
|
const requestResponse = this.requestCoordinator.respondToRequest(
|
|
@@ -24516,6 +24629,19 @@ async function registerThreadRoutes(app2) {
|
|
|
24516
24629
|
params.pendingSteerId
|
|
24517
24630
|
);
|
|
24518
24631
|
});
|
|
24632
|
+
app2.post(
|
|
24633
|
+
"/api/threads/:id/pending-steers/:pendingSteerId/steer",
|
|
24634
|
+
async (request) => {
|
|
24635
|
+
const params = z5.object({
|
|
24636
|
+
id: z5.string().uuid(),
|
|
24637
|
+
pendingSteerId: z5.string().uuid()
|
|
24638
|
+
}).parse(request.params);
|
|
24639
|
+
return app2.services.threadService.steerPendingPrompt(
|
|
24640
|
+
params.id,
|
|
24641
|
+
params.pendingSteerId
|
|
24642
|
+
);
|
|
24643
|
+
}
|
|
24644
|
+
);
|
|
24519
24645
|
app2.post("/api/threads/:id/interrupt", async (request) => {
|
|
24520
24646
|
const params = z5.object({ id: z5.string().uuid() }).parse(request.params);
|
|
24521
24647
|
const body = interruptSchema.parse(request.body ?? {});
|