farming-code 2.2.29 → 2.2.30
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 +25 -66
- package/README.zh_cn.md +15 -66
- package/backend/acp-runtime.js +1 -15
- package/backend/acp-session-state.js +78 -88
- package/backend/agent-session-history.js +45 -12
- package/backend/codex-session-history.js +1 -1
- package/backend/codex-transcript.js +0 -202
- package/backend/config-manager.js +3 -19
- package/backend/farming-app-cli.js +1 -1
- package/backend/farming-cli.js +3 -57
- package/backend/index-html.js +21 -3
- package/backend/main-agent-skills.js +1 -18
- package/backend/server.js +6 -48
- package/backend/storage-layout.js +15 -0
- package/backend/update-service.js +55 -935
- package/config/farming.deploy.env.example +1 -1
- package/config/farming.install.env.example +1 -1
- package/dist/acp/codex-acp-1.1.4.mjs +387 -7
- package/dist/assets/App-Z1N7T0eM.js +695 -0
- package/dist/assets/{ProjectFilesSection-Cf2BEcAN.js → ProjectFilesSection-K3GHuQ4U.js} +2 -2
- package/dist/assets/code-dark-kLsQLvpf.css +1 -0
- package/dist/assets/{index-CQNaZc3l.js → index-D-zetSol.js} +2 -2
- package/dist/assets/main-DUTz-Rd2.css +1 -0
- package/dist/index.html +2 -1
- package/extensions/browser/backend/browser-resource-manager.js +27 -5
- package/extensions/browser/backend/browser-router.js +8 -0
- package/extensions/browser/backend/executable-discovery.js +26 -12
- package/extensions/browser/backend/managed-chromium-installer.js +593 -0
- package/extensions/browser/frontend/types.ts +8 -1
- package/extensions/browser/frontend/useBrowserResources.ts +8 -0
- package/index.html +1 -0
- package/package.json +1 -1
- package/backend/agent-memory-report.js +0 -583
- package/backend/bundle-update-helper.js +0 -113
- package/backend/codex-rollout-follower.js +0 -326
- package/dist/assets/App-Ex2oVVXO.js +0 -693
- package/dist/assets/code-dark-BEZTQwYZ.css +0 -1
- package/dist/assets/main-DK9Sy7-c.css +0 -1
package/README.md
CHANGED
|
@@ -25,19 +25,16 @@ Farming Code runs on the same development machine as your repositories and codin
|
|
|
25
25
|
|
|
26
26
|
## Quick Start
|
|
27
27
|
|
|
28
|
-
With Node.js 22.13 LTS (22.x) or Node.js 24+ and
|
|
28
|
+
With Node.js 22.13 LTS (22.x) or Node.js 24+ and access to a supported coding
|
|
29
|
+
Agent provider:
|
|
29
30
|
|
|
30
31
|
```bash
|
|
31
32
|
npm install --global farming-code@latest && farming daemon
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
Open one of the authenticated URLs printed by the command, choose **New Agent**,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
farming url
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
For a new remote browser, use the authenticated **Network** URL printed when `farming daemon` starts. A VPN, SSH tunnel, or HTTPS reverse proxy can provide a stable reachable address, but the first visit still needs the Farming startup token.
|
|
35
|
+
Open one of the authenticated URLs printed by the command, choose **New Agent**,
|
|
36
|
+
and start a task. See [Getting started](./docs/getting-started.md) for the complete
|
|
37
|
+
first-run flow.
|
|
41
38
|
|
|
42
39
|

|
|
43
40
|
|
|
@@ -47,26 +44,23 @@ Farming Code is the default desktop and mobile interface. It groups work by proj
|
|
|
47
44
|
|
|
48
45
|
### Agents, Chat, and Terminal
|
|
49
46
|
|
|
50
|
-
Start or resume Codex, Claude Code, OpenCode, Qoder, and other detected coding
|
|
47
|
+
Start or resume Codex, Claude Code, OpenCode, Qoder, and other detected coding
|
|
48
|
+
Agents. Use structured Chat to read results and inspect the process, or Terminal
|
|
49
|
+
to work directly with the CLI.
|
|
51
50
|
|
|
52
51
|

|
|
53
52
|
|
|
54
53
|
### Files and Review
|
|
55
54
|
|
|
56
|
-
Browse, search, and lightly edit Project Files without leaving the current task.
|
|
55
|
+
Browse, search, and lightly edit Project Files without leaving the current task.
|
|
56
|
+
Inspect changes and open Review when you need evidence.
|
|
57
57
|
|
|
58
58
|
### Browser Resources
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Farming can also connect to an externally managed Chromium CDP endpoint, including one exposed by Docker; see [external CDP browser setup](./docs/products/code/external-cdp-browser.md).
|
|
63
|
-
|
|
64
|
-
This first Browser Viewer targets web pages and simple Agent interaction. It is not a replacement for the browser's full window chrome, DevTools, downloads UI, extensions, or Computer Use.
|
|
60
|
+
Farming lets people and Agents use the same project browser. See [Farming Browser](docs/products/code/browser-agent-cli.md).
|
|
65
61
|
|
|
66
62
|
## Supported Agents
|
|
67
63
|
|
|
68
|
-
Farming prepares the pinned Codex and Claude executables needed by its structured runtimes during installation or update and verifies them again before startup, while continuing to discover other installed CLIs on the host. Codex, Claude Code, OpenCode, and Qoder support both structured Chat and a native Terminal; other detected coding agents use the Terminal path.
|
|
69
|
-
|
|
70
64
|
| Agent | Structured Chat | Terminal | History / resume |
|
|
71
65
|
| --- | --- | --- | --- |
|
|
72
66
|
| Codex | Yes | Yes | Yes |
|
|
@@ -75,25 +69,15 @@ Farming prepares the pinned Codex and Claude executables needed by its structure
|
|
|
75
69
|
| Qoder | Yes | Yes | Yes |
|
|
76
70
|
| bash / zsh | — | Yes | No |
|
|
77
71
|
|
|
78
|
-
|
|
72
|
+
Provider-backed Agents still require a valid provider login. Other detected CLIs
|
|
73
|
+
must be able to run on the Farming host.
|
|
79
74
|
|
|
80
75
|
## Remote Use
|
|
81
76
|
|
|
82
|
-
Run Farming on the development machine and open its authenticated URL from
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
│ HTTP / WebSocket
|
|
87
|
-
▼
|
|
88
|
-
Development machine
|
|
89
|
-
Farming server
|
|
90
|
-
├── coding Agent processes
|
|
91
|
-
├── real terminals
|
|
92
|
-
├── repositories and project files
|
|
93
|
-
└── optional agent-browser sessions
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
The browser can disconnect and reconnect without stopping an Agent. A normal Farming server restart can also reconnect supported live terminal sessions. The desktop layout keeps projects, conversations, files, and review together; the mobile layout focuses one conversation, terminal, or file at a time.
|
|
77
|
+
Run Farming on the development machine and open its authenticated URL from a
|
|
78
|
+
desktop or phone that can reach it. Agents keep running when the browser
|
|
79
|
+
disconnects. See [Operations](./docs/operations/README.md) for remote-access and
|
|
80
|
+
security guidance.
|
|
97
81
|
|
|
98
82
|
## Farming CRT
|
|
99
83
|
|
|
@@ -109,51 +93,26 @@ Farming Net is a separate, token-protected directory for Farming deployments. It
|
|
|
109
93
|
|
|
110
94
|
## Installation And Updates
|
|
111
95
|
|
|
112
|
-
Install with the Quick Start command above.
|
|
96
|
+
Install with the Quick Start command above. npm installations can update from
|
|
97
|
+
**Settings → Updates**.
|
|
113
98
|
|
|
114
99
|

|
|
115
100
|
|
|
116
|
-
Standalone CLI and directory bundles remain available from [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases).
|
|
101
|
+
Standalone CLI and directory bundles remain available from [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases) for manual installation. The in-app updater never reads GitHub Releases; it is available only to npm installations.
|
|
117
102
|
|
|
118
103
|
## Security
|
|
119
104
|
|
|
120
105
|
Farming controls real terminals and files on the development machine. Run it on a trusted host and network. Do not expose it directly to the public internet without a VPN, SSH tunnel, HTTPS reverse proxy, or equivalent access control.
|
|
121
106
|
|
|
122
|
-
|
|
107
|
+
See [SECURITY.md](./SECURITY.md) for deployment and reporting guidance.
|
|
123
108
|
|
|
124
109
|
## Documentation
|
|
125
110
|
|
|
126
|
-
- [
|
|
127
|
-
- [
|
|
128
|
-
- [
|
|
129
|
-
- [Farming Net deployment portal](./docs/products/net/README.md)
|
|
130
|
-
- [Mobile guide](./docs/products/code/mobile-guide.md)
|
|
131
|
-
- [ACP runtime](./docs/products/code/acp-runtime.md)
|
|
132
|
-
- [Review foundation](./docs/products/code/review-foundation.md)
|
|
111
|
+
- [Documentation home](./docs/README.md)
|
|
112
|
+
- [Getting started](./docs/getting-started.md)
|
|
113
|
+
- [Operations](./docs/operations/README.md)
|
|
133
114
|
- [Release history](https://github.com/zhuwenzhuang/farming/releases)
|
|
134
|
-
- [
|
|
135
|
-
|
|
136
|
-
## Development Checks
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
npm test
|
|
140
|
-
npm run typecheck
|
|
141
|
-
npm run lint
|
|
142
|
-
FARMING_BASE_PATH=/farming npm run build
|
|
143
|
-
npm run test:e2e:playwright
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Product screenshots are generated from an anonymous demo workspace with real browser flows:
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
npm run docs:product:screenshots
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
To refresh only selected files, pass a comma-separated list:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
FARMING_SCREENSHOT_FILES=01-code-workspace.png npm run docs:product:screenshots
|
|
156
|
-
```
|
|
115
|
+
- [Contributing](./CONTRIBUTING.md)
|
|
157
116
|
|
|
158
117
|
## License
|
|
159
118
|
|
package/README.zh_cn.md
CHANGED
|
@@ -25,19 +25,14 @@ Farming Code 与代码仓库和 Coding CLI 运行在同一台开发机上。Agen
|
|
|
25
25
|
|
|
26
26
|
## 快速开始
|
|
27
27
|
|
|
28
|
-
准备好 Node.js 22.13 LTS(22.x)或 Node.js 24
|
|
28
|
+
准备好 Node.js 22.13 LTS(22.x)或 Node.js 24+,并确保至少有一个可用的
|
|
29
|
+
Coding Agent Provider:
|
|
29
30
|
|
|
30
31
|
```bash
|
|
31
32
|
npm install --global farming-code@latest && farming daemon
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
打开命令输出的任意一个带鉴权 URL,选择 **New Agent
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
farming url
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
新的远程浏览器应使用 `farming daemon` 启动时输出的带鉴权 **Network** URL。VPN、SSH Tunnel 或 HTTPS Reverse Proxy 可以提供稳定且可达的地址,但第一次访问仍需要 Farming 启动 Token。
|
|
35
|
+
打开命令输出的任意一个带鉴权 URL,选择 **New Agent**,然后启动任务。完整首次使用流程见[快速开始](./docs/getting-started.zh_cn.md)。
|
|
41
36
|
|
|
42
37
|

|
|
43
38
|
|
|
@@ -47,26 +42,20 @@ Farming Code 是默认的桌面与手机界面。它按项目组织工作,把
|
|
|
47
42
|
|
|
48
43
|
### Agent、Chat 与 Terminal
|
|
49
44
|
|
|
50
|
-
启动或恢复 Codex、Claude Code、OpenCode、Qoder 以及其他检测到的 Coding
|
|
45
|
+
启动或恢复 Codex、Claude Code、OpenCode、Qoder 以及其他检测到的 Coding Agent。使用结构化 Chat 阅读结果和检查过程,或使用 Terminal 直接操作 CLI。
|
|
51
46
|
|
|
52
47
|

|
|
53
48
|
|
|
54
49
|
### Files 与 Review
|
|
55
50
|
|
|
56
|
-
浏览、搜索并轻量编辑 Project Files
|
|
51
|
+
浏览、搜索并轻量编辑 Project Files,不需要离开当前任务。需要证据时可以检查修改并打开 Review。
|
|
57
52
|
|
|
58
53
|
### Browser Resource
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Farming 也可以连接由管理员独立管理的 Chromium CDP Endpoint,包括 Docker 中暴露的 Chromium;配置方式见[外部 CDP 浏览器指南](./docs/products/code/external-cdp-browser.zh_cn.md)。
|
|
63
|
-
|
|
64
|
-
第一版 Browser Viewer 面向网页展示与简单 Agent 操作,不替代浏览器完整窗口、DevTools、下载界面、浏览器扩展或 Computer Use。
|
|
55
|
+
Farming 让人和 Agent 使用同一个 Project Browser。详见 [Farming Browser](docs/products/code/browser-agent-cli.zh_cn.md)。
|
|
65
56
|
|
|
66
57
|
## 支持的 Agent
|
|
67
58
|
|
|
68
|
-
Farming 在安装或更新阶段准备结构化 Runtime 所需的锁定版本 Codex 与 Claude Executable,并在启动前再次校验,同时继续发现开发机上安装的其他 CLI。Codex、Claude Code、OpenCode 和 Qoder 同时支持结构化 Chat 与原生 Terminal;其他检测到的 Coding Agent 使用 Terminal 路径。
|
|
69
|
-
|
|
70
59
|
| Agent | 结构化 Chat | Terminal | History / Resume |
|
|
71
60
|
| --- | --- | --- | --- |
|
|
72
61
|
| Codex | 是 | 是 | 是 |
|
|
@@ -75,25 +64,11 @@ Farming 在安装或更新阶段准备结构化 Runtime 所需的锁定版本 Co
|
|
|
75
64
|
| Qoder | 是 | 是 | 是 |
|
|
76
65
|
| bash / zsh | — | 是 | 否 |
|
|
77
66
|
|
|
78
|
-
|
|
67
|
+
使用 Provider 的 Agent 仍需要有效登录;其他被发现的 CLI 必须能在 Farming Host 上正常运行。
|
|
79
68
|
|
|
80
69
|
## 远程使用
|
|
81
70
|
|
|
82
|
-
在开发机上运行 Farming,再从能够访问这台机器的电脑或手机打开带鉴权 URL
|
|
83
|
-
|
|
84
|
-
```text
|
|
85
|
-
电脑或手机浏览器
|
|
86
|
-
│ HTTP / WebSocket
|
|
87
|
-
▼
|
|
88
|
-
开发机
|
|
89
|
-
Farming Server
|
|
90
|
-
├── Coding Agent 进程
|
|
91
|
-
├── 真实 Terminal
|
|
92
|
-
├── 代码仓库与项目文件
|
|
93
|
-
└── 可选的系统浏览器进程
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
浏览器断开或重新连接不会停止 Agent。Farming Server 正常重启后也可以重新连接受支持的实时 Terminal Session。桌面布局把项目、对话、文件和 Review 放在一起;手机布局一次聚焦一段对话、一个 Terminal 或一个文件。
|
|
71
|
+
在开发机上运行 Farming,再从能够访问这台机器的电脑或手机打开带鉴权 URL。浏览器断开后 Agent 仍会继续运行。远程访问和安全说明见[运行与维护](./docs/operations/README.zh_cn.md)。
|
|
97
72
|
|
|
98
73
|
## Farming CRT
|
|
99
74
|
|
|
@@ -109,51 +84,25 @@ Farming Net 是独立、带 Token 鉴权的 Farming 部署目录。它提供一
|
|
|
109
84
|
|
|
110
85
|
## 安装与更新
|
|
111
86
|
|
|
112
|
-
|
|
87
|
+
按上面的快速开始命令安装即可。npm 安装可以从 **Settings → Updates** 更新。
|
|
113
88
|
|
|
114
89
|

