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 +195 -18
- package/apps/supervisor-api/dist/index.js +180 -58
- 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
|
}
|
|
@@ -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
|
-
|
|
17852
|
-
|
|
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 ??
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
22272
|
-
scheduleQueuedContinuationDrain: (localThreadId
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
23104
|
-
const key =
|
|
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
|
|
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
|
|
23122
|
-
const pending = this.auxiliaryState.
|
|
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 ?? {});
|