@ckanner/flywheel 0.32.1 → 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 +10 -1
- package/README.md +7 -0
- package/README.zh-CN.md +5 -0
- 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 +7 -4
- 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 +9 -2
- package/docs/en/releasing.md +132 -174
- 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 +2 -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 +7 -3
- 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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open, realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const PROTOCOL = 1;
|
|
6
|
+
export const SESSION_PROTOCOL = 'dsh-sdk/session-resume/worker-v2';
|
|
7
|
+
export const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
export const ERROR_CODES = new Set([
|
|
9
|
+
'AUTH', 'QUOTA', 'RATE_LIMIT', 'SERVER', 'TRANSPORT', 'TIMEOUT',
|
|
10
|
+
'EMPTY_RESPONSE', 'INVALID_REQUEST', 'CONTEXT_WINDOW_EXCEEDED',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export class BridgeError extends Error {
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function record(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The engine validates contract/report contents. The host validates the typed
|
|
25
|
+
// envelope before dispatch so an invalid command cannot become an unknown run.
|
|
26
|
+
export function validInvestigationCommand(value) {
|
|
27
|
+
if (!record(value) || !['start', 'run', 'status', 'report', 'cancel'].includes(value.action) ||
|
|
28
|
+
Object.keys(value).some(key => !['action', 'revision', 'contract', 'report'].includes(key))) return false;
|
|
29
|
+
if ('revision' in value && (!Number.isSafeInteger(value.revision) || value.revision < 0)) return false;
|
|
30
|
+
if (!['start', 'status'].includes(value.action) && !(value.revision > 0)) return false;
|
|
31
|
+
if ((value.action === 'start') !== ('contract' in value) ||
|
|
32
|
+
(value.action === 'report') !== ('report' in value)) return false;
|
|
33
|
+
return (!('contract' in value) || record(value.contract)) && (!('report' in value) || record(value.report));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function nonempty(value, label, max = 512) {
|
|
37
|
+
if (typeof value !== 'string' || value.length > max || value.trim() === '' || value.includes('\0')) {
|
|
38
|
+
throw new BridgeError('INVALID_REQUEST', `${label} must be a non-empty string (max ${max})`);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function directory(value, label) {
|
|
44
|
+
nonempty(value, label, 4096);
|
|
45
|
+
if (!isAbsolute(value)) throw new BridgeError('INVALID_REQUEST', `${label} must be absolute`);
|
|
46
|
+
const resolved = await realpath(value);
|
|
47
|
+
if (!(await stat(resolved)).isDirectory()) {
|
|
48
|
+
throw new BridgeError('INVALID_REQUEST', `${label} must be a directory`);
|
|
49
|
+
}
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function validateStart(value) {
|
|
54
|
+
if (!record(value) || value.type !== 'start' || ![PROTOCOL, 2].includes(value.protocol)) {
|
|
55
|
+
throw new BridgeError('INVALID_REQUEST', 'expected one protocol 1 or 2 start request');
|
|
56
|
+
}
|
|
57
|
+
const allowed = new Set(['type', 'protocol', 'requestId', 'cwd', 'provider', 'model',
|
|
58
|
+
'reasoningEffort', 'permissionMode', 'prompt', 'maxTokens', 'timeoutMs', 'workspaceCwd', 'sessionTitle']);
|
|
59
|
+
if (value.protocol === 2) {
|
|
60
|
+
allowed.add('sessionId');
|
|
61
|
+
allowed.add('persistent');
|
|
62
|
+
if (value.persistent !== true) throw new BridgeError('INVALID_REQUEST', 'protocol 2 requires explicit persistent history');
|
|
63
|
+
if (value.sessionId !== undefined) nonempty(value.sessionId, 'sessionId', 512);
|
|
64
|
+
}
|
|
65
|
+
for (const key of Object.keys(value)) {
|
|
66
|
+
if (!allowed.has(key)) throw new BridgeError('INVALID_REQUEST', `unsupported start field: ${key}`);
|
|
67
|
+
}
|
|
68
|
+
const requestId = nonempty(value.requestId, 'requestId', 128);
|
|
69
|
+
const provider = nonempty(value.provider, 'provider');
|
|
70
|
+
const model = nonempty(value.model, 'model');
|
|
71
|
+
const prompt = nonempty(value.prompt, 'prompt', MAX_FRAME_BYTES);
|
|
72
|
+
if (!['read-only', 'workspace-write', 'danger-full-access'].includes(value.permissionMode)) {
|
|
73
|
+
throw new BridgeError('INVALID_REQUEST', 'permissionMode must be read-only, workspace-write or danger-full-access');
|
|
74
|
+
}
|
|
75
|
+
if (value.reasoningEffort !== undefined) nonempty(value.reasoningEffort, 'reasoningEffort', 64);
|
|
76
|
+
if (value.maxTokens !== undefined && (!Number.isSafeInteger(value.maxTokens) || value.maxTokens < 1)) {
|
|
77
|
+
throw new BridgeError('INVALID_REQUEST', 'maxTokens must be a positive integer');
|
|
78
|
+
}
|
|
79
|
+
const timeoutMs = value.timeoutMs ?? 30 * 60 * 1000;
|
|
80
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 24 * 60 * 60 * 1000) {
|
|
81
|
+
throw new BridgeError('INVALID_REQUEST', 'timeoutMs must be between 1 and 86400000');
|
|
82
|
+
}
|
|
83
|
+
if (value.sessionTitle !== undefined) nonempty(value.sessionTitle, 'sessionTitle', 512);
|
|
84
|
+
const request = { ...value, requestId, provider, model, prompt, timeoutMs, cwd: await directory(value.cwd, 'cwd'),
|
|
85
|
+
...(value.workspaceCwd === undefined ? {} : { workspaceCwd: await directory(value.workspaceCwd, 'workspaceCwd') }) };
|
|
86
|
+
if (request.workspaceCwd && request.workspaceCwd !== request.cwd && request.permissionMode === 'workspace-write') {
|
|
87
|
+
throw new BridgeError('INVALID_REQUEST', 'DSH workspace-write binds its sandbox to the session project; separate execution roots require an explicitly compatible permission mode');
|
|
88
|
+
}
|
|
89
|
+
return request;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function readPolicyReceipt(path, identity) {
|
|
93
|
+
if (!isAbsolute(path) || await realpath(dirname(path)) !== dirname(path)) {
|
|
94
|
+
throw new BridgeError('INVALID_REQUEST', 'policy receipt directory is not physical');
|
|
95
|
+
}
|
|
96
|
+
const parent = await stat(dirname(path));
|
|
97
|
+
if (!parent.isDirectory() || (parent.mode & 0o077) !== 0) {
|
|
98
|
+
throw new BridgeError('INVALID_REQUEST', 'policy receipt directory is not private');
|
|
99
|
+
}
|
|
100
|
+
// A malicious/incorrect SDK must not stall the worker by substituting a FIFO.
|
|
101
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
102
|
+
try {
|
|
103
|
+
const info = await file.stat();
|
|
104
|
+
if (!info.isFile() || info.size > 65536 || (info.mode & 0o077) !== 0) {
|
|
105
|
+
throw new BridgeError('INVALID_REQUEST', 'policy receipt is not a bounded private regular file');
|
|
106
|
+
}
|
|
107
|
+
const buffer = Buffer.alloc(65537);
|
|
108
|
+
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
109
|
+
if (bytesRead !== info.size) throw new BridgeError('INVALID_REQUEST', 'policy receipt changed while being read');
|
|
110
|
+
const receipt = JSON.parse(buffer.subarray(0, bytesRead).toString('utf8'));
|
|
111
|
+
if (!record(receipt) || Object.entries(identity).some(([key, value]) => receipt[key] !== value)) {
|
|
112
|
+
throw new BridgeError('INVALID_REQUEST', 'policy receipt identity mismatch; prompt withheld');
|
|
113
|
+
}
|
|
114
|
+
return receipt;
|
|
115
|
+
} finally {
|
|
116
|
+
await file.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function normalizedError(error) {
|
|
121
|
+
return {
|
|
122
|
+
code: ERROR_CODES.has(error?.code) ? error.code : 'TRANSPORT',
|
|
123
|
+
message: error instanceof Error ? error.message : String(error),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function modelErrorCode(code) {
|
|
128
|
+
if (ERROR_CODES.has(code)) return code;
|
|
129
|
+
if (code === 'MISSING_CREDENTIAL' || code === 'INVALID_CREDENTIAL' ||
|
|
130
|
+
code === 'ACCOUNT_SIGN_IN_REQUIRED' || code === 'ACCOUNT_TOKEN_INVALID') return 'AUTH';
|
|
131
|
+
if (code === 'ACCOUNT_QUOTA') return 'QUOTA';
|
|
132
|
+
if (code === 'NO_ADAPTER' || code === 'UNSUPPORTED_REASONING_EFFORT') return 'INVALID_REQUEST';
|
|
133
|
+
return 'SERVER';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const ACCOUNT_RECOVERY = new Map([
|
|
137
|
+
['ACCOUNT_SIGN_IN_REQUIRED', 'Sign in to DeepSeek in DSH, then ensure the SDK runtime can access that account and the configured endpoint permits account authentication before retrying.'],
|
|
138
|
+
['ACCOUNT_TOKEN_INVALID', 'The DeepSeek account login was rejected. Sign in again in DSH and ensure the SDK runtime can access the renewed login before retrying.'],
|
|
139
|
+
['ACCOUNT_QUOTA', 'Check the DeepSeek account balance and usage; retry when account quota is available.'],
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
function modelErrorMessage(detail, fallback) {
|
|
143
|
+
const message = detail?.message ?? fallback;
|
|
144
|
+
const recovery = ACCOUNT_RECOVERY.get(detail?.code);
|
|
145
|
+
return recovery === undefined ? message : `${detail.code}: ${message}\n${recovery}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Fold committed per-call records; streamed usage snapshots are never summed. */
|
|
149
|
+
export class SessionFold {
|
|
150
|
+
constructor(sessionId, { strict = false } = {}) {
|
|
151
|
+
this.sessionId = sessionId;
|
|
152
|
+
this.strict = strict;
|
|
153
|
+
this.seen = new Set();
|
|
154
|
+
this.policy = null;
|
|
155
|
+
this.beginCall();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Resume may append model selection/policy facts, but those are not a paid
|
|
159
|
+
// invocation. Never inherit previous assistant output, terminal or usage.
|
|
160
|
+
beginCall() {
|
|
161
|
+
this.receipts = new Set();
|
|
162
|
+
this.text = '';
|
|
163
|
+
this.assistantCount = 0;
|
|
164
|
+
this.usageComplete = true;
|
|
165
|
+
this.usage = { input_tokens: 0, output_tokens: 0, reasoning_tokens: 0,
|
|
166
|
+
cache_read_tokens: 0, cache_write_tokens: 0, total_tokens: 0 };
|
|
167
|
+
this.terminal = null;
|
|
168
|
+
this.turnId = '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
accept(params) {
|
|
172
|
+
if (!record(params) || params.sessionId !== this.sessionId) return false;
|
|
173
|
+
const event = params.event;
|
|
174
|
+
if (!record(event) || typeof event.type !== 'string' ||
|
|
175
|
+
!Number.isSafeInteger(event.seq) || event.seq < 0 || !record(event.data)) {
|
|
176
|
+
throw new BridgeError('TRANSPORT', 'malformed DSH session event');
|
|
177
|
+
}
|
|
178
|
+
if (this.seen.has(event.seq)) return false;
|
|
179
|
+
this.seen.add(event.seq);
|
|
180
|
+
const data = event.data;
|
|
181
|
+
if (event.type === 'agent/inbox/spliced' && Array.isArray(data.inserted)) {
|
|
182
|
+
for (const message of data.inserted) if (typeof message?.id === 'string') this.receipts.add(message.id);
|
|
183
|
+
}
|
|
184
|
+
if (event.type === 'assistant/message') {
|
|
185
|
+
if (!Array.isArray(data.message?.content) || !data.message.content.every(block =>
|
|
186
|
+
record(block) && typeof block.type === 'string' &&
|
|
187
|
+
(block.type !== 'text' || typeof block.text === 'string'))) {
|
|
188
|
+
throw new BridgeError('TRANSPORT', 'malformed committed assistant message');
|
|
189
|
+
}
|
|
190
|
+
this.assistantCount += 1;
|
|
191
|
+
this.text = data.message.content.filter(block => block.type === 'text').map(block => block.text).join('');
|
|
192
|
+
this.addUsage(data.usage);
|
|
193
|
+
}
|
|
194
|
+
if (event.type === 'turn/end') {
|
|
195
|
+
if (!record(data.reason) || typeof data.reason.kind !== 'string' ||
|
|
196
|
+
(this.strict && (!Number.isSafeInteger(data.turn) || data.turn < 0 ||
|
|
197
|
+
!['completed', 'aborted', 'blocked', 'error', 'max-tokens'].includes(data.reason.kind)))) {
|
|
198
|
+
throw new BridgeError('TRANSPORT', 'malformed DSH turn terminal');
|
|
199
|
+
}
|
|
200
|
+
this.terminal = data.reason;
|
|
201
|
+
this.turnId = Number.isSafeInteger(data.turn) ? String(data.turn) : '';
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
addUsage(usage) {
|
|
207
|
+
const valid = value => Number.isSafeInteger(value) && value >= 0;
|
|
208
|
+
const fields = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens', 'totalTokens'];
|
|
209
|
+
if (!record(usage) || !valid(usage.inputTokens) || !valid(usage.outputTokens) ||
|
|
210
|
+
fields.some(key => usage[key] !== undefined && !valid(usage[key]))) {
|
|
211
|
+
this.usageComplete = false;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const cacheRead = usage.cacheReadTokens ?? 0;
|
|
215
|
+
const cacheWrite = usage.cacheWriteTokens ?? 0;
|
|
216
|
+
const input = usage.inputTokens + cacheRead + cacheWrite;
|
|
217
|
+
const values = {
|
|
218
|
+
input_tokens: input, output_tokens: usage.outputTokens,
|
|
219
|
+
reasoning_tokens: usage.reasoningTokens ?? 0,
|
|
220
|
+
cache_read_tokens: cacheRead, cache_write_tokens: cacheWrite,
|
|
221
|
+
total_tokens: usage.totalTokens ?? input + usage.outputTokens,
|
|
222
|
+
};
|
|
223
|
+
for (const [key, value] of Object.entries(values)) {
|
|
224
|
+
if (!Number.isSafeInteger(this.usage[key] + value)) this.usageComplete = false;
|
|
225
|
+
else this.usage[key] += value;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
verifiedPolicy(request) {
|
|
230
|
+
const policy = this.policy;
|
|
231
|
+
const fullAccess = request.permissionMode === 'danger-full-access';
|
|
232
|
+
if (!policy || policy.protocol !== PROTOCOL || policy.permissionMode !== request.permissionMode ||
|
|
233
|
+
policy.cwd !== (request.workspaceCwd ?? request.cwd) || policy.approvalPolicy !== 'never' ||
|
|
234
|
+
policy.confinedShell !== !fullAccess ||
|
|
235
|
+
(fullAccess ? policy.enforcement !== undefined : policy.enforcement !== 'full')) {
|
|
236
|
+
throw new BridgeError('INVALID_REQUEST', 'DSH did not attest the requested cwd and permission policy; prompt withheld');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
finish(messageId) {
|
|
241
|
+
if (!this.receipts.has(messageId)) throw new BridgeError('TRANSPORT', 'missing durable prompt receipt');
|
|
242
|
+
if (!this.terminal) throw new BridgeError('TRANSPORT', 'idle without committed turn/end');
|
|
243
|
+
if (this.terminal.kind !== 'completed') {
|
|
244
|
+
// DSH's account-tasks hook cancels the active account turn after sign-out,
|
|
245
|
+
// including when rejecting an expired token. Do not infer this from text
|
|
246
|
+
// or treat other cancellation hooks as authentication failures.
|
|
247
|
+
if (this.terminal.kind === 'aborted' && this.terminal.reason?.kind === 'hook' &&
|
|
248
|
+
this.terminal.reason.reason === 'deepseek-account/signed-out') {
|
|
249
|
+
throw new BridgeError('AUTH', 'DeepSeek account signed out (deepseek-account/signed-out). Sign in again in DSH and ensure the SDK runtime can access that account before retrying.');
|
|
250
|
+
}
|
|
251
|
+
const detail = this.terminal.error;
|
|
252
|
+
throw new BridgeError(modelErrorCode(detail?.code),
|
|
253
|
+
modelErrorMessage(detail, `DSH turn ended as ${this.terminal.kind}`));
|
|
254
|
+
}
|
|
255
|
+
if (!this.assistantCount || this.text.trim() === '') {
|
|
256
|
+
throw new BridgeError('EMPTY_RESPONSE', 'DSH completed without assistant text');
|
|
257
|
+
}
|
|
258
|
+
return { text: this.text, usage: this.usageComplete ? this.usage : null };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { validInvestigationCommand } from './protocol.mjs';
|
|
3
|
+
|
|
4
|
+
// This read model folds existing public command/tool events. Third-party DSH
|
|
5
|
+
// plugins must not invent Session.append event types or write engine state.
|
|
6
|
+
export const taskProjectionKey = 'flywheelTask';
|
|
7
|
+
const modes = ['auto', 'quick', 'full'];
|
|
8
|
+
const taskActions = ['investigate', 'launch', 'start', 'status', 'stop', 'step', 'session', 'deliver'];
|
|
9
|
+
export const mutatesTask = action => ['investigate', 'launch', 'start', 'stop', 'step', 'session', 'deliver'].includes(action);
|
|
10
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
const path = value => typeof value === 'string' && value.startsWith('/') && !/[\0\r\n]/.test(value);
|
|
12
|
+
const parse = value => { try { return JSON.parse(value); } catch { return null; } };
|
|
13
|
+
const sameTask = (a, b) => a.taskDir === b.taskDir && a.cwd === b.cwd;
|
|
14
|
+
const operationSchema = z.object({
|
|
15
|
+
operationId: z.string(), sequence: z.number().int().nonnegative(),
|
|
16
|
+
action: z.enum(taskActions), taskDir: z.string(), cwd: z.string(),
|
|
17
|
+
});
|
|
18
|
+
const taskViewSchema = operationSchema.extend({
|
|
19
|
+
outcome: z.enum(['pending', 'confirmed', 'unknown']), message: z.string(),
|
|
20
|
+
});
|
|
21
|
+
const viewSchema = taskViewSchema.extend({ tasks: z.array(taskViewSchema).optional() }).nullable();
|
|
22
|
+
const stateSchema = z.object({
|
|
23
|
+
cwd: z.string(), inheritedEventCount: z.number().int().nonnegative(),
|
|
24
|
+
calls: z.record(z.string(), operationSchema), view: viewSchema, tasks: z.record(z.string(), taskViewSchema),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function request(input, cwd, operationId, sequence, launchOnly = false) {
|
|
28
|
+
if (!record(input)) return null;
|
|
29
|
+
const value = launchOnly ? { ...input, action: 'launch' } : input;
|
|
30
|
+
if (!taskActions.includes(value.action) || !path(value.taskDir) || !path(value.cwd ?? cwd) ||
|
|
31
|
+
Object.keys(input).some(key => !(launchOnly ? ['taskDir', 'cwd', 'mode'] : ['action', 'taskDir', 'cwd', 'mode', 'only', 'packet', 'revision', 'retryReason', 'session', 'delivery', 'investigation']).includes(key)) ||
|
|
32
|
+
('mode' in value && (!['start', 'launch'].includes(value.action) || !modes.includes(value.mode)))) return null;
|
|
33
|
+
if (value.action === 'step' && (!['plan', 'execute', 'review', 'verify', 'integrate', 'final'].includes(value.only) || !Number.isSafeInteger(value.revision) || value.revision < 1)) return null;
|
|
34
|
+
if (value.action === 'investigate' ? !validInvestigationCommand(value.investigation) : 'investigation' in value) return null;
|
|
35
|
+
if (value.action === 'session' && (!record(value.session) || !['claim','send','return','reconcile','release'].includes(value.session.action) || !Number.isSafeInteger(value.session.revision) || value.session.revision < 1 || typeof value.session.key !== 'string')) return null;
|
|
36
|
+
if (value.action === 'deliver' && (!record(value.delivery) || value.delivery.confirmed !== true || !['create','merge'].includes(value.delivery.action))) return null;
|
|
37
|
+
return { operationId, sequence, action: value.action, taskDir: value.taskDir, cwd: value.cwd ?? cwd };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function begin(state, input, id, event, launchOnly = false) {
|
|
41
|
+
const operation = request(input, state.cwd, id, event.seq, launchOnly);
|
|
42
|
+
if (!operation) return state;
|
|
43
|
+
const unresolved = state.view && state.view.outcome !== 'confirmed';
|
|
44
|
+
// A second attempted mutation cannot erase the first uncertain target, even
|
|
45
|
+
// when a model calls a new tool before the earlier response reaches its view.
|
|
46
|
+
const binds = mutatesTask(operation.action) && !unresolved;
|
|
47
|
+
return { ...state, calls: { ...state.calls, [id]: operation },
|
|
48
|
+
view: binds ? { ...operation, outcome: 'pending', message: '', ...(Object.keys(state.tasks).length ? { tasks: Object.values(state.tasks) } : {}) } : state.view };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function settle(state, id, value, failed, error) {
|
|
52
|
+
const operation = state.calls[id];
|
|
53
|
+
if (!operation) return state;
|
|
54
|
+
const calls = { ...state.calls };
|
|
55
|
+
delete calls[id];
|
|
56
|
+
let view = state.view;
|
|
57
|
+
let tasks = state.tasks;
|
|
58
|
+
const owns = view?.operationId === id;
|
|
59
|
+
const successful = !failed && record(value) && value.protocol === 1 && value.ok === true &&
|
|
60
|
+
path(value.taskDir) && (value.cwd === undefined || path(value.cwd)) && typeof value.running === 'boolean';
|
|
61
|
+
if (successful && !(operation.action === 'status' && id.startsWith('command:') && (!view || !sameTask(operation, view)))) {
|
|
62
|
+
const task = { ...operation, taskDir: value.taskDir, cwd: value.cwd ?? operation.cwd, outcome: 'confirmed', message: '' };
|
|
63
|
+
const key = JSON.stringify([task.taskDir, task.cwd]);
|
|
64
|
+
if (!tasks[key] || tasks[key].sequence < operation.sequence) {
|
|
65
|
+
tasks = Object.fromEntries(Object.entries({ ...tasks, [key]: task }).sort((a,b) => b[1].sequence - a[1].sequence).slice(0,100));
|
|
66
|
+
}
|
|
67
|
+
// Compare the logged request, not lexical input against a realpath result.
|
|
68
|
+
// A later successful status explicitly reconciles the original target.
|
|
69
|
+
const reconciles = operation.action === 'status' && view && sameTask(operation, view) && operation.sequence > view.sequence;
|
|
70
|
+
if (owns || reconciles || (!view && operation.action === 'status') ||
|
|
71
|
+
(view?.outcome === 'confirmed' && operation.sequence > view.sequence && (['launch', 'start', 'investigate'].includes(operation.action) || operation.action === 'status' && id.startsWith('tool:')))) {
|
|
72
|
+
view = { ...operation, taskDir: value.taskDir, cwd: value.cwd ?? operation.cwd, outcome: 'confirmed', message: '' };
|
|
73
|
+
}
|
|
74
|
+
} else if (owns) {
|
|
75
|
+
const typed = record(value) && value.protocol === 1 && value.type === 'flywheel-control-error' &&
|
|
76
|
+
value.action === operation.action && ['not-dispatched', 'unknown'].includes(value.dispatch);
|
|
77
|
+
// Only host-produced structured proof can release an ambiguous mutation.
|
|
78
|
+
const notDispatched = typed && value.dispatch === 'not-dispatched' || error?.code === 'ABORTED_BEFORE_DISPATCH';
|
|
79
|
+
view = { ...view, outcome: notDispatched ? 'confirmed' : 'unknown',
|
|
80
|
+
message: typed && typeof value.message === 'string' ? value.message : '' };
|
|
81
|
+
}
|
|
82
|
+
if (view && tasks !== state.tasks) view = { ...view, tasks: Object.values(tasks) };
|
|
83
|
+
return { ...state, calls, view, tasks };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function contentValue(content) {
|
|
87
|
+
if (!Array.isArray(content)) return null;
|
|
88
|
+
const text = content.filter(block => block?.type === 'text').map(block => block.text).join('\n');
|
|
89
|
+
return parse(text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const taskAssociationProjection = {
|
|
93
|
+
key: taskProjectionKey,
|
|
94
|
+
stateVersion: 3,
|
|
95
|
+
stateSchema,
|
|
96
|
+
init: (header, inheritedEventCount) => ({
|
|
97
|
+
cwd: typeof header.cwd === 'string' ? header.cwd : '', inheritedEventCount,
|
|
98
|
+
calls: {}, view: null, tasks: {},
|
|
99
|
+
}),
|
|
100
|
+
apply(state, event) {
|
|
101
|
+
if (event.type === 'working-directory/change') return { ...state, cwd: event.data.cwd };
|
|
102
|
+
// A fork is a new conversation: inherited Flywheel tools are context, not
|
|
103
|
+
// permission to control the parent conversation's background task.
|
|
104
|
+
if (event.seq < state.inheritedEventCount) return state;
|
|
105
|
+
const data = event.data;
|
|
106
|
+
if (event.type === 'command/run' && data.name === 'flywheel') {
|
|
107
|
+
return begin(state, parse(data.args), 'command:' + data.commandId, event);
|
|
108
|
+
}
|
|
109
|
+
if (event.type === 'command/done') {
|
|
110
|
+
return settle(state, 'command:' + data.commandId, parse(data.text), data.kind !== 'success');
|
|
111
|
+
}
|
|
112
|
+
if (event.type === 'tool/call' && ['flywheel_launch', 'flywheel_control'].includes(data.name)) {
|
|
113
|
+
return begin(state, parse(data.arguments), 'tool:' + data.callId, event, data.name === 'flywheel_launch');
|
|
114
|
+
}
|
|
115
|
+
if (event.type === 'tool/result') {
|
|
116
|
+
return settle(state, 'tool:' + data.message.toolCallId, contentValue(data.message.content), data.message.isError, data.error);
|
|
117
|
+
}
|
|
118
|
+
if (event.type === 'tool/ptc-dispatch-start' && ['flywheel_launch', 'flywheel_control'].includes(data.name)) {
|
|
119
|
+
return begin(state, data.arguments, 'tool:' + data.subCallId, event, data.name === 'flywheel_launch');
|
|
120
|
+
}
|
|
121
|
+
if (event.type === 'tool/ptc-dispatch') {
|
|
122
|
+
return settle(state, 'tool:' + data.subCallId, contentValue(data.content), data.isError, data.error);
|
|
123
|
+
}
|
|
124
|
+
return state;
|
|
125
|
+
},
|
|
126
|
+
wire: { viewSchema, view: state => state.view },
|
|
127
|
+
};
|
package/dist/worker.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runWorker } from './bridge.mjs';
|
|
3
|
+
import { prewarmInput } from './prewarm.mjs';
|
|
4
|
+
import { BridgeError, MAX_FRAME_BYTES, normalizedError } from './protocol.mjs';
|
|
5
|
+
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
process.once('SIGTERM', () => controller.abort());
|
|
8
|
+
process.once('SIGINT', () => controller.abort());
|
|
9
|
+
process.once('SIGHUP', () => controller.abort());
|
|
10
|
+
process.stdout.on('error', () => controller.abort());
|
|
11
|
+
// The engine writes exactly one request then closes stdin. EOF after admission
|
|
12
|
+
// is normal and must not cancel an in-flight model turn.
|
|
13
|
+
async function readInput() {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
const admission = setTimeout(() => process.stdin.destroy(new BridgeError('TIMEOUT', 'no complete worker request within 10 seconds')), 10000);
|
|
17
|
+
try {
|
|
18
|
+
for await (const chunk of process.stdin) {
|
|
19
|
+
bytes += chunk.length;
|
|
20
|
+
if (bytes > MAX_FRAME_BYTES) throw new BridgeError('INVALID_REQUEST', 'worker request exceeds 8 MiB');
|
|
21
|
+
chunks.push(chunk);
|
|
22
|
+
}
|
|
23
|
+
const lines = Buffer.concat(chunks).toString('utf8').trim().split('\n');
|
|
24
|
+
if (lines.length !== 1 || !lines[0]) throw new BridgeError('INVALID_REQUEST', 'expected exactly one JSON line then EOF');
|
|
25
|
+
try { return JSON.parse(lines[0]); } catch {
|
|
26
|
+
throw new BridgeError('INVALID_REQUEST', 'worker request is not valid JSON');
|
|
27
|
+
}
|
|
28
|
+
} finally {
|
|
29
|
+
clearTimeout(admission);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const emit = value => process.stdout.write(JSON.stringify(value) + '\n');
|
|
34
|
+
let warmInput;
|
|
35
|
+
try {
|
|
36
|
+
if (process.argv.slice(2).some(arg => !['--doctor', '--prewarm'].includes(arg)) || process.argv.length > 3) {
|
|
37
|
+
throw new BridgeError('INVALID_REQUEST', 'worker accepts only optional --doctor or --prewarm');
|
|
38
|
+
}
|
|
39
|
+
warmInput = process.argv.includes('--prewarm') ? prewarmInput(controller) : undefined;
|
|
40
|
+
const request = warmInput ? await warmInput.admission : await readInput();
|
|
41
|
+
const result = await runWorker(request, {
|
|
42
|
+
emit, signal: controller.signal, doctor: process.argv.includes('--doctor'),
|
|
43
|
+
stderr: chunk => process.stderr.write(chunk), prewarm: warmInput,
|
|
44
|
+
});
|
|
45
|
+
process.exitCode = !result || ['ok', 'closed'].includes(result.status) ? 0 : result.status === 'cancelled' ? 143 : 1;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
emit({ protocol: 1, requestId: 'invalid-request', type: 'result', status: 'error', settled: true,
|
|
48
|
+
text: '', usage: null, costUsd: null, error: normalizedError(error) });
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
} finally {
|
|
51
|
+
warmInput?.stop();
|
|
52
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Only the desktop Host owns Workspace mutations. SDK workers must never open
|
|
2
|
+
// a second writer on the desktop's workspace storage.
|
|
3
|
+
const workerId = /^flywheel-[a-f0-9-]{36}$/;
|
|
4
|
+
|
|
5
|
+
export async function reconcileWorkspaces(ctx, response) {
|
|
6
|
+
const registry = ctx.get?.('workspaceRegistry');
|
|
7
|
+
const persistence = ctx.get?.('sessionPersistence');
|
|
8
|
+
if (!registry || !persistence) return;
|
|
9
|
+
const project = response?.state?.project?.['workspace-cwd'];
|
|
10
|
+
if (project) await registry.create(project);
|
|
11
|
+
const groups = new Map(registry.list().map(workspace => [workspace.path, workspace]));
|
|
12
|
+
if (!groups.size) return;
|
|
13
|
+
// Public metadata listing, never a scan of project directories or session
|
|
14
|
+
// event bodies. attachSession independently validates the persisted header.
|
|
15
|
+
for (const snapshot of await persistence.list()) {
|
|
16
|
+
const header = snapshot.header;
|
|
17
|
+
if (!workerId.test(header.id)) continue;
|
|
18
|
+
const workspace = groups.get(header.cwd);
|
|
19
|
+
if (workspace && !workspace.sessionIds.includes(header.id)) {
|
|
20
|
+
await workspace.attachSession(header.id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function watchWorkspaces(ctx) {
|
|
26
|
+
// SDK profile/older compositions may omit Workspace entirely. Keep the
|
|
27
|
+
// controller available, and mount reconciliation when the public services exist.
|
|
28
|
+
ctx.inject?.(['workspaceRegistry', 'sessionPersistence'], (scope) => {
|
|
29
|
+
let busy = false;
|
|
30
|
+
const refresh = async () => {
|
|
31
|
+
if (busy) return;
|
|
32
|
+
busy = true;
|
|
33
|
+
try { await reconcileWorkspaces(scope); }
|
|
34
|
+
catch (error) { scope.logger.warn('Flywheel 工作区归属未同步:%s', error.message); }
|
|
35
|
+
finally { busy = false; }
|
|
36
|
+
};
|
|
37
|
+
const timer = setInterval(refresh, 30000);
|
|
38
|
+
timer.unref?.();
|
|
39
|
+
scope.on('dispose', () => clearInterval(timer));
|
|
40
|
+
return refresh();
|
|
41
|
+
});
|
|
42
|
+
}
|
package/docs/README.md
CHANGED
|
@@ -31,7 +31,7 @@ execution separately.
|
|
|
31
31
|
| --- | --- | --- |
|
|
32
32
|
| Development setup, quality gates, and pull requests | [Contributing](en/contributing.md) | [贡献指南](zh-CN/contributing.md) |
|
|
33
33
|
| Documentation ownership, bilingual updates, and review checklist | [Maintenance rules](en/contributing.md#keep-documentation-organized) | [文档维护规范](zh-CN/contributing.md#保持文档结构清晰) |
|
|
34
|
-
| Build, verify, publish, and recover a
|
|
34
|
+
| Build, verify, publish, and recover a complete single-package release | [Release runbook](en/releasing.md) | [发行手册](zh-CN/releasing.md) |
|
|
35
35
|
| DSH package, host compatibility, and worker setup | [Plugin README](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/README.md) | [插件说明](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/README.zh-CN.md) |
|
|
36
36
|
|
|
37
37
|
The [architecture specification](design.md) is the maintained design contract
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
| --- | --- | --- |
|
|
28
28
|
| 开发环境、质量门禁和 PR 流程 | [贡献指南](zh-CN/contributing.md) | [Contributing](en/contributing.md) |
|
|
29
29
|
| 文档归属、双语更新和审阅清单 | [文档维护规范](zh-CN/contributing.md#保持文档结构清晰) | [Maintenance rules](en/contributing.md#keep-documentation-organized) |
|
|
30
|
-
|
|
|
30
|
+
| 单包构建、验证、发布和恢复 | [发行手册](zh-CN/releasing.md) | [Release runbook](en/releasing.md) |
|
|
31
31
|
| DSH 包、宿主兼容和 worker 接入 | [插件说明](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/README.zh-CN.md) | [Plugin README](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/README.md) |
|
|
32
32
|
|
|
33
33
|
[架构规范](design.md)是当前维护的设计契约,以中文编写。[Worker 协议](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/PROTOCOL.md)、[schema 与模板](https://github.com/ckanner/flywheel/tree/main/templates)及[仓库约定](https://github.com/ckanner/flywheel/blob/main/AGENTS.md)定义实现接口;版本变化见 [CHANGELOG](../CHANGELOG.md)。
|
package/docs/design.md
CHANGED
|
@@ -628,6 +628,7 @@ Go 的 `make quality-gate` 执行 vet、gofmt 和 **逐函数 100% statement cov
|
|
|
628
628
|
| P5 就绪队列与一次性 DSH 预热 | [实施计划](plans/2026-11-13-p5-ready-queue-prewarm.md) | [工程验证与收益边界](validation/2026-11-13-p5-ready-queue-prewarm.md) |
|
|
629
629
|
| 历史影子评估(已退役) | [原 JEV 方案](plans/2026-11-10-jev-shadow.md) | [历史验收](validation/2026-11-10-jev-shadow.md) |
|
|
630
630
|
| 0.30.2 发行与安装 | [发行流程](zh-CN/releasing.md) | [发行记录](validation/2026-11-11-release-0.30.2.md) |
|
|
631
|
+
| 0.33.0 单包分发 | [发行流程](zh-CN/releasing.md) | [工程验证](validation/2026-11-14-single-package.md) |
|
|
631
632
|
| 0.31.0 发行与安装 | [发行流程](zh-CN/releasing.md) | [发行记录](validation/2026-11-11-release-0.31.0.md) |
|
|
632
633
|
|
|
633
634
|
提案保留当时的背景和分期,验收记录保留实际结果;现行行为以本文、schema 和当前使用指南为准。新增实现应修改对应章节,发布事实写入 changelog 和验收记录,不在文档开头或结尾追加另一套现行说明。
|
|
@@ -785,19 +786,21 @@ DSH 插件按公开 API 的兼容系列声明 `>=0.2.1-rc.5 <0.3.0-0`;DSH 使
|
|
|
785
786
|
|
|
786
787
|
### 13.1 npm 安装与发行
|
|
787
788
|
|
|
788
|
-
正式分发以 npm 为入口:`npx -y @ckanner/flywheel@latest install` 显式协调同版本全局 CLI、worker、DSH 桌面插件与内嵌 skills。普通 `npm install` 不通过 postinstall 修改用户配置;安装器不要求源码 checkout 或本机 Go
|
|
789
|
+
正式分发以 npm 为入口:`npx -y @ckanner/flywheel@latest install` 显式协调同版本全局 CLI、worker、DSH 桌面插件与内嵌 skills。普通 `npm install` 不通过 postinstall 修改用户配置;安装器不要求源码 checkout 或本机 Go 编译器。从 0.33 起只发布 `@ckanner/flywheel`:一个 tarball 内含 CLI、DSH Host/Client、worker 与四个平台 Go 二进制;skills 与模板继续内嵌在引擎中。`native/manifest.json` 固定版本、平台集合和各二进制摘要,启动器仅选择包内当前平台并核对完整性,不再下载或查找独立平台包。源码中的 `plugins/dsh` 仅为 private 构建/测试区。安装不会替换用户的 DSH 桌面或代办模型账号登录。
|
|
790
|
+
|
|
791
|
+
桌面从拆包版本升级时,安装器先读取现有依赖,安装并确认新的完整包,再通过官方 DSH CLI 移除旧 `@ckanner/dsh-plugin-flywheel`,最后确认旧名不存在且新版本精确匹配。旧包移除失败时,仅在能确认旧包仍存在且替代包为本次新增的情况下回退;取消不启动新的回退子进程,局部状态在 JSON 报告中保留,重试先重新读取实际状态。自定义 file/git 来源的旧插件不自动迁移。升级前应退出 DSH;当前进程未重载不算新版已生效。全局配置、项目契约和历史调用账本不因包迁移而重写。
|
|
789
792
|
|
|
790
793
|
npm shim 异步运行原生引擎,收到 TERM/INT/HUP 时转发给 Go 并等待退出;不能把仅 wrapper 退出当成子进程完成。worker 通过安装包公开入口解析绝对路径。桌面入口使用安装记录与绝对启动路径,避免依赖 GUI 恰好继承终端 PATH。用户显式配置优先,安装不覆盖已有 Flywheel 模型配置。
|
|
791
794
|
|
|
792
795
|
skills 继续从同一 Go 二进制内嵌资源安装。每个受管快照保存内容与权限清单;升级前确认原内容未被用户修改,额外文件、未知标记或陌生目录保留并报冲突。先写完整 staging,再以 rename 切换并保留失败回滚;这是逐 skill 更新,不是全部目录的整体事务。旧文本标记只在目录与当前内嵌快照完全一致时迁移。`install-skills --json` 供安装器取得完整结果,部分失败不能报告整套成功。
|
|
793
796
|
|
|
794
|
-
发布以精确 HEAD
|
|
797
|
+
发布以精确 HEAD 标签与干净候选为依据。构建一次完整单包候选(candidate format 2),Linux/macOS 安装测试消费同一个 tarball;候选验证同时核对四个平台二进制、公开入口、MIT 许可和内置 adapter。已存在版本必须核对 integrity;先发 candidate 标签,精确版本从 registry 安装验证后才推进 latest,防止倒退。源码 PR CI 通过、npm pack 成功、registry 安装通过与用户 Mac 实机验收是不同证据,分别记录。
|
|
795
798
|
|
|
796
|
-
历史 0.28.2 使用 DeepSeek API Key 默认,账号默认修复从 0.28.3 开始提供;这些版本差异保留在 changelog 和发行记录,升级仍须显式迁移已有配置,不重写已发布的包。DSH 插件精确依赖上游 `dsh-subagent-codex-pro@0.9.0`;该已审查源码候选以真实 npm tarball 锁定并 bundle
|
|
799
|
+
历史 0.28.2 使用 DeepSeek API Key 默认,账号默认修复从 0.28.3 开始提供;这些版本差异保留在 changelog 和发行记录,升级仍须显式迁移已有配置,不重写已发布的包。DSH 插件精确依赖上游 `dsh-subagent-codex-pro@0.9.0`;该已审查源码候选以真实 npm tarball 锁定并 bundle 到完整包,来源、许可证和摘要见 [vendor 记录](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/vendor/README.md)。Claude 0.4.0 维持可选 peer。旧发行失败与重试记录保留在 [npm 验证记录](validation/2026-11-01-npm-installation.md);源码、候选、CI、registry 和 Mac 实机证据分别记录,不混为一个“测试通过”。
|
|
797
800
|
|
|
798
801
|
CI、候选构建、双平台验证和发布统一固定为 Node 24.22.1/npm 12.2.1,预检只需同版 Node。发行协议测试通过隔离的本地 registry 调用真实 `npm view`,覆盖 latest、integrity、缺失字段与错误响应。存在的字段必须返回字符串,兼容用单元素数组包裹;多结果拒绝,缺失 latest 单独处理,缺失 integrity 不能当成版本不存在。只有结构化 E404 表示版本不存在,取消、认证或网络错误仍阻止发布。npm 11/12 的 `pack --json` 兼容保持严格包身份检查。
|
|
799
802
|
|
|
800
|
-
GitHub Actions 的 publish job 使用固定 Node/npm
|
|
803
|
+
GitHub Actions 的 publish job 使用固定 Node/npm 工具链,仅通过 OIDC trusted publishing 认证;显式使用空 user npmrc 并清空认证占位值,不依赖存储 token 或浏览器逐包确认。只为完整包 `@ckanner/flywheel` 配置对应 `ckanner/flywheel`/`publish.yml` 的 npm Trusted Publisher,同时允许 publish 与 dist-tag。GitHub 登录或 `id-token: write` 本身不授予 npm 包写权限;新连接需实际 CI 发布来完成 npm 的验证。维护者配置和失败恢复见 [发行指南](zh-CN/releasing.md)。
|
|
801
804
|
|
|
802
805
|
私有仓库的发行同样经过完整门禁。发布 job 在准备 npm 后,由官方 checkout 使用短暂的只读 GitHub 认证重新取得完整 refs,固定到已验证 source,再下载候选并检查精确 tag;后续不执行无凭证的远端 fetch。仓库可见性由 GitHub 事件显式传给 publisher,未知值拒绝发布。public 要求 npm provenance,private 显式关闭 npm 不支持的证明生成;这只记录 registry 的能力边界,不改变仓库可见性,也不取消本项目的 source/artifact integrity 校验。
|
|
803
806
|
|
package/docs/en/configuration.md
CHANGED
|
@@ -542,7 +542,7 @@ Main-task fresh and scoped calls both use outer worker protocol 3. Fresh selects
|
|
|
542
542
|
|
|
543
543
|
| Variable | Purpose |
|
|
544
544
|
|---|---|
|
|
545
|
-
| `FLYWHEEL_DSH_WORKER` | Optional DSH worker override; the npm launcher normally resolves the
|
|
545
|
+
| `FLYWHEEL_DSH_WORKER` | Optional DSH worker override; the npm launcher normally resolves the worker inside its own package |
|
|
546
546
|
| `FLYWHEEL_DSH_BIN` | DSH command, otherwise `dsh` on PATH |
|
|
547
547
|
| `FLYWHEEL_DSH_PROFILE` | SDK profile, default `sdk`; it must not be `desktop` |
|
|
548
548
|
| `DSH_HOME` | Optional DSH configuration root, interpreted by DSH |
|
package/docs/en/contributing.md
CHANGED
|
@@ -100,7 +100,7 @@ FLYWHEEL_NPM_PACKAGES_DIR=dist-npm npm run test:install
|
|
|
100
100
|
|---|---|
|
|
101
101
|
| Go engine | `make quality-gate`: vet, formatting, tests, and coverage |
|
|
102
102
|
| DSH plugin or worker | Plugin checks and the applicable Go-to-Node bridges |
|
|
103
|
-
| npm installation or release | Root npm tests, a
|
|
103
|
+
| npm installation or release | Root npm tests, a complete single-package candidate build, and installation of that tarball |
|
|
104
104
|
| Documentation | Links, anchors, fences, runnable examples, translation consistency, and affected embedding/package contents |
|
|
105
105
|
|
|
106
106
|
The Claude bridge needs the exact optional 0.4.0 machine API in the worker's
|
|
@@ -288,16 +288,15 @@ that limitation. Source merge and npm publication remain separate operations.
|
|
|
288
288
|
|
|
289
289
|
## Release changes
|
|
290
290
|
|
|
291
|
-
Follow the [release guide](releasing.md).
|
|
292
|
-
|
|
291
|
+
Follow the [release guide](releasing.md). Flywheel publishes one complete package containing all native platforms and the
|
|
292
|
+
pinned Codex Pro runtime. The private DSH source harness is not published. Build
|
|
293
293
|
once from final clean source and test those tarballs. Preserve the source commit,
|
|
294
294
|
tree, snapshot hash, and package integrity values; the release tag must identify
|
|
295
295
|
that source. Do not relabel a dirty candidate as a clean release or rebuild
|
|
296
296
|
different bytes for a published version.
|
|
297
297
|
|
|
298
298
|
Use the repository's release scripts for candidate checks, publication, registry
|
|
299
|
-
verification, and promotion. Publish
|
|
300
|
-
|
|
301
|
-
all six tags and integrities. A partial publication resumes with the same
|
|
299
|
+
verification, and promotion. Publish the complete candidate, test the exact registry version before moving
|
|
300
|
+
`latest`, and verify the tag and integrity. A partial publication resumes with the same
|
|
302
301
|
candidate. Manual publication requires its own explicit authorization and an
|
|
303
302
|
accurate record of completed and unavailable checks.
|
package/docs/en/flow-console.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[Documentation](../README.md) · English · [简体中文](../zh-CN/flow-console.md)
|
|
4
4
|
|
|
5
5
|
The DSH Flywheel view brings the current task, workflow stages, configuration, session
|
|
6
|
-
interaction, and delivery into one place. Keep the CLI,
|
|
6
|
+
interaction, and delivery into one place. Keep the CLI, bundled engines, and DSH
|
|
7
7
|
plugin at the same version. For installation, see [Getting started](getting-started.md).
|
|
8
8
|
|
|
9
9
|
## Find a task without entering paths
|
|
@@ -5,10 +5,17 @@
|
|
|
5
5
|
Install Flywheel, connect the backends you intend to use, then run a small task.
|
|
6
6
|
This guide follows the current repository. Check the installed version and the npm
|
|
7
7
|
version you choose; a merged change does not update an existing installation.
|
|
8
|
-
Keep the CLI,
|
|
8
|
+
Keep the CLI, bundled engines, and DSH plugin at one version.
|
|
9
9
|
|
|
10
10
|
For the complete path through task branches and PR review, see [Workflow](workflow.md).
|
|
11
11
|
|
|
12
|
+
From 0.33, `@ckanner/flywheel` is one complete package: CLI, DSH view/workers,
|
|
13
|
+
embedded skills and four platform engines. No separate platform package or first-run
|
|
14
|
+
engine download is needed. When upgrading from 0.32.x, quit DSH before running the
|
|
15
|
+
installer; it replaces the old desktop package registration and preserves your
|
|
16
|
+
configuration and histories. Reload DSH after setup. Existing old npm versions
|
|
17
|
+
remain available for historical installations.
|
|
18
|
+
|
|
12
19
|
## 1. Prepare the environment
|
|
13
20
|
|
|
14
21
|
| Requirement | Purpose |
|
|
@@ -146,7 +153,7 @@ capabilities or merge full worker histories into the parent trajectory. See the
|
|
|
146
153
|
|
|
147
154
|
Setup writes `~/.config/flywheel/npm-launcher` and an installation record containing
|
|
148
155
|
absolute Node/CLI paths and the checked DSH entry. An explicit `flywheelBinary`
|
|
149
|
-
overrides the managed launcher.
|
|
156
|
+
overrides the managed launcher. Without a record, the installed package uses its bundled CLI;
|
|
150
157
|
invalid records or modified launchers fail visibly.
|
|
151
158
|
|
|
152
159
|
After moving Node/the npm prefix or installing missing tools, rerun
|