|
|
115
90
|
|
|
116
|
-
独立 CLI 和目录 Bundle 仍可从 [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases)
|
|
91
|
+
独立 CLI 和目录 Bundle 仍可从 [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases) 手动下载安装。应用内更新不会读取 GitHub Releases,只对 npm 安装开放。
|
|
117
92
|
|
|
118
93
|
## 安全
|
|
119
94
|
|
|
120
95
|
Farming 会控制开发机上的真实终端和文件。请只运行在可信主机和可信网络中,不要在没有 VPN、SSH Tunnel、HTTPS Reverse Proxy 或等价访问控制时直接暴露到公网。
|
|
121
96
|
|
|
122
|
-
|
|
97
|
+
部署与报告说明见 [SECURITY.zh_cn.md](./SECURITY.zh_cn.md)。
|
|
123
98
|
|
|
124
99
|
## 文档
|
|
125
100
|
|
|
126
|
-
- [
|
|
127
|
-
- [
|
|
128
|
-
- [
|
|
129
|
-
- [Farming Net 部署门户](./docs/products/net/README.zh_cn.md)
|
|
130
|
-
- [移动端指南](./docs/products/code/mobile-guide.zh_cn.md)
|
|
131
|
-
- [ACP 运行时](./docs/products/code/acp-runtime.zh_cn.md)
|
|
132
|
-
- [Review 基础](./docs/products/code/review-foundation.zh_cn.md)
|
|
101
|
+
- [文档首页](./docs/README.zh_cn.md)
|
|
102
|
+
- [快速开始](./docs/getting-started.zh_cn.md)
|
|
103
|
+
- [运行与维护](./docs/operations/README.zh_cn.md)
|
|
133
104
|
- [版本历史](https://github.com/zhuwenzhuang/farming/releases)
|
|
134
|
-
- [
|
|
135
|
-
|
|
136
|
-
## 开发检查
|
|
137
|
-
|
|
138
|
-
```bash
|
|
139
|
-
npm test
|
|
140
|
-
npm run typecheck
|
|
141
|
-
npm run lint
|
|
142
|
-
FARMING_BASE_PATH=/farming npm run build
|
|
143
|
-
npm run test:e2e:playwright
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
产品截图由匿名 Demo Workspace 和真实浏览器流程生成:
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
npm run docs:product:screenshots
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
只更新指定截图时,可以传入逗号分隔的文件名:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
FARMING_SCREENSHOT_FILES=01-code-workspace.png npm run docs:product:screenshots
|
|
156
|
-
```
|
|
105
|
+
- [参与贡献](./CONTRIBUTING.zh_cn.md)
|
|
157
106
|
|
|
158
107
|
## License
|
|
159
108
|
|
package/backend/acp-runtime.js
CHANGED
|
@@ -9,7 +9,6 @@ const { promisify } = require('util');
|
|
|
9
9
|
const packageJson = require('../package.json');
|
|
10
10
|
const { AcpCheckpointStore } = require('./acp-checkpoint-store');
|
|
11
11
|
const { AcpSessionState } = require('./acp-session-state');
|
|
12
|
-
const { readCodexHistoryImageData } = require('./codex-transcript');
|
|
13
12
|
const { AcpClientFileSystem, AcpClientTerminalManager } = require('./acp/client-services');
|
|
14
13
|
const { PACKAGED_CODEX_ACP_ARG } = require('./acp/packaged-codex-acp');
|
|
15
14
|
const { PACKAGED_CLAUDE_ACP_ARG } = require('./acp/packaged-claude-acp');
|
|
@@ -36,7 +35,7 @@ const CODEX_SET_SESSION_MODEL_METHOD = 'session/set_model';
|
|
|
36
35
|
const CODEX_STEER_METHOD = '_codex/session/steer';
|
|
37
36
|
const CODEX_ACP_PACKAGE = '@agentclientprotocol/codex-acp';
|
|
38
37
|
const CODEX_ACP_VERSION = '1.1.4';
|
|
39
|
-
const CODEX_ACP_SHA256 = '
|
|
38
|
+
const CODEX_ACP_SHA256 = '75e70159692a7889d496651074cd3476687bafb9dc7d4eb3e9590509ad687611';
|
|
40
39
|
const CLAUDE_ACP_PACKAGE = '@agentclientprotocol/claude-agent-acp';
|
|
41
40
|
const CLAUDE_ACP_VERSION = '0.59.0';
|
|
42
41
|
const CLAUDE_ACP_SHA256 = 'a6aa515dd02382617bf46d9eac47b8a1022c6835bcf7a8d61e2c63939be2e49c';
|
|
@@ -857,19 +856,6 @@ class AcpRuntime extends EventEmitter {
|
|
|
857
856
|
binding.historyReplayActive = false;
|
|
858
857
|
}
|
|
859
858
|
binding.sessionState.finishHistoryReplay();
|
|
860
|
-
if (provider === 'codex' && binding.sessionState.hasCodexHistoryImageReferences()) {
|
|
861
|
-
let imageDataByPath = new Map();
|
|
862
|
-
try {
|
|
863
|
-
imageDataByPath = await readCodexHistoryImageData(requestedSessionId, {
|
|
864
|
-
codexHome: binding.env.CODEX_HOME,
|
|
865
|
-
});
|
|
866
|
-
} catch {
|
|
867
|
-
// Local history images can still be restored from their adapter paths.
|
|
868
|
-
}
|
|
869
|
-
this.requireOpenBinding(binding);
|
|
870
|
-
await binding.sessionState.hydrateCodexHistoryAttachments({ imageDataByPath });
|
|
871
|
-
this.requireOpenBinding(binding);
|
|
872
|
-
}
|
|
873
859
|
this.restorePatchDecisions(binding, restoredCheckpoint?.patchDecisions);
|
|
874
860
|
historyMode = 'load';
|
|
875
861
|
opened = true;
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
const fsp = require('fs/promises');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { fileURLToPath } = require('url');
|
|
4
|
-
|
|
5
1
|
const MAX_ACP_UPDATES = 2_000;
|
|
6
2
|
const MAX_ACP_UPDATE_LOG_VALUE_CHARS = 32 * 1024;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
'.gif': 'image/gif',
|
|
11
|
-
'.jpeg': 'image/jpeg',
|
|
12
|
-
'.jpg': 'image/jpeg',
|
|
13
|
-
'.png': 'image/png',
|
|
14
|
-
'.svg': 'image/svg+xml',
|
|
15
|
-
'.webp': 'image/webp',
|
|
16
|
-
});
|
|
3
|
+
const MAX_CODEX_SUBAGENTS = 128;
|
|
4
|
+
const MAX_CODEX_SUBAGENT_ID_CHARS = 160;
|
|
5
|
+
const MAX_CODEX_SUBAGENT_NAME_CHARS = 120;
|
|
17
6
|
const {
|
|
18
|
-
codexHistoryImageTargets,
|
|
19
7
|
visibleUserMessageText,
|
|
20
8
|
} = require('./codex-transcript');
|
|
21
9
|
const {
|
|
@@ -27,6 +15,54 @@ function clone(value) {
|
|
|
27
15
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
28
16
|
}
|
|
29
17
|
|
|
18
|
+
function boundedSubagentId(value) {
|
|
19
|
+
return String(value || '').trim().slice(0, MAX_CODEX_SUBAGENT_ID_CHARS);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeCodexSubagentStatus(value) {
|
|
23
|
+
const status = String(value || '');
|
|
24
|
+
return [
|
|
25
|
+
'pendingInit',
|
|
26
|
+
'running',
|
|
27
|
+
'completed',
|
|
28
|
+
'interrupted',
|
|
29
|
+
'errored',
|
|
30
|
+
'shutdown',
|
|
31
|
+
'notFound',
|
|
32
|
+
].includes(status) ? status : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeCodexSubagentUpdate(raw, sessionId) {
|
|
36
|
+
if (!raw || raw.version !== 1 || !Array.isArray(raw.agents)) return null;
|
|
37
|
+
const rootThreadId = boundedSubagentId(raw.rootThreadId);
|
|
38
|
+
if (!rootThreadId || rootThreadId !== sessionId) return null;
|
|
39
|
+
const agents = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (const candidate of raw.agents.slice(0, MAX_CODEX_SUBAGENTS)) {
|
|
42
|
+
const threadId = boundedSubagentId(candidate?.threadId);
|
|
43
|
+
const status = normalizeCodexSubagentStatus(candidate?.status);
|
|
44
|
+
if (!threadId || threadId === rootThreadId || !status || seen.has(threadId)) continue;
|
|
45
|
+
seen.add(threadId);
|
|
46
|
+
const parentThreadId = boundedSubagentId(candidate?.parentThreadId);
|
|
47
|
+
const name = String(candidate?.name || '').trim().slice(0, MAX_CODEX_SUBAGENT_NAME_CHARS);
|
|
48
|
+
agents.push({
|
|
49
|
+
threadId,
|
|
50
|
+
parentThreadId: parentThreadId && parentThreadId !== threadId ? parentThreadId : null,
|
|
51
|
+
...(name ? { name } : {}),
|
|
52
|
+
status,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
version: 1,
|
|
57
|
+
rootThreadId,
|
|
58
|
+
revision: Number.isFinite(Number(raw.revision))
|
|
59
|
+
? Math.max(0, Math.floor(Number(raw.revision)))
|
|
60
|
+
: 0,
|
|
61
|
+
kind: raw.kind === 'delta' ? 'delta' : 'snapshot',
|
|
62
|
+
agents,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
30
66
|
function compactUpdateForLog(update) {
|
|
31
67
|
let serialized = '';
|
|
32
68
|
try {
|
|
@@ -62,40 +98,6 @@ function contentText(content) {
|
|
|
62
98
|
.join('');
|
|
63
99
|
}
|
|
64
100
|
|
|
65
|
-
function localPathFromHistoryImageTarget(value) {
|
|
66
|
-
const target = String(value || '').trim();
|
|
67
|
-
if (!target || target.includes('\0') || /^data:/i.test(target)) return '';
|
|
68
|
-
if (/^file:\/\//i.test(target)) {
|
|
69
|
-
try {
|
|
70
|
-
return fileURLToPath(target);
|
|
71
|
-
} catch {
|
|
72
|
-
return '';
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return path.isAbsolute(target) ? target : '';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function imageBlockFromDataUrl(value) {
|
|
79
|
-
const match = String(value || '').match(/^data:(image\/(?:gif|jpe?g|png|svg\+xml|webp));base64,([a-z0-9+/=]+)$/i);
|
|
80
|
-
if (!match) return null;
|
|
81
|
-
const data = match[2];
|
|
82
|
-
if (!data || Math.ceil(data.length * 3 / 4) > MAX_CODEX_HISTORY_IMAGE_BYTES) return null;
|
|
83
|
-
return { type: 'image', mimeType: match[1].toLowerCase(), data };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function imageBlockFromLocalPath(filePath) {
|
|
87
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
88
|
-
const mimeType = CODEX_HISTORY_IMAGE_MIME_BY_EXT[ext];
|
|
89
|
-
if (!mimeType) return null;
|
|
90
|
-
try {
|
|
91
|
-
const stat = await fsp.stat(filePath);
|
|
92
|
-
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_CODEX_HISTORY_IMAGE_BYTES) return null;
|
|
93
|
-
return { type: 'image', mimeType, data: (await fsp.readFile(filePath)).toString('base64') };
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
101
|
function isContextCompactionText(content) {
|
|
100
102
|
const text = contentText(content).trim();
|
|
101
103
|
return /^\*?Context compacted(?: to fit the model's context window)?\.?\*?$/i.test(text)
|
|
@@ -157,6 +159,7 @@ class AcpSessionState {
|
|
|
157
159
|
this.configOptions = [];
|
|
158
160
|
this.title = '';
|
|
159
161
|
this.updatedAt = '';
|
|
162
|
+
this.codexSubagents = null;
|
|
160
163
|
this.truncated = false;
|
|
161
164
|
this.sequence = 0;
|
|
162
165
|
this.revision = Number.isFinite(Number(options.revisionBase))
|
|
@@ -202,6 +205,7 @@ class AcpSessionState {
|
|
|
202
205
|
state.configOptions = clone(checkpoint.configOptions || []);
|
|
203
206
|
state.title = String(checkpoint.title || '');
|
|
204
207
|
state.updatedAt = String(checkpoint.updatedAt || '');
|
|
208
|
+
state.codexSubagents = clone(checkpoint.codexSubagents ?? null);
|
|
205
209
|
state.truncated = checkpoint.truncated === true;
|
|
206
210
|
return state;
|
|
207
211
|
}
|
|
@@ -282,44 +286,6 @@ class AcpSessionState {
|
|
|
282
286
|
// History replay uses the same reducer as live updates; nothing to close.
|
|
283
287
|
}
|
|
284
288
|
|
|
285
|
-
hasCodexHistoryImageReferences() {
|
|
286
|
-
return this.provider === 'codex' && this.entries.some(entry => (
|
|
287
|
-
entry?.type === 'message'
|
|
288
|
-
&& entry.role === 'user'
|
|
289
|
-
&& codexHistoryImageTargets(contentText(entry.content)).length > 0
|
|
290
|
-
));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async hydrateCodexHistoryAttachments(options = {}) {
|
|
294
|
-
if (this.provider !== 'codex') return 0;
|
|
295
|
-
const imageDataByPath = options.imageDataByPath instanceof Map ? options.imageDataByPath : new Map();
|
|
296
|
-
let hydrated = 0;
|
|
297
|
-
for (const entry of this.entries) {
|
|
298
|
-
if (entry?.type !== 'message' || entry.role !== 'user') continue;
|
|
299
|
-
const existingImages = (entry.content || []).filter(content => content?.type === 'image');
|
|
300
|
-
const remaining = MAX_CODEX_HISTORY_IMAGES_PER_MESSAGE - existingImages.length;
|
|
301
|
-
if (remaining <= 0) continue;
|
|
302
|
-
const targets = codexHistoryImageTargets(contentText(entry.content));
|
|
303
|
-
const seenPaths = new Set();
|
|
304
|
-
const blocks = [];
|
|
305
|
-
for (const target of targets) {
|
|
306
|
-
const filePath = localPathFromHistoryImageTarget(target);
|
|
307
|
-
if (!filePath || seenPaths.has(filePath)) continue;
|
|
308
|
-
seenPaths.add(filePath);
|
|
309
|
-
const fallback = imageDataByPath.get(filePath);
|
|
310
|
-
const block = await imageBlockFromLocalPath(filePath) || imageBlockFromDataUrl(fallback);
|
|
311
|
-
if (!block) continue;
|
|
312
|
-
blocks.push(block);
|
|
313
|
-
if (blocks.length >= remaining) break;
|
|
314
|
-
}
|
|
315
|
-
if (blocks.length === 0) continue;
|
|
316
|
-
entry.content.push(...blocks);
|
|
317
|
-
this.touchEntry(entry);
|
|
318
|
-
hydrated += blocks.length;
|
|
319
|
-
}
|
|
320
|
-
return hydrated;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
289
|
apply(notification) {
|
|
324
290
|
if (!notification || notification.sessionId !== this.sessionId) return false;
|
|
325
291
|
const update = notification.update;
|
|
@@ -360,10 +326,31 @@ class AcpSessionState {
|
|
|
360
326
|
} else if (kind === 'session_info_update') {
|
|
361
327
|
if (Object.prototype.hasOwnProperty.call(update, 'title')) this.title = String(update.title || '');
|
|
362
328
|
if (Object.prototype.hasOwnProperty.call(update, 'updatedAt')) this.updatedAt = String(update.updatedAt || '');
|
|
329
|
+
this.applyCodexSubagentUpdate(update?._meta?.codex?.subagents);
|
|
363
330
|
}
|
|
364
331
|
return true;
|
|
365
332
|
}
|
|
366
333
|
|
|
334
|
+
applyCodexSubagentUpdate(raw) {
|
|
335
|
+
const update = normalizeCodexSubagentUpdate(raw, this.sessionId);
|
|
336
|
+
if (!update) return false;
|
|
337
|
+
const agentsById = update.kind === 'delta' && this.codexSubagents?.rootThreadId === update.rootThreadId
|
|
338
|
+
? new Map(this.codexSubagents.agents.map(agent => [agent.threadId, agent]))
|
|
339
|
+
: new Map();
|
|
340
|
+
for (const agent of update.agents) agentsById.set(agent.threadId, agent);
|
|
341
|
+
this.codexSubagents = {
|
|
342
|
+
version: 1,
|
|
343
|
+
rootThreadId: update.rootThreadId,
|
|
344
|
+
revision: update.revision,
|
|
345
|
+
agents: [...agentsById.values()].slice(0, MAX_CODEX_SUBAGENTS),
|
|
346
|
+
};
|
|
347
|
+
// Session metadata changes are visible transcript state even when no
|
|
348
|
+
// message/tool entry changed. Advance the reducer revision so the existing
|
|
349
|
+
// revision-driven transcript read reconciles this snapshot without polling.
|
|
350
|
+
this.revision += 1;
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
367
354
|
applyMessageChunk(update, kind) {
|
|
368
355
|
const role = kind === 'user_message_chunk' ? 'user' : 'assistant';
|
|
369
356
|
const type = kind === 'agent_thought_chunk' ? 'thought' : 'message';
|
|
@@ -523,6 +510,7 @@ class AcpSessionState {
|
|
|
523
510
|
revision: this.revision,
|
|
524
511
|
delta,
|
|
525
512
|
hasMoreBefore: startIndex > 0,
|
|
513
|
+
codexSubagents: clone(this.codexSubagents),
|
|
526
514
|
};
|
|
527
515
|
}
|
|
528
516
|
|
|
@@ -564,7 +552,7 @@ class AcpSessionState {
|
|
|
564
552
|
if (
|
|
565
553
|
entry.type === 'message'
|
|
566
554
|
&& entry.role === 'user'
|
|
567
|
-
&& (
|
|
555
|
+
&& (entry.content || []).some(content => content?.type === 'image')
|
|
568
556
|
) {
|
|
569
557
|
renderedAttachmentKinds.push('image');
|
|
570
558
|
}
|
|
@@ -608,6 +596,7 @@ class AcpSessionState {
|
|
|
608
596
|
availableCommands: clone(this.availableCommands),
|
|
609
597
|
currentModeId: this.currentModeId,
|
|
610
598
|
configOptions: clone(this.configOptions),
|
|
599
|
+
codexSubagents: clone(this.codexSubagents),
|
|
611
600
|
...clone(extra),
|
|
612
601
|
};
|
|
613
602
|
if (options.includeUpdates === true) snapshot.updates = clone(this.updates);
|
|
@@ -632,6 +621,7 @@ class AcpSessionState {
|
|
|
632
621
|
configOptions: clone(this.configOptions),
|
|
633
622
|
title: this.title,
|
|
634
623
|
updatedAt: this.updatedAt,
|
|
624
|
+
codexSubagents: clone(this.codexSubagents),
|
|
635
625
|
truncated: this.truncated,
|
|
636
626
|
};
|
|
637
627
|
}
|