@ckanner/flywheel 0.32.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/README.md +11 -2
- package/README.zh-CN.md +6 -1
- package/THIRD_PARTY_NOTICES.md +42 -0
- package/bin/runtime.js +25 -31
- package/cordis.patch.yml +5 -0
- package/dist/bridge.mjs +421 -0
- package/dist/claude-code-pro-bridge.mjs +82 -0
- package/dist/claude-code-pro-worker.mjs +5 -0
- package/dist/client.js +4078 -0
- package/dist/codex-pro-bridge.mjs +75 -0
- package/dist/codex-pro-worker.mjs +5 -0
- package/dist/console-read.mjs +51 -0
- package/dist/control.mjs +218 -0
- package/dist/index.mjs +185 -0
- package/dist/installation.mjs +56 -0
- package/dist/machine-worker.mjs +241 -0
- package/dist/policy.mjs +91 -0
- package/dist/prewarm.mjs +136 -0
- package/dist/protocol.mjs +260 -0
- package/dist/task-association.mjs +127 -0
- package/dist/worker.mjs +52 -0
- package/dist/workspaces.mjs +42 -0
- package/docs/README.md +1 -1
- package/docs/README.zh-CN.md +1 -1
- package/docs/design.md +8 -5
- package/docs/en/configuration.md +1 -1
- package/docs/en/contributing.md +5 -6
- package/docs/en/flow-console.md +1 -1
- package/docs/en/getting-started.md +13 -3
- package/docs/en/releasing.md +132 -174
- package/docs/validation/2026-11-13-release-0.32.0.md +78 -0
- package/docs/validation/2026-11-13-release-0.32.1.md +83 -0
- package/docs/validation/2026-11-14-single-package.md +60 -0
- package/docs/validation/README.md +3 -0
- package/docs/zh-CN/configuration.md +1 -1
- package/docs/zh-CN/contributing.md +4 -5
- package/docs/zh-CN/flow-console.md +1 -1
- package/docs/zh-CN/getting-started.md +10 -4
- package/docs/zh-CN/releasing.md +97 -81
- package/native/darwin-amd64/flywheel +0 -0
- package/native/darwin-arm64/flywheel +0 -0
- package/native/linux-amd64/flywheel +0 -0
- package/native/linux-arm64/flywheel +0 -0
- package/native/manifest.json +22 -0
- package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +403 -0
- package/node_modules/dsh-subagent-codex-pro/LICENSE +21 -0
- package/node_modules/dsh-subagent-codex-pro/README.md +557 -0
- package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +30 -0
- package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +355 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +65 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +144 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +98 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine.md +194 -0
- package/node_modules/dsh-subagent-codex-pro/docs/orchestration.zh.md +86 -0
- package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js +68 -0
- package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +15 -0
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/index.js +830 -0
- package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +35 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js +229 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +101 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +122 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js +167 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +141 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +128 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js +104 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +156 -0
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/run.js +842 -0
- package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/structured.js +329 -0
- package/node_modules/dsh-subagent-codex-pro/lib/structured.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js +295 -0
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts +88 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +179 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +266 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +34 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts +113 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts +3 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts +4 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts +53 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts +8 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts +2 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +70 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts +22 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +386 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts +35 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +107 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts +131 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +321 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/usage.js +356 -0
- package/node_modules/dsh-subagent-codex-pro/lib/usage.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js +986 -0
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/package.json +76 -0
- package/npm/install.js +59 -6
- package/package.json +74 -12
- package/plugins/dsh/LICENSE +21 -0
- package/plugins/dsh/PROTOCOL.md +607 -0
- package/plugins/dsh/README.en.md +4 -0
- package/plugins/dsh/README.md +342 -0
- package/plugins/dsh/README.zh-CN.md +279 -0
- package/plugins/dsh/THIRD_PARTY_NOTICES.md +42 -0
- package/plugins/dsh/vendor/README.md +17 -0
- package/plugins/dsh/vendor/codex-pro.json +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,23 @@ results are recorded separately in the [release runbook](docs/en/releasing.md)
|
|
|
5
5
|
and [validation index](docs/validation/README.md). Historical entries retain
|
|
6
6
|
their original language.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 0.33.0 — One complete npm distribution
|
|
9
|
+
|
|
10
|
+
- Consolidate the CLI, DSH host/client, workers, embedded skills and four native
|
|
11
|
+
engines into one `@ckanner/flywheel` npm tarball. Resolve and verify bundled
|
|
12
|
+
native files locally; do not download or borrow platform packages at runtime.
|
|
13
|
+
- Migrate the old desktop package through the official DSH CLI, verifying the
|
|
14
|
+
replacement before removal and retaining explicit rollback/cancellation state.
|
|
15
|
+
Keep user configuration, modified skills and task histories intact.
|
|
16
|
+
- Publish one immutable candidate through OIDC, test its registry installation,
|
|
17
|
+
then promote its single latest tag. Historical split packages remain unchanged.
|
|
18
|
+
|
|
19
|
+
## 0.32.1 — DSH compatible-series upgrades
|
|
20
|
+
|
|
21
|
+
- Admit compatible DSH 0.2-series rc and patch upgrades from 0.2.1-rc.5, while
|
|
22
|
+
keeping the 0.3 series (including prereleases) outside the compatibility boundary.
|
|
23
|
+
Replace per-version host/SDK allowlists, retain permission and cleanup checks,
|
|
24
|
+
and validate the locked 0.2.2-rc.3 host alongside the legacy host fixture.
|
|
9
25
|
|
|
10
26
|
## 0.32.0 — Session-scoped workbench and bounded runtime decisions
|
|
11
27
|
|
package/README.md
CHANGED
|
@@ -32,13 +32,22 @@ retries, budgets, workspaces, and delivery. Use it from the DeepSeek Harness
|
|
|
32
32
|
- **Makes failures visible.** Unknown execution, usage, or cleanup stays
|
|
33
33
|
unknown. The engine preserves work and stops when it cannot safely proceed.
|
|
34
34
|
|
|
35
|
+
From 0.33, `@ckanner/flywheel` is one complete package: CLI, DSH view/workers,
|
|
36
|
+
embedded skills and four platform engines. No separate platform package or first-run
|
|
37
|
+
engine download is needed. When upgrading from 0.32.x, quit DSH before running the
|
|
38
|
+
installer; it replaces the old desktop package registration and preserves your
|
|
39
|
+
configuration and histories. Reload DSH after setup. Existing old npm versions
|
|
40
|
+
remain available for historical installations.
|
|
41
|
+
|
|
35
42
|
## Install
|
|
36
43
|
|
|
37
44
|
For the desktop path, install Node.js **22.18.0 or later**, npm, Git, and a
|
|
38
45
|
supported DSH desktop with its `dsh` command available. The plugin currently
|
|
39
|
-
|
|
46
|
+
accepts the DSH **0.2 compatible series**, starting at **0.2.1-rc.5** and ending before
|
|
47
|
+
**0.3.0** (including its prereleases). Routine rc/patch upgrades within that range do not
|
|
48
|
+
require a Flywheel release. Prepare the accounts
|
|
40
49
|
and CLI tools required by the model routes you choose. DSH itself may require
|
|
41
|
-
a newer Node version; the
|
|
50
|
+
a newer Node version; the DSH 0.2.2 SDK requires Node 24.9 or later.
|
|
42
51
|
|
|
43
52
|
```sh
|
|
44
53
|
npx -y @ckanner/flywheel@latest install
|
package/README.zh-CN.md
CHANGED
|
@@ -8,6 +8,11 @@ Flywheel 是面向 coding agent 的确定性 Go 编排器。模型负责规划
|
|
|
8
8
|
|
|
9
9
|
[快速开始](docs/zh-CN/getting-started.md) · [文档](docs/README.zh-CN.md) · [参与贡献](docs/zh-CN/contributing.md) · [MIT 许可证](LICENSE)
|
|
10
10
|
|
|
11
|
+
从 0.33 起,`@ckanner/flywheel` 一个包包含 CLI、DSH 界面/worker、内嵌技能和
|
|
12
|
+
四个平台引擎,不再需要独立平台包或首次运行下载引擎。从 0.32.x 升级前先退出 DSH,
|
|
13
|
+
再运行原来的安装命令;安装器替换旧桌面包登记,保留配置和任务历史,完成后重载 DSH。
|
|
14
|
+
历史 npm 版本继续保留。
|
|
15
|
+
|
|
11
16
|
## Flywheel 能做什么
|
|
12
17
|
|
|
13
18
|
- **根据任务选择流程。** Auto 为满足条件、边界明确的任务选择 Quick,需要规划或更广泛验收时选择 Full。
|
|
@@ -19,7 +24,7 @@ Flywheel 是面向 coding agent 的确定性 Go 编排器。模型负责规划
|
|
|
19
24
|
|
|
20
25
|
## 安装
|
|
21
26
|
|
|
22
|
-
使用桌面入口前,请准备 **Node.js 22.18.0 或更高版本**、npm、Git,以及带可用 `dsh` 命令的受支持 DSH
|
|
27
|
+
使用桌面入口前,请准备 **Node.js 22.18.0 或更高版本**、npm、Git,以及带可用 `dsh` 命令的受支持 DSH 桌面。插件采用 **DSH 0.2 兼容系列**范围,从 **0.2.1-rc.5** 起,到 **0.3.0 的任何预发布版之前**;范围内常规 rc/补丁升级无需重新发布 Flywheel。还需准备所选模型路由使用的账号和 CLI 工具。DSH 自身可能要求更高版本的 Node;DSH 0.2.2 SDK 要求 Node 24.9 或更高。
|
|
23
28
|
|
|
24
29
|
```sh
|
|
25
30
|
npx -y @ckanner/flywheel@latest install
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## DeepSeek Harness UI patterns
|
|
4
|
+
|
|
5
|
+
Flywheel's plugin-owned Button, Input, and ongoing StateDot treatments are
|
|
6
|
+
adapted from DeepSeek Harness at commit
|
|
7
|
+
`b830ec950faba0a46dda564be3e2d854afed94f1`:
|
|
8
|
+
|
|
9
|
+
- `packages/client/ui-primitives/src/Button.tsx` and its stylesheet
|
|
10
|
+
- `packages/client/ui-primitives/src/Input.tsx` and its stylesheet
|
|
11
|
+
- `packages/client/ui-primitives/src/StateDot.tsx` and its stylesheet
|
|
12
|
+
|
|
13
|
+
[Upstream source](https://github.com/deepseek-ai/deepseek-harness/tree/b830ec950faba0a46dda564be3e2d854afed94f1/packages/client/ui-primitives/src)
|
|
14
|
+
|
|
15
|
+
These adapted portions retain the upstream MIT license below. Flywheel's own
|
|
16
|
+
source is also licensed under MIT; see [LICENSE](LICENSE).
|
|
17
|
+
|
|
18
|
+
MIT License
|
|
19
|
+
|
|
20
|
+
Copyright (c) 2026 DeepSeek
|
|
21
|
+
|
|
22
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
23
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
24
|
+
in the Software without restriction, including without limitation the rights
|
|
25
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
26
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
27
|
+
furnished to do so, subject to the following conditions:
|
|
28
|
+
|
|
29
|
+
The above copyright notice and this permission notice shall be included in all
|
|
30
|
+
copies or substantial portions of the Software.
|
|
31
|
+
|
|
32
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
33
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
34
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
35
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
36
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
37
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
38
|
+
SOFTWARE.
|
|
39
|
+
|
|
40
|
+
## Bundled Codex Pro public package
|
|
41
|
+
|
|
42
|
+
This distribution bundles `dsh-subagent-codex-pro@0.9.0`, Copyright (c) 2026 ckanner, under the MIT License. Its complete license is retained at `node_modules/dsh-subagent-codex-pro/LICENSE` inside the complete package. Source identity and tarball integrity are recorded in `plugins/dsh/vendor/README.md` and `plugins/dsh/vendor/codex-pro.json` in the source repository. The bundled package is the upstream public machine API implementation, including its compiled supervisor; it contains no native Codex executable or user credentials.
|
package/bin/runtime.js
CHANGED
|
@@ -8,26 +8,16 @@ const platforms = { darwin: "darwin", linux: "linux" };
|
|
|
8
8
|
const architectures = { arm64: "arm64", x64: "amd64" };
|
|
9
9
|
const forwardedSignals = ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
10
10
|
const ownPackage = require("../package.json");
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const path = require("node:path");
|
|
13
|
+
const crypto = require("node:crypto");
|
|
11
14
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} catch {
|
|
19
|
-
throw new Error(missingMessage);
|
|
20
|
-
}
|
|
21
|
-
// npm may legitimately hoist dependencies. Check the resolved package's
|
|
22
|
-
// identity, rather than assuming a particular machine's node_modules path.
|
|
23
|
-
// A partial installation must not pick up an incompatible parent package.
|
|
24
|
-
if (installed.name !== name) {
|
|
25
|
-
throw new Error(`package identity mismatch for ${name}: found ${installed.name}; reinstall @ckanner/flywheel`);
|
|
26
|
-
}
|
|
27
|
-
if (typeof expectedVersion !== "string" || installed.version !== expectedVersion) {
|
|
28
|
-
throw new Error(`version mismatch for ${name}: expected ${expectedVersion}, found ${installed.version}; reinstall @ckanner/flywheel`);
|
|
29
|
-
}
|
|
30
|
-
return entryPath;
|
|
15
|
+
function packageFile(relative) {
|
|
16
|
+
if (ownPackage.name !== '@ckanner/flywheel') throw new Error('Flywheel package identity mismatch');
|
|
17
|
+
const filename = path.resolve(__dirname, '..', relative);
|
|
18
|
+
const stat = fs.lstatSync(filename, { throwIfNoEntry: false });
|
|
19
|
+
if (!stat?.isFile() || stat.isSymbolicLink()) throw new Error(`Bundled ${relative} is missing or invalid; reinstall @ckanner/flywheel`);
|
|
20
|
+
return filename;
|
|
31
21
|
}
|
|
32
22
|
|
|
33
23
|
function loadNativePath() {
|
|
@@ -36,27 +26,31 @@ function loadNativePath() {
|
|
|
36
26
|
if (!platform || !arch) {
|
|
37
27
|
throw new Error(`unsupported platform ${process.platform}/${process.arch} (supported: darwin/linux × amd64/arm64)`);
|
|
38
28
|
}
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
const key = `${platform}-${arch}`;
|
|
30
|
+
const manifest = JSON.parse(fs.readFileSync(packageFile('native/manifest.json'), 'utf8'));
|
|
31
|
+
if (manifest.protocol !== 1 || manifest.version !== ownPackage.version) {
|
|
32
|
+
throw new Error('Bundled engine version mismatch; reinstall @ckanner/flywheel');
|
|
33
|
+
}
|
|
34
|
+
const entry = manifest.platforms?.[key];
|
|
35
|
+
const filename = packageFile(`native/${key}/flywheel`);
|
|
36
|
+
const bytes = fs.readFileSync(filename);
|
|
37
|
+
if (!entry || entry.bytes !== bytes.length || !/^[a-f0-9]{64}$/.test(entry.sha256 ?? '') ||
|
|
38
|
+
crypto.createHash('sha256').update(bytes).digest('hex') !== entry.sha256) {
|
|
39
|
+
throw new Error(`Bundled engine integrity mismatch for ${key}; reinstall @ckanner/flywheel`);
|
|
40
|
+
}
|
|
41
|
+
return filename;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function loadWorkerPath() {
|
|
45
|
-
|
|
46
|
-
return packageEntry(name, "worker", ownPackage.dependencies?.[name],
|
|
47
|
-
"the matching @ckanner/dsh-plugin-flywheel worker is missing; reinstall @ckanner/flywheel");
|
|
45
|
+
return packageFile('dist/worker.mjs');
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
function loadCodexProWorkerPath() {
|
|
51
|
-
|
|
52
|
-
return packageEntry(name, "codex-pro-worker", ownPackage.dependencies?.[name],
|
|
53
|
-
"the matching @ckanner/dsh-plugin-flywheel Codex Pro worker is missing; reinstall @ckanner/flywheel");
|
|
49
|
+
return packageFile('dist/codex-pro-worker.mjs');
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
function loadClaudeCodeProWorkerPath() {
|
|
57
|
-
|
|
58
|
-
return packageEntry(name, "claude-code-pro-worker", ownPackage.dependencies?.[name],
|
|
59
|
-
"the matching @ckanner/dsh-plugin-flywheel Claude Code Pro worker is missing; reinstall @ckanner/flywheel");
|
|
53
|
+
return packageFile('dist/claude-code-pro-worker.mjs');
|
|
60
54
|
}
|
|
61
55
|
|
|
62
56
|
function assertSupportedNode(version = process.versions.node) {
|
package/cordis.patch.yml
ADDED
package/dist/bridge.mjs
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { validatePrepare, matchWarmStart, writeWarmReceipt } from './prewarm.mjs';
|
|
8
|
+
import { BridgeError, MAX_FRAME_BYTES, normalizedError, readPolicyReceipt, record, SESSION_PROTOCOL, SessionFold, validateStart } from './protocol.mjs';
|
|
9
|
+
|
|
10
|
+
export function makeOverlay(request, policyPath = fileURLToPath(new URL('./policy.mjs', import.meta.url)), admission = {}, historyRoot) {
|
|
11
|
+
// Each patch replaces an entire config row. Pin the checked rc.5/rc.1 enforcement
|
|
12
|
+
// implementations, not the user's mutable permission dropdown defaults.
|
|
13
|
+
const disable = ['permission', 'tool-plugin-manager', 'tool-working-directory',
|
|
14
|
+
'tool-subagent', 'tool-subagent-fork', 'tool-subagent-control', 'tool-subagent-list-agents',
|
|
15
|
+
'subagent-spawn-in-process', 'subagent-fork-in-process', 'tool-workflow',
|
|
16
|
+
'tool-goal', 'goal-round-driver', 'schedule', 'tool-schedule',
|
|
17
|
+
'session-title-llm', 'tool-web', 'compaction-basic', 'command-compact', 'hmr'];
|
|
18
|
+
return [
|
|
19
|
+
...disable.map(id => ({ id, disabled: true })),
|
|
20
|
+
...(historyRoot ? [{ id: 'session-persistence-jsonl', config: { root: historyRoot } }] : []),
|
|
21
|
+
{ id: 'sandbox', name: '@deepseek-ai/dsh-sandbox-local', disabled: false, config: {} },
|
|
22
|
+
{ id: 'sandbox-policy', name: '@deepseek-ai/dsh-sandbox-policy', disabled: false,
|
|
23
|
+
config: { mode: request.permissionMode, workspaceRoot: request.cwd } },
|
|
24
|
+
{ id: 'bash-sandbox', name: '@deepseek-ai/dsh-bash-sandbox', disabled: false,
|
|
25
|
+
config: { timeoutMs: 60000, graceMs: 100 } },
|
|
26
|
+
{ id: 'fs-sandbox', name: '@deepseek-ai/dsh-fs-sandbox', disabled: false, config: {} },
|
|
27
|
+
{ id: 'approval', name: '@deepseek-ai/dsh-user-approval', disabled: false, config: { policy: 'never' } },
|
|
28
|
+
{ id: 'sdk-jsonrpc-server', config: { maxTokensAsSuccess: false } },
|
|
29
|
+
{ insert: [{ id: 'flywheel-worker-policy', name: policyPath,
|
|
30
|
+
config: { permissionMode: request.permissionMode, cwd: request.cwd,
|
|
31
|
+
...(request.workspaceCwd ? { workspaceCwd: request.workspaceCwd } : {}),
|
|
32
|
+
...(!request.sessionId && request.sessionTitle ? { sessionTitle: request.sessionTitle } : {}), ...admission } }] },
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function bounded(promise, ms) {
|
|
37
|
+
let timer;
|
|
38
|
+
try {
|
|
39
|
+
return await Promise.race([promise, new Promise(resolve => { timer = setTimeout(resolve, ms); })]);
|
|
40
|
+
} finally {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function abortable(promise, signal) {
|
|
46
|
+
signal.throwIfAborted();
|
|
47
|
+
let listener;
|
|
48
|
+
try {
|
|
49
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
50
|
+
listener = () => reject(signal.reason);
|
|
51
|
+
signal.addEventListener('abort', listener, { once: true });
|
|
52
|
+
})]);
|
|
53
|
+
} finally {
|
|
54
|
+
signal.removeEventListener('abort', listener);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A bounded JSON-RPC transport. It does not own a new process group. */
|
|
59
|
+
export class Runtime {
|
|
60
|
+
constructor(bin, args, options, notify) {
|
|
61
|
+
this.nextId = 0;
|
|
62
|
+
this.pending = new Map();
|
|
63
|
+
this.buffer = Buffer.alloc(0);
|
|
64
|
+
this.failed = null;
|
|
65
|
+
this.protocolFailure = null;
|
|
66
|
+
this.shutdownAcknowledged = false;
|
|
67
|
+
this.exit = null;
|
|
68
|
+
this.closed = false;
|
|
69
|
+
this.child = spawn(bin, args, { ...options, detached: false, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
70
|
+
this.exited = new Promise(resolve => { this.resolveExit = resolve; });
|
|
71
|
+
this.child.on('error', error => {
|
|
72
|
+
this.fail(new BridgeError('TRANSPORT', `cannot start DSH: ${error.message}`));
|
|
73
|
+
if (this.child.pid === undefined) {
|
|
74
|
+
this.exit = { code: null, signal: null };
|
|
75
|
+
this.resolveExit(this.exit);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
this.child.on('exit', (code, signal) => {
|
|
79
|
+
this.exit = { code, signal };
|
|
80
|
+
});
|
|
81
|
+
this.child.on('close', (code, signal) => {
|
|
82
|
+
this.closed = true;
|
|
83
|
+
if (this.buffer.length) {
|
|
84
|
+
this.protocolFailure ??= new BridgeError('TRANSPORT', 'DSH stdout ended in an incomplete frame');
|
|
85
|
+
this.fail(this.protocolFailure);
|
|
86
|
+
}
|
|
87
|
+
this.fail(new BridgeError('TRANSPORT', `DSH transport closed (exit=${code}, signal=${signal ?? 'none'})`));
|
|
88
|
+
this.resolveExit(this.exit ?? { code, signal });
|
|
89
|
+
});
|
|
90
|
+
this.child.stdin.on('error', error => this.fail(new BridgeError('TRANSPORT', `DSH stdin: ${error.message}`)));
|
|
91
|
+
this.child.stderr.on('data', chunk => {
|
|
92
|
+
// Logs stay separate from the engine's protocol. No prompt/credential dump.
|
|
93
|
+
options.stderr?.(chunk);
|
|
94
|
+
});
|
|
95
|
+
this.child.stdout.on('data', chunk => {
|
|
96
|
+
try {
|
|
97
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
98
|
+
let newline;
|
|
99
|
+
while ((newline = this.buffer.indexOf(10)) !== -1) {
|
|
100
|
+
if (newline > MAX_FRAME_BYTES) throw new BridgeError('TRANSPORT', 'DSH frame exceeds 8 MiB');
|
|
101
|
+
const line = this.buffer.subarray(0, newline).toString('utf8');
|
|
102
|
+
this.buffer = this.buffer.subarray(newline + 1);
|
|
103
|
+
if (line.trim() === '') continue;
|
|
104
|
+
const message = JSON.parse(line);
|
|
105
|
+
if (!record(message) || message.jsonrpc !== '2.0') {
|
|
106
|
+
throw new BridgeError('TRANSPORT', 'DSH stdout is not JSON-RPC 2.0');
|
|
107
|
+
}
|
|
108
|
+
if (Object.hasOwn(message, 'id')) {
|
|
109
|
+
if (message.id === 'flywheel-shutdown') {
|
|
110
|
+
this.shutdownAcknowledged = !message.error && Object.hasOwn(message, 'result');
|
|
111
|
+
}
|
|
112
|
+
const waiting = this.pending.get(message.id);
|
|
113
|
+
if (!waiting) continue; // Includes the best-effort shutdown acknowledgement.
|
|
114
|
+
this.pending.delete(message.id);
|
|
115
|
+
if (message.error) {
|
|
116
|
+
waiting.reject(new BridgeError('TRANSPORT', `${waiting.method}: ${message.error.message ?? 'JSON-RPC error'}`));
|
|
117
|
+
} else if (Object.hasOwn(message, 'result')) waiting.resolve(message.result);
|
|
118
|
+
else waiting.reject(new BridgeError('TRANSPORT', 'DSH response has no result or error'));
|
|
119
|
+
} else if (typeof message.method === 'string') notify(message.method, message.params);
|
|
120
|
+
else throw new BridgeError('TRANSPORT', 'malformed DSH notification');
|
|
121
|
+
}
|
|
122
|
+
if (this.buffer.length > MAX_FRAME_BYTES) throw new BridgeError('TRANSPORT', 'DSH frame exceeds 8 MiB');
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.protocolFailure = error instanceof BridgeError ? error : new BridgeError('TRANSPORT', `invalid DSH frame: ${error.message}`);
|
|
125
|
+
this.buffer = Buffer.alloc(0);
|
|
126
|
+
this.fail(this.protocolFailure);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fail(error) {
|
|
132
|
+
this.failed ??= error;
|
|
133
|
+
for (const waiter of this.pending.values()) waiter.reject(this.failed);
|
|
134
|
+
this.pending.clear();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
request(method, params) {
|
|
138
|
+
if (this.failed) return Promise.reject(this.failed);
|
|
139
|
+
if (this.exit) return Promise.reject(new BridgeError('TRANSPORT', 'DSH already exited'));
|
|
140
|
+
const id = ++this.nextId;
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
this.pending.set(id, { resolve, reject, method });
|
|
143
|
+
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async close() {
|
|
148
|
+
// The Go watchdog owns the whole inherited process group. We request the
|
|
149
|
+
// SDK's child-first drain and bound the leader wait; Go performs the final
|
|
150
|
+
// group sweep even on normal exit. A child must not detach/setsid.
|
|
151
|
+
let forced = false;
|
|
152
|
+
if (!this.exit && !this.child.stdin.destroyed) {
|
|
153
|
+
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 'flywheel-shutdown', method: 'shutdown', params: {} }) + '\n');
|
|
154
|
+
// SDK shutdown includes persistent history flush and plugin disposal.
|
|
155
|
+
// Keep this bounded below Go's default 5s cancellation grace, while
|
|
156
|
+
// allowing a normal fsync to complete under concurrent local workloads.
|
|
157
|
+
await bounded(this.exited, 2000);
|
|
158
|
+
}
|
|
159
|
+
if (!this.exit) {
|
|
160
|
+
forced = true;
|
|
161
|
+
this.child.stdin.end();
|
|
162
|
+
this.child.kill('SIGTERM');
|
|
163
|
+
await bounded(this.exited, 250);
|
|
164
|
+
}
|
|
165
|
+
if (!this.exit) {
|
|
166
|
+
this.child.kill('SIGKILL');
|
|
167
|
+
await bounded(this.exited, 350);
|
|
168
|
+
}
|
|
169
|
+
if (!this.exit) throw new BridgeError('TRANSPORT', 'DSH leader did not exit during bounded shutdown');
|
|
170
|
+
if (!this.closed && this.child.pid !== undefined) {
|
|
171
|
+
await bounded(this.exited, 350);
|
|
172
|
+
if (!this.closed) throw new BridgeError('TRANSPORT', 'DSH exited but its protocol streams did not reach quiescence');
|
|
173
|
+
}
|
|
174
|
+
return { ...this.exit, forced, graceful: this.shutdownAcknowledged, spawned: this.child.pid !== undefined };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function runWorker(input, { emit, env = process.env, signal, doctor = false, stderr, prewarm } = {}) {
|
|
179
|
+
let request;
|
|
180
|
+
let prepared;
|
|
181
|
+
let formalStarted = false;
|
|
182
|
+
let warmClosed = false;
|
|
183
|
+
let runtime;
|
|
184
|
+
let temp;
|
|
185
|
+
let sessionId;
|
|
186
|
+
let fold;
|
|
187
|
+
let outcome;
|
|
188
|
+
let failure;
|
|
189
|
+
let cancelled = false;
|
|
190
|
+
let settled = true;
|
|
191
|
+
let readySent = false;
|
|
192
|
+
let resumeAccepted = false;
|
|
193
|
+
let resumeUnconfirmed = false;
|
|
194
|
+
let dispatch = 'not-dispatched';
|
|
195
|
+
let promptId = '';
|
|
196
|
+
let waited = false;
|
|
197
|
+
const beforeReady = [];
|
|
198
|
+
let deadline;
|
|
199
|
+
let started = Date.now();
|
|
200
|
+
const controller = new AbortController();
|
|
201
|
+
const cancel = () => {
|
|
202
|
+
cancelled = true;
|
|
203
|
+
controller.abort(new BridgeError('TRANSPORT', 'worker cancelled'));
|
|
204
|
+
};
|
|
205
|
+
const abort = () => runtime?.fail(controller.signal.reason);
|
|
206
|
+
controller.signal.addEventListener('abort', abort);
|
|
207
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
208
|
+
const frame = value => emit({ protocol: request?.protocol ?? (input?.protocol === 2 ? 2 : 1), requestId: request?.requestId ?? input?.requestId ?? 'invalid-request', ...value });
|
|
209
|
+
try {
|
|
210
|
+
const budget = Number.isSafeInteger(input?.timeoutMs) && input.timeoutMs > 0 && input.timeoutMs <= 86400000
|
|
211
|
+
? input.timeoutMs : 30 * 60 * 1000;
|
|
212
|
+
deadline = setTimeout(() => controller.abort(new BridgeError('TIMEOUT', 'DSH worker lifecycle deadline exceeded')), budget);
|
|
213
|
+
if (signal?.aborted) cancel();
|
|
214
|
+
// Validate owned cleanup metadata even if an early close cancelled startup.
|
|
215
|
+
// This never creates a Session and lets EOF/close publish no-dispatch proof.
|
|
216
|
+
request = prewarm ? await validatePrepare(input) : await abortable(validateStart(input), controller.signal);
|
|
217
|
+
if (prewarm) {
|
|
218
|
+
prepared = request;
|
|
219
|
+
clearTimeout(deadline);
|
|
220
|
+
deadline = setTimeout(() => controller.abort(new BridgeError('TIMEOUT', 'SDK prewarm expired before dispatch')), request.ttlMs);
|
|
221
|
+
}
|
|
222
|
+
if (doctor && request.protocol === 2) throw new BridgeError('INVALID_REQUEST', 'doctor does not create or resume persistent histories');
|
|
223
|
+
controller.signal.throwIfAborted();
|
|
224
|
+
const bin = env.FLYWHEEL_DSH_BIN || 'dsh';
|
|
225
|
+
if (bin.includes('\0') || bin.startsWith('-')) throw new BridgeError('INVALID_REQUEST', 'invalid FLYWHEEL_DSH_BIN');
|
|
226
|
+
const profile = env.FLYWHEEL_DSH_PROFILE || 'sdk';
|
|
227
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(profile) || profile === 'desktop') {
|
|
228
|
+
throw new BridgeError('INVALID_REQUEST', 'FLYWHEEL_DSH_PROFILE must name an SDK profile, not desktop');
|
|
229
|
+
}
|
|
230
|
+
temp = await realpath(await mkdtemp(join(tmpdir(), 'flywheel-dsh-')));
|
|
231
|
+
const patch = join(temp, 'worker.patch.json');
|
|
232
|
+
sessionId = prepared ? undefined : request.sessionId ?? `flywheel-${randomUUID()}`;
|
|
233
|
+
const admission = { receiptPath: join(temp, 'policy.json'), requestId: prepared ? prepared.warmId : request.requestId,
|
|
234
|
+
...(prepared ? { deferredAdmissionPath: join(temp, 'admission.json') } : { sessionId }), nonce: randomUUID() };
|
|
235
|
+
await writeFile(patch, JSON.stringify(makeOverlay(request, undefined, admission, doctor ? join(temp, 'probe-history') : undefined)), { mode: 0o600 });
|
|
236
|
+
controller.signal.throwIfAborted();
|
|
237
|
+
if (!prepared) fold = new SessionFold(sessionId, { strict: request.protocol === 2 });
|
|
238
|
+
runtime = new Runtime(bin, ['--profile', profile, '--patch', patch], {
|
|
239
|
+
cwd: request.cwd,
|
|
240
|
+
env: { ...env, DSH_PERMISSION_MODE: request.permissionMode,
|
|
241
|
+
DSH_MAX_TOKENS_AS_SUCCESS: 'false', DSH_TELEMETRY_DISABLED: '1', FLYWHEEL_DSH_NATIVE_WORKER: '1' },
|
|
242
|
+
stderr,
|
|
243
|
+
}, (method, params) => {
|
|
244
|
+
if (prepared && !formalStarted && method === 'session.event') {
|
|
245
|
+
throw new BridgeError('TRANSPORT', 'SDK emitted a Session event before formal prewarm dispatch');
|
|
246
|
+
}
|
|
247
|
+
if (method === 'session.event' && fold.accept(params)) {
|
|
248
|
+
const eventFrame = { type: 'event', sessionId, event: params.event };
|
|
249
|
+
if (readySent) frame(eventFrame);
|
|
250
|
+
else {
|
|
251
|
+
beforeReady.push(eventFrame);
|
|
252
|
+
if (beforeReady.length > 256) throw new BridgeError('TRANSPORT', 'excessive SDK events before readiness');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (method === 'subagent.started' && params?.parentSessionId === sessionId) {
|
|
256
|
+
throw new BridgeError('INVALID_REQUEST', 'DSH child delegation is disabled: the Go engine owns workers');
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
if (controller.signal.aborted) runtime.fail(controller.signal.reason);
|
|
260
|
+
const initialized = await runtime.request('initialize', {
|
|
261
|
+
cwd: request.workspaceCwd ?? request.cwd, provider: request.provider, model: request.model,
|
|
262
|
+
...(request.reasoningEffort === undefined ? {} : { reasoningEffort: request.reasoningEffort }),
|
|
263
|
+
...(request.maxTokens === undefined ? {} : { maxTokens: request.maxTokens }),
|
|
264
|
+
});
|
|
265
|
+
if (initialized?.serverInfo?.name !== 'deepseek-harness-sdk-runtime') {
|
|
266
|
+
throw new BridgeError('TRANSPORT', 'unexpected SDK runtime identity');
|
|
267
|
+
}
|
|
268
|
+
if (prepared) {
|
|
269
|
+
prewarm.ready();
|
|
270
|
+
emit({ protocol: 1, type: 'warm-ready', warmId: prepared.warmId, binding: prepared.binding,
|
|
271
|
+
level: 'sdk-initialized', dispatched: false });
|
|
272
|
+
const next = await abortable(prewarm.dispatch, controller.signal);
|
|
273
|
+
if (next === null) { warmClosed = true; throw new BridgeError('TRANSPORT', 'unused prewarm closed'); }
|
|
274
|
+
// Once a full request arrives its lifecycle belongs to the already durable
|
|
275
|
+
// invocation, even if validation fails before any model input is sent.
|
|
276
|
+
formalStarted = true;
|
|
277
|
+
request = next;
|
|
278
|
+
// Receiving the owned start retires the prepare deadline before filesystem
|
|
279
|
+
// validation yields. The formal deadline covers that validation as well.
|
|
280
|
+
started = Date.now();
|
|
281
|
+
clearTimeout(deadline);
|
|
282
|
+
const formalBudget = Number.isSafeInteger(next?.timeoutMs) && next.timeoutMs > 0 && next.timeoutMs <= 86400000
|
|
283
|
+
? next.timeoutMs : 30 * 60 * 1000;
|
|
284
|
+
deadline = setTimeout(() => controller.abort(new BridgeError('TIMEOUT', 'DSH worker lifecycle deadline exceeded')), formalBudget);
|
|
285
|
+
request = await abortable(validateStart(next), controller.signal);
|
|
286
|
+
matchWarmStart(prepared, request);
|
|
287
|
+
sessionId = request.sessionId ?? `flywheel-${randomUUID()}`;
|
|
288
|
+
fold = new SessionFold(sessionId, { strict: request.protocol === 2 });
|
|
289
|
+
await writeFile(admission.deferredAdmissionPath, JSON.stringify({ requestId: admission.requestId,
|
|
290
|
+
nonce: admission.nonce, sessionId, ...(!request.sessionId && request.sessionTitle ? { sessionTitle: request.sessionTitle } : {}) }),
|
|
291
|
+
{ mode: 0o600, flag: 'wx' });
|
|
292
|
+
}
|
|
293
|
+
if (request.sessionId !== undefined) {
|
|
294
|
+
// Public SDK resume restores exactly this identity, without a new input
|
|
295
|
+
// and without a replacement on failure. A generic RPC error cannot tell
|
|
296
|
+
// missing history from another live owner; never silently cold-start it.
|
|
297
|
+
resumeUnconfirmed = true;
|
|
298
|
+
const restored = await runtime.request('session/resume', { sessionId });
|
|
299
|
+
if (!record(restored) || Object.keys(restored).length !== 0) {
|
|
300
|
+
throw new BridgeError('TRANSPORT', 'SDK resume did not confirm restoration');
|
|
301
|
+
}
|
|
302
|
+
resumeAccepted = true;
|
|
303
|
+
resumeUnconfirmed = false;
|
|
304
|
+
}
|
|
305
|
+
// This creates the fresh Session and its permission facts without an LLM turn.
|
|
306
|
+
let working = await runtime.request('session/working-directory/get', { sessionId });
|
|
307
|
+
if (request.workspaceCwd !== undefined && working?.cwd !== request.cwd) {
|
|
308
|
+
working = await runtime.request('session/working-directory/set', { sessionId, path: request.cwd });
|
|
309
|
+
}
|
|
310
|
+
if (working?.cwd !== request.cwd) throw new BridgeError('INVALID_REQUEST', 'DSH effective cwd differs from the requested workspace');
|
|
311
|
+
fold.policy = await abortable(readPolicyReceipt(admission.receiptPath,
|
|
312
|
+
{ requestId: admission.requestId, sessionId, nonce: admission.nonce }), controller.signal);
|
|
313
|
+
if (request.workspaceCwd !== undefined && fold.policy.workspaceCwd !== request.workspaceCwd) {
|
|
314
|
+
throw new BridgeError('INVALID_REQUEST', 'DSH session project workspace differs from the requested project');
|
|
315
|
+
}
|
|
316
|
+
fold.verifiedPolicy(request);
|
|
317
|
+
frame({ type: 'ready', sessionId, cwd: request.cwd, ...(request.workspaceCwd ? { workspaceCwd: request.workspaceCwd } : {}), provider: request.provider, model: request.model,
|
|
318
|
+
...(request.reasoningEffort === undefined ? {} : { reasoningEffort: request.reasoningEffort }),
|
|
319
|
+
permissionMode: request.permissionMode, approvalPolicy: 'never',
|
|
320
|
+
policy: { protocol: fold.policy.protocol, cwd: fold.policy.cwd, permissionMode: fold.policy.permissionMode,
|
|
321
|
+
approvalPolicy: fold.policy.approvalPolicy, confinedShell: fold.policy.confinedShell,
|
|
322
|
+
...(fold.policy.enforcement === undefined ? {} : { enforcement: fold.policy.enforcement }) },
|
|
323
|
+
...(request.protocol === 2 ? { persistent: true, resumed: resumeAccepted } : {}) });
|
|
324
|
+
readySent = true;
|
|
325
|
+
// v2 publishes only this invocation's events. Restoration/configuration
|
|
326
|
+
// records are reflected in the checked ready receipt, never as new output.
|
|
327
|
+
if (request.protocol === 1) for (const eventFrame of beforeReady) frame(eventFrame);
|
|
328
|
+
beforeReady.length = 0;
|
|
329
|
+
if (doctor) {
|
|
330
|
+
outcome = { text: 'SDK route, cwd and permission handshake verified; no model prompt sent.', usage: null };
|
|
331
|
+
} else {
|
|
332
|
+
// The next prompt is the only invocation owned by this worker. Reset any
|
|
333
|
+
// restoration facts before dispatch; historical usage must never be added.
|
|
334
|
+
fold.beginCall();
|
|
335
|
+
dispatch = 'unknown';
|
|
336
|
+
const receipt = await runtime.request('session/prompt', {
|
|
337
|
+
sessionId, contentBlocks: [{ type: 'text', text: request.prompt }],
|
|
338
|
+
});
|
|
339
|
+
if (typeof receipt?.messageId !== 'string' || receipt.messageId === '') {
|
|
340
|
+
throw new BridgeError('TRANSPORT', 'SDK prompt returned no messageId');
|
|
341
|
+
}
|
|
342
|
+
promptId = receipt.messageId;
|
|
343
|
+
dispatch = 'dispatched';
|
|
344
|
+
const completed = await runtime.request('session/wait', { sessionId });
|
|
345
|
+
if (!record(completed) || Object.keys(completed).length !== 0) {
|
|
346
|
+
throw new BridgeError('TRANSPORT', 'SDK wait did not confirm a quiescent session');
|
|
347
|
+
}
|
|
348
|
+
waited = true;
|
|
349
|
+
outcome = fold.finish(receipt.messageId);
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (!warmClosed) failure = normalizedError(error);
|
|
353
|
+
} finally {
|
|
354
|
+
if (runtime) {
|
|
355
|
+
try {
|
|
356
|
+
const exit = await runtime.close();
|
|
357
|
+
if (exit.spawned && (exit.forced || exit.code !== 0 || exit.signal || !exit.graceful)) {
|
|
358
|
+
settled = false;
|
|
359
|
+
failure ??= { code: 'TRANSPORT', message: 'DSH runtime did not prove graceful shutdown of its managed processes' };
|
|
360
|
+
}
|
|
361
|
+
if (runtime.protocolFailure) {
|
|
362
|
+
// A provider result cannot attest a damaged SDK stream. Keep its
|
|
363
|
+
// diagnosis as context, while protocol and cleanup facts stay independent.
|
|
364
|
+
const protocolFailure = normalizedError(runtime.protocolFailure);
|
|
365
|
+
failure = failure && failure.message !== protocolFailure.message
|
|
366
|
+
? { ...protocolFailure, message: `${protocolFailure.message}; preceding failure: ${failure.message}` }
|
|
367
|
+
: protocolFailure;
|
|
368
|
+
}
|
|
369
|
+
} catch (error) {
|
|
370
|
+
settled = false;
|
|
371
|
+
failure = normalizedError(error);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
clearTimeout(deadline);
|
|
375
|
+
if (controller.signal.aborted) failure ??= normalizedError(controller.signal.reason);
|
|
376
|
+
if (request && Date.now() - started > request.timeoutMs && !cancelled) {
|
|
377
|
+
failure = { code: 'TIMEOUT', message: 'DSH worker lifecycle deadline exceeded (including cleanup)' };
|
|
378
|
+
}
|
|
379
|
+
if (temp) await rm(temp, { recursive: true, force: true }).catch(error => {
|
|
380
|
+
failure ??= { code: 'TRANSPORT', message: `could not remove worker overlay: ${error.message}` };
|
|
381
|
+
});
|
|
382
|
+
signal?.removeEventListener('abort', cancel);
|
|
383
|
+
controller.signal.removeEventListener('abort', abort);
|
|
384
|
+
if (prepared && !formalStarted) {
|
|
385
|
+
const confirmed = settled && !runtime?.protocolFailure;
|
|
386
|
+
try { await writeWarmReceipt(prepared, confirmed, failure); }
|
|
387
|
+
catch (error) { settled = false; failure = normalizedError(error); }
|
|
388
|
+
emit({ protocol: 1, type: 'warm-result', warmId: prepared.warmId, binding: prepared.binding,
|
|
389
|
+
dispatched: false, cleanupConfirmed: confirmed && settled, status: warmClosed && settled && !failure ? 'closed' : cancelled ? 'cancelled' : 'error',
|
|
390
|
+
...(failure ? { error: failure } : {}) });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (prepared && !formalStarted) return { status: warmClosed && settled && !failure ? 'closed' : cancelled ? 'cancelled' : 'error', settled };
|
|
394
|
+
const result = {
|
|
395
|
+
type: 'result', status: cancelled ? 'cancelled' : failure ? 'error' : 'ok', settled,
|
|
396
|
+
...(sessionId ? { sessionId } : {}),
|
|
397
|
+
text: outcome?.text ?? fold?.text ?? '',
|
|
398
|
+
usage: outcome?.usage ?? null, costUsd: null,
|
|
399
|
+
...(failure ? { error: failure } : {}),
|
|
400
|
+
};
|
|
401
|
+
if ((formalStarted ? request.protocol : input?.protocol) === 2) {
|
|
402
|
+
// Cleanup, result certainty and output validity are independent. A complete
|
|
403
|
+
// native turn with empty/model-invalid output can still be repaired in its
|
|
404
|
+
// own context. Disconnects, damaged frames and ambiguous restore failures
|
|
405
|
+
// never become a confirmed failure merely because the process has exited.
|
|
406
|
+
const terminalKnown = waited && fold?.receipts.has(promptId) && fold?.terminal && !runtime?.protocolFailure;
|
|
407
|
+
const terminal = terminalKnown ? fold.terminal.kind === 'completed' ? 'completed'
|
|
408
|
+
: fold.terminal.kind === 'aborted' ? 'cancelled' : 'failed'
|
|
409
|
+
: dispatch === 'not-dispatched' && !resumeUnconfirmed ? 'failed' : 'unknown';
|
|
410
|
+
result.lifecycle = {
|
|
411
|
+
protocol: SESSION_PROTOCOL, requestId: request?.requestId ?? input.requestId,
|
|
412
|
+
sessionId: sessionId ?? '', dispatch, outcome: terminal,
|
|
413
|
+
cleanupConfirmed: settled,
|
|
414
|
+
resumeCandidate: Boolean(terminal === 'completed' && settled && readySent && !cancelled && !controller.signal.aborted),
|
|
415
|
+
resumeAccepted, turnId: terminalKnown ? fold.turnId : '',
|
|
416
|
+
usageStatus: result.usage === null ? 'unknown' : 'complete',
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
frame(result);
|
|
420
|
+
return result;
|
|
421
|
+
}
|