@ckanner/flywheel 0.33.1 → 0.34.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 +22 -0
- package/README.md +1 -0
- package/README.zh-CN.md +2 -0
- package/THIRD_PARTY_NOTICES.md +5 -1
- package/bin/flywheel-kimi-native-worker.js +5 -0
- package/bin/flywheel.js +8 -1
- package/bin/runtime.js +5 -1
- package/dist/bridge.mjs +89 -7
- package/dist/claude-code-pro-bridge.mjs +55 -6
- package/dist/client.js +40 -5
- package/dist/codex-pro-bridge.mjs +12 -5
- package/dist/control.mjs +1 -1
- package/dist/kimi-native-bridge.mjs +429 -0
- package/dist/kimi-native-runtime.mjs +233 -0
- package/dist/kimi-native-worker.mjs +77 -0
- package/dist/kimi-tower-candidate.mjs +227 -0
- package/dist/machine-worker.mjs +39 -5
- package/dist/managed-lcu.mjs +223 -0
- package/dist/native-doctor.mjs +36 -0
- package/dist/native-jobs.mjs +72 -0
- package/dist/native-policy.mjs +371 -0
- package/dist/native-resources.mjs +144 -0
- package/dist/native.mjs +251 -0
- package/dist/policy.mjs +13 -3
- package/dist/prewarm.mjs +4 -1
- package/dist/protocol.mjs +8 -1
- package/dist/worker.mjs +16 -9
- package/docs/README.md +9 -0
- package/docs/README.zh-CN.md +3 -0
- package/docs/design.md +33 -11
- package/docs/en/claude-code-pro.md +2 -2
- package/docs/en/configuration.md +17 -5
- package/docs/en/contributing.md +5 -3
- package/docs/en/getting-started.md +2 -2
- package/docs/en/model-policy.md +2 -2
- package/docs/en/native-capabilities.md +185 -0
- package/docs/en/releasing.md +2 -2
- package/docs/en/session-recovery.md +2 -2
- package/docs/plans/2026-11-13-native-harness-capabilities.en.md +258 -0
- package/docs/plans/2026-11-13-native-harness-capabilities.md +276 -0
- package/docs/plans/README.md +1 -0
- package/docs/validation/2026-11-14-desktop-native-configuration.md +128 -0
- package/docs/validation/2026-11-14-native-harness-implementation.md +215 -0
- package/docs/validation/2026-11-14-published-companions.md +15 -0
- package/docs/validation/2026-11-14-release-0.33.1.md +78 -0
- package/docs/validation/README.md +3 -0
- package/docs/validation/evidence/2026-11-14-native-harness-candidate.json +255 -0
- package/docs/validation/evidence/2026-11-14-native-harness-sessions.json +6009 -0
- package/docs/validation/evidence/2026-11-14-native-harness-tower-candidate.json +192 -0
- package/docs/zh-CN/claude-code-pro.md +2 -2
- package/docs/zh-CN/configuration.md +21 -8
- package/docs/zh-CN/contributing.md +5 -3
- package/docs/zh-CN/getting-started.md +2 -2
- package/docs/zh-CN/model-policy.md +2 -2
- package/docs/zh-CN/native-capabilities.md +184 -0
- package/docs/zh-CN/releasing.md +2 -2
- package/docs/zh-CN/session-recovery.md +1 -1
- 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 +9 -9
- package/node_modules/dsh-preset-generator/CHANGELOG.md +77 -0
- package/node_modules/dsh-preset-generator/LICENSE +21 -0
- package/node_modules/dsh-preset-generator/README.md +149 -0
- package/node_modules/dsh-preset-generator/docs/README.zh.md +95 -0
- package/node_modules/dsh-preset-generator/package.json +43 -0
- package/node_modules/dsh-preset-generator/src/composition.mjs +150 -0
- package/node_modules/dsh-preset-generator/src/gen-presets.mjs +367 -0
- package/node_modules/dsh-preset-generator/src/policy.mjs +17 -0
- package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +16 -1
- package/node_modules/dsh-subagent-codex-pro/README.md +57 -4
- package/node_modules/dsh-subagent-codex-pro/assets/native-client.js +118 -0
- package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +6 -0
- package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +49 -2
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +1 -1
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +1 -1
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +1 -1
- package/node_modules/dsh-subagent-codex-pro/docs/machine.md +2 -2
- package/node_modules/dsh-subagent-codex-pro/docs/native-capabilities.md +254 -0
- package/node_modules/dsh-subagent-codex-pro/docs/native-capabilities.zh.md +180 -0
- package/node_modules/dsh-subagent-codex-pro/lib/config.js +56 -0
- package/node_modules/dsh-subagent-codex-pro/lib/config.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/desktop-native.js +109 -0
- package/node_modules/dsh-subagent-codex-pro/lib/desktop-native.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +3 -4
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/index.js +64 -15
- package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +6 -2
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +2 -2
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +5 -2
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +11 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js +20 -3
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/native.js +297 -0
- package/node_modules/dsh-subagent-codex-pro/lib/native.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +11 -1
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/run.js +19 -1
- package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js +123 -57
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/config.d.ts +31 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/config.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/desktop-native.d.ts +30 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/desktop-native.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +7 -5
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +31 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +2 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +7 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/native.d.ts +120 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/native.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +11 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +11 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +7 -1
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js +34 -6
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -1
- package/node_modules/dsh-subagent-codex-pro/package.json +28 -4
- package/npm/install.js +2 -2
- package/package.json +25 -10
- package/plugins/dsh/KIMI_NATIVE_PROTOCOL.en.md +76 -0
- package/plugins/dsh/KIMI_NATIVE_PROTOCOL.md +76 -0
- package/plugins/dsh/PROTOCOL.md +149 -17
- package/plugins/dsh/README.md +15 -12
- package/plugins/dsh/README.zh-CN.md +8 -8
- package/plugins/dsh/THIRD_PARTY_NOTICES.md +5 -1
- package/plugins/dsh/vendor/README.md +40 -11
- package/plugins/dsh/vendor/chatgpt.json +21 -0
- package/plugins/dsh/vendor/claude-code-pro.json +21 -0
- package/plugins/dsh/vendor/codex-pro.json +13 -8
- package/plugins/dsh/vendor/lcu.json +21 -0
- package/plugins/dsh/vendor/preset-generator.json +16 -0
package/dist/native.mjs
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { BridgeError, record, nonempty, SessionFold } from './protocol.mjs';
|
|
3
|
+
|
|
4
|
+
export function nativeDigest(value) {
|
|
5
|
+
const canonical = v => Array.isArray(v) ? v.map(canonical) : record(v)
|
|
6
|
+
? Object.fromEntries(Object.keys(v).sort().map(k => [k, canonical(v[k])])) : v;
|
|
7
|
+
return createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validateNative(value, harness = 'dsh') {
|
|
11
|
+
if (value === undefined) return;
|
|
12
|
+
if (!record(value) || value.version !== 1) throw new BridgeError('INVALID_REQUEST', 'native requires version 1');
|
|
13
|
+
if (harness !== 'dsh') return; // Public machine adapters own their native vocabularies.
|
|
14
|
+
const keys = ['version', 'preset', 'agents', 'team', 'workflows', 'web', 'lcu'];
|
|
15
|
+
if (Object.keys(value).some(k => !keys.includes(k)) || !['heavy', 'daily'].includes(value.preset)) {
|
|
16
|
+
throw new BridgeError('INVALID_REQUEST', 'DSH native requires a daily/heavy PTC composition');
|
|
17
|
+
}
|
|
18
|
+
for (const [name, fields] of Object.entries({ agents: ['maxConcurrent', 'maxDepth'], team: [], workflows: [], lcu: ['config'] })) {
|
|
19
|
+
const option = value[name];
|
|
20
|
+
if (option === undefined) continue;
|
|
21
|
+
if (!record(option) || typeof option.enabled !== 'boolean' || Object.keys(option).some(k => !['enabled', ...fields].includes(k))) {
|
|
22
|
+
throw new BridgeError('INVALID_REQUEST', `invalid native.${name}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const [key, limit] of [['maxConcurrent', 64], ['maxDepth', 16]]) {
|
|
26
|
+
const limitValue = value.agents?.[key];
|
|
27
|
+
if (limitValue !== undefined && (!Number.isSafeInteger(limitValue) || limitValue < 1 || limitValue > limit)) {
|
|
28
|
+
throw new BridgeError('INVALID_REQUEST', `invalid native.agents.${key}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if ((value.team?.enabled || value.workflows?.enabled) && value.agents?.enabled !== true) {
|
|
32
|
+
throw new BridgeError('INVALID_REQUEST', 'Team and Workflows require enabled native agents');
|
|
33
|
+
}
|
|
34
|
+
if (value.web !== undefined && (!record(value.web) || Object.entries(value.web).some(([k, v]) => !['search', 'fetch'].includes(k) || typeof v !== 'boolean'))) {
|
|
35
|
+
throw new BridgeError('INVALID_REQUEST', 'invalid native.web');
|
|
36
|
+
}
|
|
37
|
+
if (value.lcu?.config !== undefined && (!record(value.lcu.config) || Object.entries(value.lcu.config).some(([k, v]) =>
|
|
38
|
+
['app', 'command'].includes(k) ? typeof v !== 'string' || !v.trim() || v.includes('\0') :
|
|
39
|
+
['chrome', 'audio'].includes(k) ? typeof v !== 'boolean' :
|
|
40
|
+
['allowedApps', 'allowedOrigins'].includes(k) ? !Array.isArray(v) || v.some(s => typeof s !== 'string' || !s.trim()) : true))) {
|
|
41
|
+
throw new BridgeError('INVALID_REQUEST', 'invalid native.lcu.config');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateManaged(value, requestId) {
|
|
46
|
+
if (value === undefined) return;
|
|
47
|
+
if (!record(value) || value.version !== 1 || value.invocationId !== requestId ||
|
|
48
|
+
Object.keys(value).some(k => !['version', 'runId', 'invocationId', 'role', 'scope', 'candidateId', 'channel', 'taskDir'].includes(k))) {
|
|
49
|
+
throw new BridgeError('INVALID_REQUEST', 'native admission identity does not match its invocation');
|
|
50
|
+
}
|
|
51
|
+
for (const key of ['invocationId', 'role', 'scope', 'channel', 'taskDir']) nonempty(value[key], `managed.${key}`, 4096);
|
|
52
|
+
if (typeof value.runId !== 'string' || (value.candidateId !== undefined && typeof value.candidateId !== 'string')) {
|
|
53
|
+
throw new BridgeError('INVALID_REQUEST', 'invalid native run/candidate identity');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const MAX_NATIVE_EVENTS = 4096;
|
|
58
|
+
const MAX_NATIVE_EVENT_BYTES = 2 * 1024 * 1024;
|
|
59
|
+
const usageKeys = ['input_tokens', 'output_tokens', 'reasoning_tokens', 'cache_read_tokens', 'cache_write_tokens', 'total_tokens'];
|
|
60
|
+
const validUsage = usage => record(usage) && usageKeys.every(key => Number.isSafeInteger(usage[key]) && usage[key] >= 0);
|
|
61
|
+
const identityString = value => typeof value === 'string' && value.length > 0 && value.length <= 4096 && !value.includes('\0');
|
|
62
|
+
|
|
63
|
+
/** Native child histories contribute usage, never the root's answer or verdict. */
|
|
64
|
+
export class NativeTree {
|
|
65
|
+
constructor(rootSessionId, request, emit = () => {}) {
|
|
66
|
+
this.rootSessionId = rootSessionId;
|
|
67
|
+
this.request = request;
|
|
68
|
+
this.emit = emit;
|
|
69
|
+
this.children = new Map();
|
|
70
|
+
this.activations = new Map();
|
|
71
|
+
this.runtimeSeen = new Map();
|
|
72
|
+
this.events = [];
|
|
73
|
+
this.sequence = 0;
|
|
74
|
+
this.eventBytes = 0;
|
|
75
|
+
this.observationError = null;
|
|
76
|
+
}
|
|
77
|
+
fail(message) {
|
|
78
|
+
this.observationError ??= message;
|
|
79
|
+
throw new BridgeError('TRANSPORT', message);
|
|
80
|
+
}
|
|
81
|
+
event(kind, facts) {
|
|
82
|
+
const event = { version: 1, sequence: this.sequence + 1, kind, ...facts };
|
|
83
|
+
const bytes = Buffer.byteLength(JSON.stringify(event));
|
|
84
|
+
if (this.events.length >= MAX_NATIVE_EVENTS || this.eventBytes + bytes > MAX_NATIVE_EVENT_BYTES) {
|
|
85
|
+
this.fail('Native observation journal exceeded its bounded event budget');
|
|
86
|
+
}
|
|
87
|
+
this.sequence += 1;
|
|
88
|
+
this.eventBytes += bytes;
|
|
89
|
+
this.events.push(event);
|
|
90
|
+
this.emit(event);
|
|
91
|
+
}
|
|
92
|
+
ownsParent(id) { return id === this.rootSessionId || this.children.has(id); }
|
|
93
|
+
child(id, parentId, local = true) {
|
|
94
|
+
if (!this.request.agents?.enabled || !identityString(id) || id === this.rootSessionId || !this.ownsParent(parentId)) {
|
|
95
|
+
this.fail('Native child dispatch has no admitted parent lineage');
|
|
96
|
+
}
|
|
97
|
+
const existing = this.children.get(id);
|
|
98
|
+
if (existing && (existing.parentId !== parentId || existing.local !== local)) this.fail('Native child changed parent or ownership kind');
|
|
99
|
+
if (existing) return existing;
|
|
100
|
+
const child = { id, parentId, kind: 'agent', local, state: 'created', terminal: false,
|
|
101
|
+
fold: local ? new SessionFold(id, { strict: true }) : null, fingerprints: new Map(), lastEventSequence: -1, currentTurn: -1 };
|
|
102
|
+
this.children.set(id, child);
|
|
103
|
+
this.event('child.created', { id, parentId, local, state: 'created' });
|
|
104
|
+
return child;
|
|
105
|
+
}
|
|
106
|
+
notify(method, params) {
|
|
107
|
+
if (method === 'subagent.started') {
|
|
108
|
+
if (!record(params)) this.fail('Malformed native child publication');
|
|
109
|
+
this.child(params.childSessionId, params.parentSessionId);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const child = this.children.get(params?.sessionId ?? params?.childSessionId);
|
|
113
|
+
if (method === 'session.event' && child?.local) {
|
|
114
|
+
const event = params.event;
|
|
115
|
+
if (!record(event) || !Number.isSafeInteger(event.seq)) this.fail('Malformed native child event');
|
|
116
|
+
const digest = nativeDigest(event);
|
|
117
|
+
if (child.fingerprints.has(event.seq)) {
|
|
118
|
+
if (child.fingerprints.get(event.seq) !== digest) this.fail('Native child replay changed a committed event');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (event.seq < child.lastEventSequence) this.fail('Native child events arrived out of order');
|
|
122
|
+
if (child.fold.accept(params)) {
|
|
123
|
+
child.fingerprints.set(event.seq, digest);
|
|
124
|
+
child.lastEventSequence = event.seq;
|
|
125
|
+
const turn = event.data.turn;
|
|
126
|
+
if (event.type === 'turn/start') {
|
|
127
|
+
if (!Number.isSafeInteger(turn) || turn < 0 || turn < child.currentTurn) this.fail('Native child turn identity regressed');
|
|
128
|
+
child.currentTurn = turn;
|
|
129
|
+
child.terminal = false;
|
|
130
|
+
child.state = 'active';
|
|
131
|
+
child.fold.terminal = null;
|
|
132
|
+
} else if (event.type === 'turn/end') {
|
|
133
|
+
if (turn < child.currentTurn) this.fail('A late native terminal belongs to an earlier turn');
|
|
134
|
+
child.currentTurn = turn;
|
|
135
|
+
child.state = event.data.reason.kind;
|
|
136
|
+
// A committed turn ending does not establish residency-epoch settlement.
|
|
137
|
+
// subagent.finished / public subagent/end owns that separate fact.
|
|
138
|
+
}
|
|
139
|
+
this.event('child.event', { id: child.id, parentId: child.parentId, state: child.state,
|
|
140
|
+
eventType: event.type, eventSequence: event.seq, turnId: Number.isSafeInteger(turn) ? String(turn) : child.fold.turnId || null });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (method === 'subagent.finished') {
|
|
144
|
+
if (!child) {
|
|
145
|
+
if (this.ownsParent(params?.parentSessionId)) this.fail('Native child settled without a published start');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (params.parentSessionId !== child.parentId || !identityString(params.stopReason)) this.fail('Native child terminal changed parent or lacks a stop reason');
|
|
149
|
+
if (child.terminal && child.state === params.stopReason) return;
|
|
150
|
+
child.state = params.stopReason;
|
|
151
|
+
child.terminal = true;
|
|
152
|
+
this.event('child.finished', { id: child.id, parentId: child.parentId, state: child.state });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Fold the SDK policy's durable public lifecycle observations, idempotently. */
|
|
156
|
+
observeRuntime(events) {
|
|
157
|
+
if (!Array.isArray(events) || events.length > MAX_NATIVE_EVENTS) this.fail('Malformed native runtime journal');
|
|
158
|
+
for (const event of events) {
|
|
159
|
+
if (!['subagent.started', 'subagent.finished', 'agent.admission-rejected'].includes(event?.type)) continue;
|
|
160
|
+
if (event.version !== 1 || !Number.isSafeInteger(event.journalSequence) || event.journalSequence < 1 ||
|
|
161
|
+
event.rootSessionId !== this.rootSessionId) this.fail('Unowned native runtime lifecycle observation');
|
|
162
|
+
const fingerprint = nativeDigest(event);
|
|
163
|
+
if (this.runtimeSeen.has(event.journalSequence)) {
|
|
164
|
+
if (this.runtimeSeen.get(event.journalSequence) !== fingerprint) this.fail('Native runtime journal changed an observed event');
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (this.runtimeSeen.size >= MAX_NATIVE_EVENTS) this.fail('Native runtime journal exceeded its bounded event budget');
|
|
168
|
+
if (event.type === 'agent.admission-rejected') {
|
|
169
|
+
const child = this.children.get(event.id);
|
|
170
|
+
if (!child || event.id === this.rootSessionId) continue;
|
|
171
|
+
if (!child.local || child.parentId !== event.parentId || event.matched !== false) this.fail('Native child rejection changed its owned identity');
|
|
172
|
+
const active = [...this.activations.values()].some(value => value.id === child.id && !value.terminal);
|
|
173
|
+
const proven = event.beforeModelDispatch === true && event.parentLive === true && !active;
|
|
174
|
+
child.rejectedAdmissions ??= [];
|
|
175
|
+
child.rejectedAdmissions.push({ journalSequence: event.journalSequence, beforeModelDispatch: proven });
|
|
176
|
+
// Rejecting a new attempt never resets previous paid turns. Only a
|
|
177
|
+
// fresh, wholly undispatched child can acquire a proved-zero usage.
|
|
178
|
+
if (proven && child.fold.assistantCount === 0 && child.fold.terminal === null) {
|
|
179
|
+
child.state = 'admission-rejected';
|
|
180
|
+
child.terminal = true;
|
|
181
|
+
child.admissionRejected = true;
|
|
182
|
+
} else if (!proven) child.terminal = false;
|
|
183
|
+
this.runtimeSeen.set(event.journalSequence, fingerprint);
|
|
184
|
+
this.event('child.admission-rejected', { id: child.id, parentId: child.parentId, beforeModelDispatch: proven });
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const info = event.child;
|
|
188
|
+
if (!record(info) || !identityString(info.runId) || !identityString(info.provider) || typeof info.local !== 'boolean') {
|
|
189
|
+
this.fail('Malformed native runtime child identity');
|
|
190
|
+
}
|
|
191
|
+
const child = this.child(info.id, info.parentSessionId, info.local);
|
|
192
|
+
let activation = this.activations.get(info.runId);
|
|
193
|
+
if (activation && (activation.id !== info.id || activation.parentId !== info.parentSessionId ||
|
|
194
|
+
activation.provider !== info.provider || activation.local !== info.local)) this.fail('Native runtime activation changed identity');
|
|
195
|
+
if (event.type === 'subagent.started') {
|
|
196
|
+
if (!activation) {
|
|
197
|
+
activation = { runId: info.runId, id: info.id, parentId: info.parentSessionId, provider: info.provider,
|
|
198
|
+
local: info.local, state: 'active', terminal: false };
|
|
199
|
+
this.activations.set(info.runId, activation);
|
|
200
|
+
child.admissionRejected = false;
|
|
201
|
+
// Local wire events may already have reported settlement before this
|
|
202
|
+
// journal is read. An active epoch still appears in completion().
|
|
203
|
+
if (!info.local) { child.state = 'active'; child.terminal = false; }
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
if (!activation || !identityString(info.stopReason)) this.fail('Native runtime terminal has no attributable start');
|
|
207
|
+
if (activation.terminal && activation.state !== info.stopReason) this.fail('Native runtime terminal outcome changed');
|
|
208
|
+
activation.state = info.stopReason;
|
|
209
|
+
activation.terminal = true;
|
|
210
|
+
child.state = info.stopReason;
|
|
211
|
+
child.terminal = ![...this.activations.values()].some(value => value.id === child.id && !value.terminal);
|
|
212
|
+
}
|
|
213
|
+
this.runtimeSeen.set(event.journalSequence, fingerprint);
|
|
214
|
+
this.event(event.type, { id: child.id, parentId: child.parentId, runId: info.runId,
|
|
215
|
+
provider: info.provider, local: info.local, state: activation.state });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Capture this at root completion, before shutdown can cancel pending work. */
|
|
219
|
+
completion() {
|
|
220
|
+
const pending = new Set([...this.children.values()].filter(child => !child.terminal).map(child => child.id));
|
|
221
|
+
for (const activation of this.activations.values()) if (!activation.terminal) pending.add(activation.id);
|
|
222
|
+
const pendingIds = [...pending].sort();
|
|
223
|
+
return { confirmed: this.observationError === null && pendingIds.length === 0, pendingIds };
|
|
224
|
+
}
|
|
225
|
+
receipt(effective, cleanup) {
|
|
226
|
+
return { version: 1, requested: this.request, effective,
|
|
227
|
+
observed: { events: this.events, truncated: false, complete: this.observationError === null,
|
|
228
|
+
...(this.observationError ? { error: this.observationError } : {}) },
|
|
229
|
+
children: [...this.children.values()].map(({ fold, fingerprints, lastEventSequence, currentTurn, ...child }) => ({ ...child,
|
|
230
|
+
turnId: fold?.turnId || null, usage: this.knownUsage(child, fold) ? { ...fold.usage } : null })),
|
|
231
|
+
activations: [...this.activations.values()].map(value => ({ ...value })), cleanup,
|
|
232
|
+
usage: { strategy: 'exclusive-committed-session-events', status: this.usageComplete() ? 'complete' : 'partial' } };
|
|
233
|
+
}
|
|
234
|
+
knownUsage(child, fold = child.fold) {
|
|
235
|
+
return child.local && child.terminal && fold?.usageComplete && validUsage(fold.usage) &&
|
|
236
|
+
(fold.assistantCount > 0 && fold.terminal !== null || child.admissionRejected === true && fold.assistantCount === 0);
|
|
237
|
+
}
|
|
238
|
+
usageComplete() {
|
|
239
|
+
return this.observationError === null && this.completion().confirmed && [...this.children.values()].every(child => this.knownUsage(child));
|
|
240
|
+
}
|
|
241
|
+
addUsage(rootUsage) {
|
|
242
|
+
if (!validUsage(rootUsage) || !this.usageComplete()) return null;
|
|
243
|
+
const result = { ...rootUsage };
|
|
244
|
+
for (const { fold } of this.children.values()) for (const key of usageKeys) {
|
|
245
|
+
const sum = result[key] + fold.usage[key];
|
|
246
|
+
if (!Number.isSafeInteger(sum)) return null;
|
|
247
|
+
result[key] = sum;
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
}
|
package/dist/policy.mjs
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
import { link, realpath, stat, unlink, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, isAbsolute } from 'node:path';
|
|
5
5
|
import { readPolicyReceipt, nonempty } from './protocol.mjs';
|
|
6
|
+
import { createNativePolicy } from './native-policy.mjs';
|
|
6
7
|
|
|
7
8
|
export const inject = ['sandboxPolicy', 'approval', 'shell', 'workingDirectory'];
|
|
8
9
|
|
|
9
10
|
export async function apply(ctx, config) {
|
|
10
11
|
const { permissionMode, cwd, workspaceCwd, receiptPath, requestId, nonce, deferredAdmissionPath } = config;
|
|
11
12
|
let { sessionTitle, sessionId } = config;
|
|
13
|
+
let nativePolicy;
|
|
12
14
|
const fullAccess = permissionMode === 'danger-full-access';
|
|
13
15
|
if (!['read-only', 'workspace-write', 'danger-full-access'].includes(permissionMode) || typeof cwd !== 'string' ||
|
|
14
16
|
ctx.shell.sandboxMode === undefined) {
|
|
@@ -45,13 +47,18 @@ export async function apply(ctx, config) {
|
|
|
45
47
|
const session = agent.session;
|
|
46
48
|
if (deferredAdmissionPath !== undefined) {
|
|
47
49
|
const identity = await readPolicyReceipt(deferredAdmissionPath, { requestId, nonce });
|
|
48
|
-
if (Object.keys(identity).some(key => !['requestId', 'nonce', 'sessionId', 'sessionTitle'].includes(key))) {
|
|
50
|
+
if (Object.keys(identity).some(key => !['requestId', 'nonce', 'sessionId', 'sessionTitle', 'managed', 'resuming'].includes(key))) {
|
|
49
51
|
throw new Error('Flywheel deferred admission has unsupported fields');
|
|
50
52
|
}
|
|
51
53
|
sessionId = nonempty(identity.sessionId, 'sessionId', 512);
|
|
52
54
|
sessionTitle = identity.sessionTitle === undefined ? undefined : nonempty(identity.sessionTitle, 'sessionTitle', 512);
|
|
55
|
+
config.managed = identity.managed;
|
|
56
|
+
config.resuming = identity.resuming;
|
|
53
57
|
}
|
|
54
|
-
|
|
58
|
+
config.sessionId = sessionId;
|
|
59
|
+
nativePolicy ??= await createNativePolicy(ctx, config);
|
|
60
|
+
const root = String(session.id) === sessionId;
|
|
61
|
+
if (!root && !nativePolicy?.isChild(agent)) throw new Error('Flywheel policy refuses an unrelated SDK session');
|
|
55
62
|
if (workspaceCwd !== undefined && session.header.cwd !== workspaceCwd) {
|
|
56
63
|
throw new Error('Flywheel refuses to relabel a session from a different project workspace');
|
|
57
64
|
}
|
|
@@ -61,17 +68,20 @@ export async function apply(ctx, config) {
|
|
|
61
68
|
// unsupported split workspace-write requests are rejected at admission.
|
|
62
69
|
await ctx.workingDirectory.set(agent, cwd);
|
|
63
70
|
}
|
|
64
|
-
if (sessionTitle && ctx.get('sessionTitle')) {
|
|
71
|
+
if (root && sessionTitle && ctx.get('sessionTitle')) {
|
|
65
72
|
ctx.get('sessionTitle').rename(session, [...sessionTitle].slice(0, 35).join(''));
|
|
66
73
|
}
|
|
67
74
|
// Official events remain readable by a later SDK. An ad-hoc Session event
|
|
68
75
|
// would make rc.1 reject the entire persisted log on resume.
|
|
69
76
|
session.append('sandbox/mode', { mode: permissionMode });
|
|
70
77
|
session.append('approval/policy', { policy: 'never' });
|
|
78
|
+
const native = await nativePolicy?.admit(agent, root);
|
|
79
|
+
if (!root) return;
|
|
71
80
|
const policy = ctx.sandboxPolicy.resolve({ session });
|
|
72
81
|
const receipt = {
|
|
73
82
|
protocol: 1,
|
|
74
83
|
requestId, sessionId, nonce,
|
|
84
|
+
...(native ? { native } : {}),
|
|
75
85
|
...(workspaceCwd === undefined ? {} : { workspaceCwd: session.header.cwd }),
|
|
76
86
|
cwd: policy.workspaceRoot,
|
|
77
87
|
permissionMode: policy.mode,
|
package/dist/prewarm.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { open, realpath, stat, link, unlink, readdir } from 'node:fs/promises';
|
|
3
3
|
import { join, isAbsolute } from 'node:path';
|
|
4
4
|
import { BridgeError, MAX_FRAME_BYTES, directory, nonempty, record } from './protocol.mjs';
|
|
5
|
+
import { validateNative, nativeDigest } from './native.mjs';
|
|
5
6
|
|
|
6
7
|
export const PREWARM_MS = 60000;
|
|
7
8
|
const MAX_RECEIPT_BYTES = 8192;
|
|
@@ -9,7 +10,7 @@ const TRUNCATED_DIAGNOSTIC = ' [truncated]';
|
|
|
9
10
|
const BINDING_FIELDS = ['cwd', 'workspaceCwd', 'provider', 'model', 'reasoningEffort', 'permissionMode'];
|
|
10
11
|
|
|
11
12
|
export async function validatePrepare(value) {
|
|
12
|
-
const keys = new Set(['type', 'protocol', 'warmId', 'binding', 'receiptDirectory', 'ttlMs', ...BINDING_FIELDS]);
|
|
13
|
+
const keys = new Set(['type', 'protocol', 'warmId', 'binding', 'receiptDirectory', 'ttlMs', 'native', ...BINDING_FIELDS]);
|
|
13
14
|
if (!record(value) || value.type !== 'prepare' || value.protocol !== 1 ||
|
|
14
15
|
Object.keys(value).some(key => !keys.has(key)) || typeof value.warmId !== 'string' || typeof value.binding !== 'string' ||
|
|
15
16
|
!/^[a-zA-Z0-9_-]{8,128}$/.test(value.warmId) ||
|
|
@@ -19,6 +20,7 @@ export async function validatePrepare(value) {
|
|
|
19
20
|
}
|
|
20
21
|
const request = { ...value, cwd: await directory(value.cwd, 'cwd'),
|
|
21
22
|
...(value.workspaceCwd === undefined ? {} : { workspaceCwd: await directory(value.workspaceCwd, 'workspaceCwd') }) };
|
|
23
|
+
validateNative(value.native);
|
|
22
24
|
for (const key of ['provider', 'model', 'reasoningEffort']) nonempty(value[key], key);
|
|
23
25
|
if (request.workspaceCwd && request.workspaceCwd !== request.cwd && request.permissionMode === 'workspace-write') {
|
|
24
26
|
throw new BridgeError('INVALID_REQUEST', 'separate execution roots require an explicitly compatible permission mode');
|
|
@@ -38,6 +40,7 @@ export function matchWarmStart(prepared, request) {
|
|
|
38
40
|
if (BINDING_FIELDS.some(key => (prepared[key] ?? '') !== (request[key] ?? ''))) {
|
|
39
41
|
throw new BridgeError('INVALID_REQUEST', 'formal request differs from the initialized SDK binding');
|
|
40
42
|
}
|
|
43
|
+
if (nativeDigest(prepared.native ?? null) !== nativeDigest(request.native ?? null)) throw new BridgeError('INVALID_REQUEST', 'native composition changed after prewarm initialization');
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
export async function writeWarmReceipt(prepared, settled, error) {
|
package/dist/protocol.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
2
|
import { open, realpath, stat } from 'node:fs/promises';
|
|
3
3
|
import { dirname, isAbsolute } from 'node:path';
|
|
4
|
+
import { validateNative, validateManaged } from './native.mjs';
|
|
4
5
|
|
|
5
6
|
export const PROTOCOL = 1;
|
|
6
7
|
export const SESSION_PROTOCOL = 'dsh-sdk/session-resume/worker-v2';
|
|
@@ -55,7 +56,7 @@ export async function validateStart(value) {
|
|
|
55
56
|
throw new BridgeError('INVALID_REQUEST', 'expected one protocol 1 or 2 start request');
|
|
56
57
|
}
|
|
57
58
|
const allowed = new Set(['type', 'protocol', 'requestId', 'cwd', 'provider', 'model',
|
|
58
|
-
'reasoningEffort', 'permissionMode', 'prompt', 'maxTokens', 'timeoutMs', 'workspaceCwd', 'sessionTitle']);
|
|
59
|
+
'reasoningEffort', 'permissionMode', 'prompt', 'maxTokens', 'timeoutMs', 'workspaceCwd', 'sessionTitle', 'native', 'managed']);
|
|
59
60
|
if (value.protocol === 2) {
|
|
60
61
|
allowed.add('sessionId');
|
|
61
62
|
allowed.add('persistent');
|
|
@@ -66,6 +67,12 @@ export async function validateStart(value) {
|
|
|
66
67
|
if (!allowed.has(key)) throw new BridgeError('INVALID_REQUEST', `unsupported start field: ${key}`);
|
|
67
68
|
}
|
|
68
69
|
const requestId = nonempty(value.requestId, 'requestId', 128);
|
|
70
|
+
validateNative(value.native);
|
|
71
|
+
validateManaged(value.managed, requestId);
|
|
72
|
+
if (value.managed !== undefined && value.native === undefined) throw new BridgeError('INVALID_REQUEST', 'managed identity requires native options');
|
|
73
|
+
if (value.native?.lcu?.enabled && (!value.managed || value.permissionMode !== 'danger-full-access')) {
|
|
74
|
+
throw new BridgeError('INVALID_REQUEST', 'LCU requires managed identity and explicitly selected full access');
|
|
75
|
+
}
|
|
69
76
|
const provider = nonempty(value.provider, 'provider');
|
|
70
77
|
const model = nonempty(value.model, 'model');
|
|
71
78
|
const prompt = nonempty(value.prompt, 'prompt', MAX_FRAME_BYTES);
|
package/dist/worker.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { runWorker } from './bridge.mjs';
|
|
3
3
|
import { prewarmInput } from './prewarm.mjs';
|
|
4
4
|
import { BridgeError, MAX_FRAME_BYTES, normalizedError } from './protocol.mjs';
|
|
5
|
+
import { nativeDoctor } from './native-doctor.mjs';
|
|
5
6
|
|
|
6
7
|
const controller = new AbortController();
|
|
7
8
|
process.once('SIGTERM', () => controller.abort());
|
|
@@ -33,16 +34,22 @@ async function readInput() {
|
|
|
33
34
|
const emit = value => process.stdout.write(JSON.stringify(value) + '\n');
|
|
34
35
|
let warmInput;
|
|
35
36
|
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');
|
|
37
|
+
if (process.argv.slice(2).some(arg => !['--doctor', '--native-doctor', '--prewarm'].includes(arg)) || process.argv.length > 3) {
|
|
38
|
+
throw new BridgeError('INVALID_REQUEST', 'worker accepts only optional --doctor, --native-doctor or --prewarm');
|
|
39
|
+
}
|
|
40
|
+
if (process.argv.includes('--native-doctor')) {
|
|
41
|
+
const result = await nativeDoctor();
|
|
42
|
+
emit(result);
|
|
43
|
+
process.exitCode = result.available ? 0 : 1;
|
|
44
|
+
} else {
|
|
45
|
+
warmInput = process.argv.includes('--prewarm') ? prewarmInput(controller) : undefined;
|
|
46
|
+
const request = warmInput ? await warmInput.admission : await readInput();
|
|
47
|
+
const result = await runWorker(request, {
|
|
48
|
+
emit, signal: controller.signal, doctor: process.argv.includes('--doctor'),
|
|
49
|
+
stderr: chunk => process.stderr.write(chunk), prewarm: warmInput,
|
|
50
|
+
});
|
|
51
|
+
process.exitCode = !result || ['ok', 'closed'].includes(result.status) ? 0 : result.status === 'cancelled' ? 143 : 1;
|
|
38
52
|
}
|
|
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
53
|
} catch (error) {
|
|
47
54
|
emit({ protocol: 1, requestId: 'invalid-request', type: 'result', status: 'error', settled: true,
|
|
48
55
|
text: '', usage: null, costUsd: null, error: normalizedError(error) });
|
package/docs/README.md
CHANGED
|
@@ -16,6 +16,7 @@ guides are maintained in English and Simplified Chinese.
|
|
|
16
16
|
| Use the canvas, stage controls, sessions, and run history | [Workbench](en/flow-console.md) | [流程工作台](zh-CN/flow-console.md) |
|
|
17
17
|
| Resolve configuration, profiles, permissions, and scheduling | [Configuration](en/configuration.md) | [配置参考](zh-CN/configuration.md) |
|
|
18
18
|
| Understand the default models and reasoning settings | [Model policy](en/model-policy.md) | [模型策略](zh-CN/model-policy.md) |
|
|
19
|
+
| Use native Agents, Teams, Swarm, Tower and LCU | [Native capabilities](en/native-capabilities.md) | [原生能力](zh-CN/native-capabilities.md) |
|
|
19
20
|
| Enable the optional Claude machine backend | [Claude Code Pro](en/claude-code-pro.md) | [Claude Code Pro](zh-CN/claude-code-pro.md) |
|
|
20
21
|
| Resume roles and reconcile interrupted calls | [Recovery and context quality](en/session-recovery.md) | [恢复与上下文质量](zh-CN/session-recovery.md) |
|
|
21
22
|
| Compare session policies with fixed tasks and evidence | [Session evaluation](en/session-evaluation.md) | [会话评估](zh-CN/session-evaluation.md) |
|
|
@@ -54,6 +55,14 @@ The [P5 implementation plan](plans/2026-11-13-p5-ready-queue-prewarm.md) (Chines
|
|
|
54
55
|
records optional scheduling and runtime preparation; live performance and any
|
|
55
56
|
default adoption require separate evidence.
|
|
56
57
|
|
|
58
|
+
The [native harness, PTC and LCU implementation design](plans/2026-11-13-native-harness-capabilities.en.md)
|
|
59
|
+
([中文](plans/2026-11-13-native-harness-capabilities.md)), revised on 2026-11-14,
|
|
60
|
+
describes the six-repository implementation: shared SDK/PTC composition, native
|
|
61
|
+
Agents/Teams/Swarm/Workflows, Tower candidates, GUI leases, media, accounting and
|
|
62
|
+
recovery. Use the [native guide](en/native-capabilities.md) for configuration and
|
|
63
|
+
the [validation record](validation/2026-11-14-native-harness-implementation.md)
|
|
64
|
+
for the tested source and installation boundaries.
|
|
65
|
+
|
|
57
66
|
| Collection | Contents |
|
|
58
67
|
| --- | --- |
|
|
59
68
|
| [Plans](plans/README.md) | Dated design proposals and implementation decisions |
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
| 使用画布、阶段操作、会话和运行记录 | [流程工作台](zh-CN/flow-console.md) | [Workbench](en/flow-console.md) |
|
|
15
15
|
| 配置优先级、profiles、权限和调度 | [配置参考](zh-CN/configuration.md) | [Configuration](en/configuration.md) |
|
|
16
16
|
| 默认模型与推理等级的依据 | [模型策略](zh-CN/model-policy.md) | [Model policy](en/model-policy.md) |
|
|
17
|
+
| 使用原生 Agent、Team、Swarm、Tower 与 LCU | [原生能力](zh-CN/native-capabilities.md) | [Native capabilities](en/native-capabilities.md) |
|
|
17
18
|
| 接入可选 Claude machine 后端 | [Claude Code Pro](zh-CN/claude-code-pro.md) | [Claude Code Pro](en/claude-code-pro.md) |
|
|
18
19
|
| 角色续接、调用中断与对账 | [恢复与上下文质量](zh-CN/session-recovery.md) | [Recovery and context quality](en/session-recovery.md) |
|
|
19
20
|
| 使用固定任务和证据比较会话策略 | [会话评估](zh-CN/session-evaluation.md) | [Session evaluation](en/session-evaluation.md) |
|
|
@@ -38,6 +39,8 @@
|
|
|
38
39
|
|
|
39
40
|
[反馈驱动工作流与 JEV 方案](plans/2026-11-12-feedback-driven-workflows.md)(中文,历史提案)系统讨论 Auto/Quick/Full、故障调查、短反馈闭环、模型策略和有界语义决策;当前支持的行为见[调查指南](zh-CN/investigation.md)与 [JEV 指南](zh-CN/jev.md)。[P5 实施计划](plans/2026-11-13-p5-ready-queue-prewarm.md)记录可选调度与运行时准备,真实性能和默认采用策略另需证据。
|
|
40
41
|
|
|
42
|
+
[原生 Harness、PTC 与 LCU 完整设计](plans/2026-11-13-native-harness-capabilities.md)([English](plans/2026-11-13-native-harness-capabilities.en.md))于 2026-11-14 修订为六仓库实现设计,涵盖共享 SDK/PTC 组合、原生 Agent/Team/Swarm/Workflow、Tower 候选、GUI lease、媒体、用量和恢复。配置方法见[原生能力指南](zh-CN/native-capabilities.md),已测源码和安装边界见[验收记录](validation/2026-11-14-native-harness-implementation.md)。
|
|
43
|
+
|
|
41
44
|
| 分类 | 内容 |
|
|
42
45
|
| --- | --- |
|
|
43
46
|
| [方案](plans/README.md) | 按日期保存的设计提案与实施决策 |
|
package/docs/design.md
CHANGED
|
@@ -32,7 +32,7 @@ Flywheel 把明确需求变为一组有边界、有验收、有失败出口的 c
|
|
|
32
32
|
|
|
33
33
|
默认以 DSH Mac 桌面为入口,支持 Kimi、ChatGPT 订阅和 DSH 已登录并充值的 DeepSeek 账号。DeepSeek 只使用 Flash,默认 provider 为 `deepseek-account`;`deepseek-official` API Key 仍是显式可选配置,不自动回退。Kimi 通过原生 CLI 调用,不使用 ACP。GPT 可以在 DSH 原生 agent 中执行,也可以通过 Codex Pro 插件的 machine API 使用完整 Codex harness;这两个后端的工具、权限和会话语义不同。
|
|
34
34
|
|
|
35
|
-
具名模型 profiles、provider 与账号渠道的独立配置、有效配置查看和无推理安装预检已具备。`codex-pro` harness 直接消费 `dsh-subagent-codex-pro@0.
|
|
35
|
+
具名模型 profiles、provider 与账号渠道的独立配置、有效配置查看和无推理安装预检已具备。`codex-pro` harness 直接消费 `dsh-subagent-codex-pro@0.10.0` 公开 machine API,作为默认 hard 路由;原 `codex` CLI driver 仅保留为用户明确选择的兼容路径,不作自动后备。统一准备启动入口、Auto/Quick/Full 和 DSH 会话任务关联减少已明确小任务的重复规划与终验。有界并行按冻结依赖和写入范围调度;桌面控制台提供受管阶段操作、配置编辑、会话接管和明确授权的远端交付。原生能力通过 native-profiles 接入各 loop,支持 DSH Team、Kimi Agent/Swarm/Tower、Codex agents 与 Claude Workflow;各后端实际边界见 §3.3。开放式自动模型选型仍不属于现有实现;显式允许集合内的执行升级见 §13。
|
|
36
36
|
|
|
37
37
|
## 2. 设计不变量
|
|
38
38
|
|
|
@@ -82,7 +82,7 @@ DSH 使用原仓库作为 Session header 的 cwd,再通过公开工作目录
|
|
|
82
82
|
|
|
83
83
|
### 3.2 Worker 与进程所有权
|
|
84
84
|
|
|
85
|
-
DSH worker 是独立进程,通过正式 SDK 的 stdio JSON-RPC 驱动原生会话。每次调用创建一个 SDK 进程,worker 和 SDK 继承 Go 管理的进程组,不另行 detached。SDK 自己创建的托管命令还须经 SDK
|
|
85
|
+
DSH worker 是独立进程,通过正式 SDK 的 stdio JSON-RPC 驱动原生会话。每次调用创建一个 SDK 进程,worker 和 SDK 继承 Go 管理的进程组,不另行 detached。SDK 自己创建的托管命令还须经 SDK 关闭协议排空;普通路径关闭保留 2 秒用于历史落盘与插件释放;启用 native 时为 15 秒,LCU 路径为 135 秒,Go 外层分别至少 20/145 秒。之后才进行有界 TERM/KILL 收尾。无法证明正常关闭就不能报告成功。可显式启用 SDK 内的 Team,但不能把 external subagent 名称当成原生 LLM provider。
|
|
86
86
|
|
|
87
87
|
可选 `scheduler.prewarm:true` 提前启动一次性 DSH Review 运行环境:在串行 packet 的 Execute 已确认收尾后,与 L1/适用 Quick 检查重叠执行 SDK 启动与 `initialize`。准备阶段不创建或恢复 Session,不发送 prompt,不使用占位推理。新协议以 `--prewarm` 明确选择,与原 v1/v2 的一次性请求保持兼容;最多一个待用 SDK,60 秒 TTL,stdin EOF 始终触发有界清理。正式 Review 仍先通过全部适用验证、预算和角色调用准入,持久化实际 invocation intent 后才能消费同一运行环境。
|
|
88
88
|
|
|
@@ -95,6 +95,18 @@ IPC lifeline,再由 guardian 派发 Claude CLI 并拥有其进程组;worker
|
|
|
95
95
|
收束原生工作。正常结果等待 native 与 guardian 退出。清理与任务结果独立:外层被
|
|
96
96
|
强杀导致回执丢失时,不能因为 guardian 清理了进程就推断任务成功或安全重放。
|
|
97
97
|
|
|
98
|
+
### 3.3 原生能力、任务树和独立任务
|
|
99
|
+
|
|
100
|
+
`native-profile` 在模型/阶段/路由绑定中引用版本化 `native-profiles`;能力跟随实际 harness,不改变 provider、账号或模型的既有绑定。DSH 复用 `dsh-preset-generator@0.4.0` 的 SDK global PTC overlay,明确不是挂载 named Agent Preset。首个 prompt 前实际执行 canonical JSON、handle、多图、TypeScript 和输出协议 probe;同版本号不代替行为验证。各后代在公开 Agent 生命周期中按真实 registry 和 lineage 准入。
|
|
101
|
+
|
|
102
|
+
每次 invocation 记录 requested/effective/observed、owned children、usage、completion 和 cleanup;默认值写入 effective,不改写原始 requested。native-event 只是观察,不充当 Pro durable lifecycle ACK。completion 在 root 结束、开始 cleanup 之前冻结;取消残留 child 不能倒推原任务已完成。SDK policy 的 bounded journal 按 requestId/nonce/rootSessionId/连续序号核对,最终 complete/events/bytes 不一致时保持失败与未知清理。
|
|
103
|
+
|
|
104
|
+
LCU 通过公开 managed API 挂载,root 与真实后代共享一个跨进程 `local-desktop` lease并顺序操作 GUI;每次操作核对 caller 与 epoch,未知 owner 不被自动接管。PTC tool 名为 `lcu_cua`,程序对象仍是 `cua`。取消必须区分 MCP 请求、turn cleanup 和 native 物理退出。图片从实际 AttachmentStore 读取并归档至 task 的 reports/native/invocation;capture 与 archive hash 分开,模型送达未验证时如实标注。
|
|
105
|
+
|
|
106
|
+
Kimi native 路径调用独立 worker,拥有 `kimi web` 的本地 server、registry、PID 与订阅,实际核对模型/effort/mode/permission后再派发。普通 CLI 路径保留。Agent/Swarm 使用角色账本;Tower 创建独立 `.git` 候选,只有 Execute 在完成和清理确认、source HEAD/bytes/modes/外部契约复核及 patch check 后导回。该副本不构成 OS 文件系统沙箱,Server 仅接受显式 full access;未知调用不重放。
|
|
107
|
+
|
|
108
|
+
Codex 0.10.0 使用 config/read核对原生 agents controls与当前child turn;Claude 0.5.0支持Agent定义/Workflow/Advisor,并区分init工具事实与无法验证的hard cap。Codex原生review不消费角色prompt,因此通过 `flywheel native codex-review`独立入口;Claude `claude-review`、`claude-team`、`claude-recover`和`claude-control`保留远端job/tmux所有权与恢复回执。它们不写Flywheel state、不自动产生Review或Judge verdict,远端本地超时不证明云任务已结束。使用与协议见[原生能力指南](zh-CN/native-capabilities.md)及[完整设计](plans/2026-11-13-native-harness-capabilities.md)。
|
|
109
|
+
|
|
98
110
|
## 4. 执行闭环
|
|
99
111
|
|
|
100
112
|
### 4.0 Auto、Quick 与 Full
|
|
@@ -133,8 +145,10 @@ Plan 不得直接修改实现、任务目录外的冻结 oracle,或 `flywheel.
|
|
|
133
145
|
规划检查点提交前后核对候选内容,并检查新 HEAD 的路径范围与已提交契约。Git hook 修改已加载契约、仅修改 index、或混入实现变更时拒绝继续同步;保留工作区,要求重新加载并验证。
|
|
134
146
|
|
|
135
147
|
scoped 下 Plan 按任务/里程碑保存自己的历史,显式 `resident-session:false` 继续关闭
|
|
136
|
-
Plan 复用;省略该字段不等于关闭。fresh
|
|
137
|
-
|
|
148
|
+
Plan 复用;省略该字段不等于关闭。fresh 仅在未配置 `native-profile` 的旧 Kimi CLI
|
|
149
|
+
路径保留旧 Plan resident 行为,但不把它记录为通用作用域可复用句柄。原生 Agent/Swarm
|
|
150
|
+
将全局 `fresh` 下的显式 `resident-session:true` 意图映射为 scoped 角色账本,显式 `sessions.roles.plan:"fresh"`
|
|
151
|
+
仍优先关闭复用;不导入旧 CLI Session,Tower 始终 fresh。实际绑定、工作目录、权限和运行配置不匹配时不续用旧
|
|
138
152
|
历史;未知调用先阻断,不能以冷启动覆盖。会话不是状态真相。
|
|
139
153
|
|
|
140
154
|
### 4.1.1 可信 Plan 检查点
|
|
@@ -289,8 +303,9 @@ Node 发布的预热终态回执与 Go 读取端共用 8192 字节上限,按
|
|
|
289
303
|
|
|
290
304
|
调用安全记录与历史复用分别控制。Codex Pro、Claude Code Pro、DSH、Kimi 的主任务调用在 fresh/scoped
|
|
291
305
|
下都先保存 intent 再派发。`last.persistent` 表示是否参与通用角色历史;只有它为 true
|
|
292
|
-
的可信结果才保存可复用句柄。fresh
|
|
293
|
-
Plan resident
|
|
306
|
+
的可信结果才保存可复用句柄。fresh 的原始后端会话标识仍可保留在回执中;仅未配置
|
|
307
|
+
`native-profile` 的旧 Kimi CLI Plan 保留 resident 行为,不冒充跨里程碑的通用会话。
|
|
308
|
+
其他适配器、mock 与 Intake/Garden
|
|
294
309
|
保留原路径。模式或后端变化不能绕过已保存的未知调用。
|
|
295
310
|
|
|
296
311
|
每个作用域有一个 `.sessions` 原子回执槽,使用私有目录/文件、限定大小与禁止符号
|
|
@@ -452,9 +467,9 @@ SIGKILL 后的进程组探测必须确认 `ESRCH` 才算清理成功。探测的
|
|
|
452
467
|
|
|
453
468
|
### 7.1 Codex Pro 接入契约
|
|
454
469
|
|
|
455
|
-
`codex-pro` 不是 DSH LLM provider ID。使用 `harness: "codex-pro"`、显式模型、thinking 和账号 channel 选择该路径;不接受被忽略的 provider 或旧 resident-session 选项。主任务 fresh/scoped 均使用外层 worker v3,分别显式选择 ephemeral/persistent;低层兼容 v1/v2。内部仍为插件 0.
|
|
470
|
+
`codex-pro` 不是 DSH LLM provider ID。使用 `harness: "codex-pro"`、显式模型、thinking 和账号 channel 选择该路径;不接受被忽略的 provider 或旧 resident-session 选项。主任务 fresh/scoped 均使用外层 worker v3,分别显式选择 ephemeral/persistent;低层兼容 v1/v2。内部仍为插件 0.10.0 的 machine request/receipt protocol 1,另有 lifecycle version 1。权限由显式 `permissions.mode` 决定,不能从桌面全访问设置隐式继承。
|
|
456
471
|
|
|
457
|
-
worker 精确依赖插件 `0.
|
|
472
|
+
worker 精确依赖插件 `0.10.0` 的公开 `runMachine` API,使用其协议 1 请求、进度回调和终态回执。模型/cwd/sandbox/approval 回执不符合请求时失败;接受 `workspace-write`、`read-only` 或明确选择的 `danger-full-access`,审批策略为 `never`。完全权限在公开 machine API 边界映射为 `dangerously-bypass-approvals-and-sandbox`,回执中的 Codex sandbox 为 `danger-full-access`、policy 类型为 `dangerFullAccess`,不能要求不存在的受限模式字段。插件仅能证明 effort 已被配置接受时,保留 `accepted` 与 `effectiveEffort: null`,不得改写成已证明实际推理强度。
|
|
458
473
|
|
|
459
474
|
权限检查分两个时点:插件检查实际配置,主任务 v3 的 thread-ready 再把完整 sandboxPolicy 送交 Go,在 ACK 前完成完整检查,阻止不符合策略的 turn/start。终态还须重新核对。受限模式不得有工作区外额外 writable roots 或 networkAccess;完全权限明确记录 dangerFullAccess,不宣称 OS 隔离。旧 v1/v2 以及未请求 lifecycle 的辅助调用仍只有终态完整策略检查,不能外推为预执行 guard。doctor 报告静态接口能力,不证明具体账号、历史或实际沙箱可用。
|
|
460
475
|
|
|
@@ -470,7 +485,7 @@ worker 精确依赖插件 `0.9.0` 的公开 `runMachine` API,使用其协议 1
|
|
|
470
485
|
`thinking` 缺省 high,支持插件允许的 low/medium/high/xhigh/max;CLI、模型与账号仍
|
|
471
486
|
可能拒绝某档位,不能把传参成功宣称为实际推理强度已验证。
|
|
472
487
|
|
|
473
|
-
Flywheel 分发 worker,把插件 0.
|
|
488
|
+
Flywheel 分发 worker,把插件 0.5.0 声明为精确的 optional peer。未选择它时不用安装
|
|
474
489
|
该 peer;选中时 `doctor` 与 worker 核验包名/版本/公开导出,并检查本地 `claude`
|
|
475
490
|
命令。桌面安装目录不一定是 worker 的 Node 解析树;必须安装在该 worker 能解析的
|
|
476
491
|
同一依赖树或全局 prefix。依赖缺失、接口不符和无效请求在 machine 调用前证明未派发;
|
|
@@ -508,6 +523,8 @@ unknown。身份错配的结果可以保留原始回执作审计,但其 usage/
|
|
|
508
523
|
预算限制新的派发;消费已完成的可信回执不额外发请求或计费,不应因预算已满丢掉该
|
|
509
524
|
结果。回执消费后需要的新阶段或重试仍受预算门禁控制。
|
|
510
525
|
|
|
526
|
+
原生树的用量只有在exclusive归属、去重和本次调用完整性都成立时汇总。DSH本地子Session committed events与Codex子thread delta可在完整时加到root;Claude task snapshots不可直接相加;Kimi从实际step统计,compaction、恢复旧child、Tower或缺失计量保持unknown。scheduler的provider配额限制根调用,不是各harness内部的统一whole-tree硬上限;Claude observed guard明确hard=false,Kimi显式请求不可执行的hard cap时拒绝该组合。hard budget保留对未知调用的阻断,不用root-only数字伪装完整总量。
|
|
527
|
+
|
|
511
528
|
### 8.1 运行索引与指标比较
|
|
512
529
|
|
|
513
530
|
运行索引在全局配置旁的 `runs/` 为每次明确的 CLI/控制接口启动独立保存,可用绝对 `FLYWHEEL_RUNS_DIR` 覆盖。索引记录 run ID、契约/配置摘要、启动和结束回执,以及可用的 DSH 会话 ID;只读取最近 100 条登记,不扫描项目或磁盘,也不作为进程仍存活或任务已完成的证明。
|
|
@@ -628,6 +645,7 @@ Go 的 `make quality-gate` 执行 vet、gofmt 和 **逐函数 100% statement cov
|
|
|
628
645
|
| P5 就绪队列与一次性 DSH 预热 | [实施计划](plans/2026-11-13-p5-ready-queue-prewarm.md) | [工程验证与收益边界](validation/2026-11-13-p5-ready-queue-prewarm.md) |
|
|
629
646
|
| 历史影子评估(已退役) | [原 JEV 方案](plans/2026-11-10-jev-shadow.md) | [历史验收](validation/2026-11-10-jev-shadow.md) |
|
|
630
647
|
| 0.30.2 发行与安装 | [发行流程](zh-CN/releasing.md) | [发行记录](validation/2026-11-11-release-0.30.2.md) |
|
|
648
|
+
| 0.33.1 桌面读取与发行 | [发行流程](zh-CN/releasing.md) | [发行与验收记录](validation/2026-11-14-release-0.33.1.md) |
|
|
631
649
|
| 0.33.0 单包分发 | [发行流程](zh-CN/releasing.md) | [工程验证](validation/2026-11-14-single-package.md) |
|
|
632
650
|
| 0.31.0 发行与安装 | [发行流程](zh-CN/releasing.md) | [发行记录](validation/2026-11-11-release-0.31.0.md) |
|
|
633
651
|
|
|
@@ -732,7 +750,9 @@ JEV 关闭对既有任务的影响在内容区说明,固定保存栏用简短
|
|
|
732
750
|
|
|
733
751
|
原生 UI 接入和独立 worker 是不同边界。SDK worker 的临时 `--patch` 仅用于该子进程,不修改桌面 profile,也不卸载桌面的插件、skills、工具或 UI。桌面已加载插件、已登录账号、已选模型,不代表 SDK profile 或其他 harness 已配置相同能力;控制面显示引擎实际配置,不静默继承或迁移桌面设置。
|
|
734
752
|
|
|
735
|
-
|
|
753
|
+
未选择 native-profile 的 worker 保留原有工具组合与关闭自主 goal/schedule、辅助标题和自动压缩的策略。显式 native-profile 可启用同一受管调用内的 agents、Team、Workflow、Web 与 LCU;能力必须实际准入,不能只改提示词。完整权限仍不自动启用任何原生能力。临时 overlay不修改用户的 desktop preset、profile 或提醒存储。
|
|
754
|
+
|
|
755
|
+
桌面 heavy/daily 与 SDK composition 复用生成器策略,但用户桌面历史不继承到独立 loop。工作台的模型与会话表可选择同 harness 的 native-profile,高级 JSON 编辑定义,配置查看和 doctor 展示实际引用。doctor的本地 manifest通过不等于运行 admission;native DSH不以legacy probe冒充实际PTC/LCU准入。完整设计与验证见[原生能力设计](plans/2026-11-13-native-harness-capabilities.md)和[验收记录](validation/2026-11-14-native-harness-implementation.md)。
|
|
736
756
|
|
|
737
757
|
### 12.3 受管阶段与远端交付
|
|
738
758
|
|
|
@@ -756,6 +776,8 @@ min-applicability、min-choice 和 allowed-data 限定请求。开启配置校
|
|
|
756
776
|
|
|
757
777
|
可选的顶层 profiles 定义 harness/model/channel/thinking/provider;loops、routes 和顶层 judge-panel 通过 profile 引用,加载时展开为完整实际绑定。当前 binding 显式出现的字段覆盖 profile,省略才继承;不支持 profile 继承。必需字符串的 null、空白、错拼字段及未知引用在模型启动前失败。thinking 显式空字符串表示当前 harness 的既有默认,不重新继承;resident-session 仍属于阶段,显式 false 保留。judge-panel:null 继续关闭第二意见。旧 inline 配置保留已有缺省语义。
|
|
758
778
|
|
|
779
|
+
可选 `native-profiles` 的字段信源为 `templates/native-profiles.schema.json`,每个定义明确harness与version 1 options。loops/routes/model profiles/judge-panel/execution-policy bindings的native-profile引用在LOAD时校验存在且harness一致。实际options进入session/history/prewarm和被引用execution-policy冻结身份,durable session configuration与state schema同时保存nativeProfile/nativeOptions;恢复按原请求核对。无关profile不改变已冻结身份,显式空字符串关闭模型profile继承。静态native配置与JEV唯一开关独立。
|
|
780
|
+
|
|
759
781
|
可选 `execution-policy` 的字段信源为 `templates/execution-policy.schema.json`,
|
|
760
782
|
版本 1 接受完整受管绑定、逐 route 允许集合和有界切换,缺省保留原绑定。packet
|
|
761
783
|
派发前冻结策略身份。质量升级只在可信重复 L1 空转、剩余尝试与任务预算允许时
|
|
@@ -796,7 +818,7 @@ skills 继续从同一 Go 二进制内嵌资源安装。每个受管快照保存
|
|
|
796
818
|
|
|
797
819
|
发布以精确 HEAD 标签与干净候选为依据。构建一次完整单包候选(candidate format 2),Linux/macOS 安装测试消费同一个 tarball;候选验证同时核对四个平台二进制、公开入口、MIT 许可和内置 adapter。已存在版本必须核对 integrity;先发 candidate 标签,精确版本从 registry 安装验证后才推进 latest,防止倒退。源码 PR CI 通过、npm pack 成功、registry 安装通过与用户 Mac 实机验收是不同证据,分别记录。
|
|
798
820
|
|
|
799
|
-
历史 0.28.2 使用 DeepSeek API Key 默认,账号默认修复从 0.28.3 开始提供;这些版本差异保留在 changelog 和发行记录,升级仍须显式迁移已有配置,不重写已发布的包。DSH 插件精确依赖上游 `dsh-subagent-codex-pro@0.
|
|
821
|
+
历史 0.28.2 使用 DeepSeek API Key 默认,账号默认修复从 0.28.3 开始提供;这些版本差异保留在 changelog 和发行记录,升级仍须显式迁移已有配置,不重写已发布的包。DSH 插件精确依赖上游 `dsh-subagent-codex-pro@0.10.0`;该正式发行包按 registry 的真实 npm tarball 与 integrity 锁定并 bundle 到完整包,来源、许可证和摘要见 [vendor 记录](https://github.com/ckanner/flywheel/blob/main/plugins/dsh/vendor/README.md)。generator 0.4.0的共享PTC composition同样固定并bundle;Claude 0.5.0与LCU 0.5.0维持可选peer。旧发行失败与重试记录保留在 [npm 验证记录](validation/2026-11-01-npm-installation.md);源码、候选、CI、registry 和 Mac 实机证据分别记录,不混为一个“测试通过”。
|
|
800
822
|
|
|
801
823
|
CI、候选构建、双平台验证和发布统一固定为 Node 24.22.1/npm 12.2.1,预检只需同版 Node。发行协议测试通过隔离的本地 registry 调用真实 `npm view`,覆盖 latest、integrity、缺失字段与错误响应。存在的字段必须返回字符串,兼容用单元素数组包裹;多结果拒绝,缺失 latest 单独处理,缺失 integrity 不能当成版本不存在。只有结构化 E404 表示版本不存在,取消、认证或网络错误仍阻止发布。npm 11/12 的 `pack --json` 兼容保持严格包身份检查。
|
|
802
824
|
|
|
@@ -8,7 +8,7 @@ The plugin runs the user's installed Claude Code CLI, with its native tool loop
|
|
|
8
8
|
and login environment. Go retains scheduling, role sessions, verification,
|
|
9
9
|
budgets, and delivery.
|
|
10
10
|
|
|
11
|
-
The adapter requires **`dsh-subagent-claude-code-pro@0.
|
|
11
|
+
The adapter requires **`dsh-subagent-claude-code-pro@0.5.0`**, machine protocol
|
|
12
12
|
1 and the public lifecycle/recovery interfaces. The older desktop-tool interface
|
|
13
13
|
and older machine contracts are not substitutes. No default stage or route
|
|
14
14
|
selects Claude. Package availability must be checked separately from source
|
|
@@ -22,7 +22,7 @@ Flywheel does not copy credentials or sign in on the user's behalf.
|
|
|
22
22
|
|
|
23
23
|
Flywheel ships `flywheel-claude-code-pro-worker` and declares the exact plugin as
|
|
24
24
|
an **optional peer**. Other backends work without it. To select Claude, install
|
|
25
|
-
`dsh-subagent-claude-code-pro@0.
|
|
25
|
+
`dsh-subagent-claude-code-pro@0.5.0` in the same Node dependency tree or global npm
|
|
26
26
|
prefix as Flywheel. Installing it only in a DSH desktop profile does not establish
|
|
27
27
|
that the independent worker can resolve it.
|
|
28
28
|
|