@ckanner/flywheel 0.33.0 → 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.
Files changed (151) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -0
  3. package/README.zh-CN.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +5 -1
  5. package/bin/flywheel-kimi-native-worker.js +5 -0
  6. package/bin/flywheel.js +8 -1
  7. package/bin/runtime.js +5 -1
  8. package/dist/bridge.mjs +89 -7
  9. package/dist/claude-code-pro-bridge.mjs +55 -6
  10. package/dist/client.js +107 -10
  11. package/dist/codex-pro-bridge.mjs +12 -5
  12. package/dist/control.mjs +1 -1
  13. package/dist/kimi-native-bridge.mjs +429 -0
  14. package/dist/kimi-native-runtime.mjs +233 -0
  15. package/dist/kimi-native-worker.mjs +77 -0
  16. package/dist/kimi-tower-candidate.mjs +227 -0
  17. package/dist/machine-worker.mjs +39 -5
  18. package/dist/managed-lcu.mjs +223 -0
  19. package/dist/native-doctor.mjs +36 -0
  20. package/dist/native-jobs.mjs +72 -0
  21. package/dist/native-policy.mjs +371 -0
  22. package/dist/native-resources.mjs +144 -0
  23. package/dist/native.mjs +251 -0
  24. package/dist/policy.mjs +13 -3
  25. package/dist/prewarm.mjs +4 -1
  26. package/dist/protocol.mjs +8 -1
  27. package/dist/worker.mjs +16 -9
  28. package/docs/README.md +9 -0
  29. package/docs/README.zh-CN.md +3 -0
  30. package/docs/design.md +34 -12
  31. package/docs/en/claude-code-pro.md +2 -2
  32. package/docs/en/configuration.md +17 -5
  33. package/docs/en/contributing.md +5 -3
  34. package/docs/en/flow-console.md +1 -1
  35. package/docs/en/getting-started.md +2 -2
  36. package/docs/en/model-policy.md +2 -2
  37. package/docs/en/native-capabilities.md +185 -0
  38. package/docs/en/releasing.md +2 -2
  39. package/docs/en/session-recovery.md +2 -2
  40. package/docs/plans/2026-11-13-native-harness-capabilities.en.md +258 -0
  41. package/docs/plans/2026-11-13-native-harness-capabilities.md +276 -0
  42. package/docs/plans/README.md +1 -0
  43. package/docs/validation/2026-11-14-desktop-native-configuration.md +128 -0
  44. package/docs/validation/2026-11-14-native-harness-implementation.md +215 -0
  45. package/docs/validation/2026-11-14-published-companions.md +15 -0
  46. package/docs/validation/2026-11-14-release-0.33.0.md +49 -0
  47. package/docs/validation/2026-11-14-release-0.33.1.md +78 -0
  48. package/docs/validation/README.md +4 -0
  49. package/docs/validation/evidence/2026-11-14-native-harness-candidate.json +255 -0
  50. package/docs/validation/evidence/2026-11-14-native-harness-sessions.json +6009 -0
  51. package/docs/validation/evidence/2026-11-14-native-harness-tower-candidate.json +192 -0
  52. package/docs/zh-CN/claude-code-pro.md +2 -2
  53. package/docs/zh-CN/configuration.md +21 -8
  54. package/docs/zh-CN/contributing.md +5 -3
  55. package/docs/zh-CN/flow-console.md +1 -1
  56. package/docs/zh-CN/getting-started.md +2 -2
  57. package/docs/zh-CN/model-policy.md +2 -2
  58. package/docs/zh-CN/native-capabilities.md +184 -0
  59. package/docs/zh-CN/releasing.md +2 -2
  60. package/docs/zh-CN/session-recovery.md +1 -1
  61. package/native/darwin-amd64/flywheel +0 -0
  62. package/native/darwin-arm64/flywheel +0 -0
  63. package/native/linux-amd64/flywheel +0 -0
  64. package/native/linux-arm64/flywheel +0 -0
  65. package/native/manifest.json +9 -9
  66. package/node_modules/dsh-preset-generator/CHANGELOG.md +77 -0
  67. package/node_modules/dsh-preset-generator/LICENSE +21 -0
  68. package/node_modules/dsh-preset-generator/README.md +149 -0
  69. package/node_modules/dsh-preset-generator/docs/README.zh.md +95 -0
  70. package/node_modules/dsh-preset-generator/package.json +43 -0
  71. package/node_modules/dsh-preset-generator/src/composition.mjs +150 -0
  72. package/node_modules/dsh-preset-generator/src/gen-presets.mjs +367 -0
  73. package/node_modules/dsh-preset-generator/src/policy.mjs +17 -0
  74. package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +16 -1
  75. package/node_modules/dsh-subagent-codex-pro/README.md +57 -4
  76. package/node_modules/dsh-subagent-codex-pro/assets/native-client.js +118 -0
  77. package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +6 -0
  78. package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +49 -2
  79. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +1 -1
  80. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +1 -1
  81. package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +1 -1
  82. package/node_modules/dsh-subagent-codex-pro/docs/machine.md +2 -2
  83. package/node_modules/dsh-subagent-codex-pro/docs/native-capabilities.md +254 -0
  84. package/node_modules/dsh-subagent-codex-pro/docs/native-capabilities.zh.md +180 -0
  85. package/node_modules/dsh-subagent-codex-pro/lib/config.js +56 -0
  86. package/node_modules/dsh-subagent-codex-pro/lib/config.js.map +1 -0
  87. package/node_modules/dsh-subagent-codex-pro/lib/desktop-native.js +109 -0
  88. package/node_modules/dsh-subagent-codex-pro/lib/desktop-native.js.map +1 -0
  89. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +3 -4
  90. package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -1
  91. package/node_modules/dsh-subagent-codex-pro/lib/index.js +64 -15
  92. package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -1
  93. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +6 -2
  94. package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -1
  95. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +2 -2
  96. package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -1
  97. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +5 -2
  98. package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -1
  99. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +11 -0
  100. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -1
  101. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +1 -0
  102. package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -1
  103. package/node_modules/dsh-subagent-codex-pro/lib/machine.js +20 -3
  104. package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -1
  105. package/node_modules/dsh-subagent-codex-pro/lib/native.js +297 -0
  106. package/node_modules/dsh-subagent-codex-pro/lib/native.js.map +1 -0
  107. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +11 -1
  108. package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -1
  109. package/node_modules/dsh-subagent-codex-pro/lib/run.js +19 -1
  110. package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -1
  111. package/node_modules/dsh-subagent-codex-pro/lib/tool.js +123 -57
  112. package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -1
  113. package/node_modules/dsh-subagent-codex-pro/lib/types/config.d.ts +31 -0
  114. package/node_modules/dsh-subagent-codex-pro/lib/types/config.d.ts.map +1 -0
  115. package/node_modules/dsh-subagent-codex-pro/lib/types/desktop-native.d.ts +30 -0
  116. package/node_modules/dsh-subagent-codex-pro/lib/types/desktop-native.d.ts.map +1 -0
  117. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +7 -5
  118. package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -1
  119. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +31 -0
  120. package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -1
  121. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +2 -0
  122. package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -1
  123. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -1
  124. package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -1
  125. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +7 -0
  126. package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -1
  127. package/node_modules/dsh-subagent-codex-pro/lib/types/native.d.ts +120 -0
  128. package/node_modules/dsh-subagent-codex-pro/lib/types/native.d.ts.map +1 -0
  129. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +11 -0
  130. package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -1
  131. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +11 -0
  132. package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -1
  133. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +7 -1
  134. package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -1
  135. package/node_modules/dsh-subagent-codex-pro/lib/wire.js +34 -6
  136. package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -1
  137. package/node_modules/dsh-subagent-codex-pro/package.json +28 -4
  138. package/npm/install.js +2 -2
  139. package/package.json +25 -10
  140. package/plugins/dsh/KIMI_NATIVE_PROTOCOL.en.md +76 -0
  141. package/plugins/dsh/KIMI_NATIVE_PROTOCOL.md +76 -0
  142. package/plugins/dsh/PROTOCOL.md +151 -17
  143. package/plugins/dsh/README.md +15 -12
  144. package/plugins/dsh/README.zh-CN.md +8 -8
  145. package/plugins/dsh/THIRD_PARTY_NOTICES.md +5 -1
  146. package/plugins/dsh/vendor/README.md +40 -11
  147. package/plugins/dsh/vendor/chatgpt.json +21 -0
  148. package/plugins/dsh/vendor/claude-code-pro.json +21 -0
  149. package/plugins/dsh/vendor/codex-pro.json +13 -8
  150. package/plugins/dsh/vendor/lcu.json +21 -0
  151. package/plugins/dsh/vendor/preset-generator.json +16 -0
@@ -0,0 +1,429 @@
1
+ import { isAbsolute } from 'node:path';
2
+ import { realpath } from 'node:fs/promises';
3
+ import { OwnedKimiRuntime, KimiSocket, inspectKimiSchemas, digest, error, pause, checkAbort } from './kimi-native-runtime.mjs';
4
+ import { createTowerCandidate, freezeTowerCandidate, transferTowerCandidate, towerCandidateReceipt, towerPrompt } from './kimi-tower-candidate.mjs';
5
+
6
+ const object = value => value && typeof value === 'object' && !Array.isArray(value);
7
+ const identifier = value => typeof value === 'string' && value.trim() && value.length <= 512 && !/[\0\r\n]/.test(value);
8
+ const encoded = value => encodeURIComponent(value);
9
+
10
+ export function validateKimiNativeStart(input) {
11
+ if (!object(input) || input.type !== 'start' || input.protocol !== 1 || !identifier(input.requestId)
12
+ || Object.keys(input).some(key => !['type', 'protocol', 'requestId', 'cwd', 'workspaceCwd', 'model', 'reasoningEffort', 'permissionMode', 'prompt', 'timeoutMs', 'sessionId', 'persistent', 'native', 'managed'].includes(key))
13
+ || !identifier(input.cwd) || !isAbsolute(input.cwd) || !identifier(input.model) || !identifier(input.reasoningEffort)
14
+ || (input.workspaceCwd !== undefined && (!identifier(input.workspaceCwd) || !isAbsolute(input.workspaceCwd)))
15
+ || (input.persistent !== undefined && typeof input.persistent !== 'boolean')
16
+ || input.permissionMode !== 'danger-full-access' || typeof input.prompt !== 'string' || !input.prompt.trim()
17
+ || Buffer.byteLength(JSON.stringify(input)) > 1024 * 1024 || !Number.isSafeInteger(input.timeoutMs) || input.timeoutMs <= 0
18
+ || (input.sessionId !== undefined && (!identifier(input.sessionId) || input.persistent !== true))) {
19
+ throw error('INVALID_REQUEST', 'Expected a bounded protocol 1 Kimi start request with explicit full-access permission');
20
+ }
21
+ const native = input.native, managed = input.managed;
22
+ if (!object(native) || native.version !== 1 || !['agent', 'swarm', 'tower'].includes(native.mode)
23
+ || Object.keys(native).some(key => !['version', 'mode', 'agents', 'agentProfile', 'tower'].includes(key))
24
+ || (native.agentProfile !== undefined && !identifier(native.agentProfile))
25
+ || (native.agents !== undefined && (!object(native.agents) || typeof native.agents.enabled !== 'boolean'
26
+ || Object.keys(native.agents).some(key => !['enabled', 'maxConcurrent'].includes(key))))
27
+ || (native.tower !== undefined && (!object(native.tower) || native.mode !== 'tower'
28
+ || Object.keys(native.tower).some(key => key !== 'branch') || (native.tower.branch !== undefined && !identifier(native.tower.branch))))) {
29
+ throw error('INVALID_NATIVE_PROFILE', 'The requested Kimi native profile is invalid or unsupported');
30
+ }
31
+ if (native.agents?.maxConcurrent !== undefined) throw error('UNSUPPORTED_HARD_CAP', 'The current Kimi API has no enforceable whole-tree agents.maxConcurrent; native batch/Tower limits are not that guarantee');
32
+ if (native.agents?.enabled === false && native.mode !== 'agent') throw error('INVALID_NATIVE_PROFILE', 'Swarm and Tower require native agents to be enabled');
33
+ if (native.mode === 'tower' && input.sessionId !== undefined) throw error('TOWER_RESUME_UNSUPPORTED', 'Tower uses a fresh isolated repository and cannot resume an earlier source candidate');
34
+ if (!object(managed) || managed.version !== 1 || typeof managed.runId !== 'string' || managed.runId.length > 512 || /[\0\r\n]/.test(managed.runId)
35
+ || ['invocationId', 'role', 'scope', 'channel', 'taskDir'].some(key => !identifier(managed[key])) || managed.invocationId !== input.requestId
36
+ || Object.keys(managed).some(key => !['version', 'runId', 'invocationId', 'role', 'scope', 'candidateId', 'channel', 'taskDir'].includes(key))
37
+ || !isAbsolute(managed.taskDir) || (managed.candidateId !== undefined && !identifier(managed.candidateId))) {
38
+ throw error('INVALID_REQUEST', 'Kimi native execution requires an orchestrator-owned role and task binding');
39
+ }
40
+ return input;
41
+ }
42
+
43
+ /** A replacement-based projection; HTTP acceptance and live turns may race. */
44
+ export class KimiConversation {
45
+ constructor() { this.agents = new Map(); this.turns = new Map(); this.tasks = new Map(); this.snapshots = new Set(); this.pending = new Map(); this.lastChange = Date.now(); this.bytes = 0; this.unmeteredWork = false; }
46
+ agentTurns(id) { if (!this.turns.has(id)) this.turns.set(id, new Map()); return this.turns.get(id); }
47
+ mergeTurn(agentId, turn) {
48
+ if (!object(turn) || !identifier(turn.turn_id) || !Number.isSafeInteger(turn.ordinal) || turn.ordinal < 0) throw error('WS_PROTOCOL', 'Kimi turn identity or ordinal is invalid');
49
+ const turns = this.agentTurns(agentId), prior = turns.get(turn.turn_id);
50
+ turns.set(turn.turn_id, { ...prior, ...turn, items: turn.items ?? prior?.items ?? [] });
51
+ if (turns.size > 4096) throw error('OBSERVATION_LIMIT', 'Kimi exceeded the bounded turn observation limit');
52
+ }
53
+ fold(frame) {
54
+ if (!identifier(frame.agent_id)) return;
55
+ this.bytes += Buffer.byteLength(JSON.stringify(frame));
56
+ if (this.bytes > 64 * 1024 * 1024) throw error('OBSERVATION_LIMIT', 'Kimi exceeded the bounded invocation observation limit');
57
+ this.lastChange = Date.now();
58
+ const agentId = frame.agent_id, turns = this.agentTurns(agentId);
59
+ switch (frame.type) {
60
+ case 'agent': this.agents.set(agentId, frame); if (frame.compacting) this.unmeteredWork = true; break;
61
+ case 'turns':
62
+ if (!Array.isArray(frame.turns)) throw error('WS_PROTOCOL', 'Kimi conversation snapshot is invalid');
63
+ if (this.snapshots.has(agentId)) throw error('CONVERSATION_RESET', 'Kimi replaced a subscribed conversation; this invocation cannot certify continuous observation');
64
+ for (const turn of frame.turns) this.mergeTurn(agentId, turn);
65
+ this.snapshots.add(agentId); break;
66
+ case 'turns.failed': throw error('CONVERSATION_UNAVAILABLE', 'Kimi could not load the subscribed conversation');
67
+ case 'turn.started': case 'turn.ended': this.mergeTurn(agentId, frame.turn); break;
68
+ case 'turn.updated': {
69
+ const turn = turns.get(frame.turn_id);
70
+ if (turn && frame.step) { turn.steps = (turn.steps || []).filter(step => step.index !== frame.step.index); turn.steps.push(frame.step); }
71
+ break;
72
+ }
73
+ case 'item.started': case 'item.ended': {
74
+ const turn = turns.get(frame.turn_id);
75
+ if (!turn || !object(frame.item)) throw error('WS_PROTOCOL', 'Kimi item has no matching turn');
76
+ const index = turn.items.findIndex(item => item.item_id === frame.item.item_id);
77
+ if (index < 0) turn.items.push(frame.item); else turn.items[index] = frame.item;
78
+ break;
79
+ }
80
+ case 'item.updated': case 'item.delta': {
81
+ const item = turns.get(frame.turn_id)?.items.find(item => item.item_id === frame.item_id);
82
+ if (!item) throw error('WS_PROTOCOL', 'Kimi item update has no matching item');
83
+ if (frame.type === 'item.updated') Object.assign(item, frame.set);
84
+ else if (['text', 'input_text', 'output'].includes(frame.field) && typeof frame.delta === 'string') item[frame.field] = (item[frame.field] || '') + frame.delta;
85
+ break;
86
+ }
87
+ case 'item.removed': {
88
+ const turn = turns.get(frame.turn_id); if (turn) turn.items = turn.items.filter(item => item.item_id !== frame.item_id); break;
89
+ }
90
+ case 'turns.removed': for (const id of frame.turn_ids || []) turns.delete(id); break;
91
+ case 'task': this.tasks.set(agentId + '/' + frame.task_id, frame); break;
92
+ case 'task.list':
93
+ // An absent running task is no longer active, but retain its observation
94
+ // for accounting rather than silently deleting the child's identity.
95
+ for (const [key, task] of this.tasks) if (task.agent_id === agentId && task.status === 'running'
96
+ && !(frame.tasks || []).some(next => next.task_id === task.task_id)) this.tasks.set(key, { ...task, status: 'ended', outcome: task.outcome || 'unknown' });
97
+ for (const task of frame.tasks || []) this.tasks.set(agentId + '/' + task.task_id, { ...task, agent_id: agentId });
98
+ break;
99
+ case 'interaction': {
100
+ const key = agentId + '/' + (frame.interaction_id || frame.id);
101
+ if (frame.status === 'pending') this.pending.set(key, frame); else this.pending.delete(key);
102
+ break;
103
+ }
104
+ }
105
+ if (this.tasks.size > 4096 || this.turns.size > 512) throw error('OBSERVATION_LIMIT', 'Kimi exceeded the bounded child observation limit');
106
+ }
107
+ newTurns(agentId, baseline = -1) { return [...this.agentTurns(agentId).values()].filter(turn => turn.ordinal > baseline && turn.kind === 'agent').sort((a, b) => a.ordinal - b.ordinal); }
108
+ matchingPrompt(promptId) { return this.newTurns('main').find(turn => turn.items.some(item => item.type === 'user_message' && item.prompt_ids?.includes(promptId))); }
109
+ childIds(baseline = -1) {
110
+ const ids = new Set([...this.tasks.values()].map(task => task.child_agent_id).filter(Boolean));
111
+ for (const [agentId] of this.turns) for (const turn of this.newTurns(agentId, agentId === 'main' ? baseline : -1)) {
112
+ for (const item of turn.items) for (const ref of item.agent_refs || []) if (identifier(ref.agent_id)) ids.add(ref.agent_id);
113
+ }
114
+ ids.delete('main');
115
+ return ids;
116
+ }
117
+ quiescent() {
118
+ return !this.pending.size && ![...this.tasks.values()].some(task => task.status === 'running')
119
+ && ![...this.agents.values()].some(agent => agent.status === 'running' || agent.compacting);
120
+ }
121
+ }
122
+
123
+ export function kimiObservedUsage(conversation, baseline, mode, { resumed = false } = {}) {
124
+ const partial = [], sum = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 };
125
+ let complete = mode !== 'tower' && !conversation.unmeteredWork, stepsSeen = 0;
126
+ if ([...conversation.tasks.values()].some(task => task.kind === 'subagent' && !task.child_agent_id)) complete = false;
127
+ const children = conversation.childIds(baseline);
128
+ // A resumed root can resume an old child. The API has no invocation ledger
129
+ // for that child's preceding turns, so do not re-bill its whole history.
130
+ const unscopedChildren = resumed && children.size > 0;
131
+ if (unscopedChildren) complete = false;
132
+ for (const childId of children) if (!conversation.snapshots.has(childId) || !conversation.newTurns(childId).length) complete = false;
133
+ for (const [agentId] of conversation.turns) {
134
+ if (resumed && agentId !== 'main') continue;
135
+ const floor = agentId === 'main' ? baseline : -1;
136
+ if ([...conversation.agentTurns(agentId).values()].some(turn => turn.ordinal > floor
137
+ && turn.items.some(item => item.type === 'system' && item.kind === 'compaction'))) complete = false;
138
+ const turns = conversation.newTurns(agentId, floor);
139
+ for (const turn of turns) {
140
+ if (turn.status !== 'ended' || !turn.steps?.length) complete = false;
141
+ for (const item of turn.items) if (item.type === 'tool_call' && /^(?:Agent|AgentSwarm|TowerSpawn)$/.test(item.name || '') && !item.agent_refs?.length) complete = false;
142
+ for (const step of turn.steps || []) {
143
+ const usage = step.usage;
144
+ if (!usage || ['input_other', 'output', 'input_cache_read', 'input_cache_creation'].some(key => !Number.isSafeInteger(usage[key]) || usage[key] < 0)) { complete = false; continue; }
145
+ stepsSeen++;
146
+ partial.push({ agentId, turnId: turn.turn_id, step: step.index, usage });
147
+ sum.input += usage.input_other + usage.input_cache_read + usage.input_cache_creation;
148
+ sum.output += usage.output; sum.cacheRead += usage.input_cache_read; sum.cacheWrite += usage.input_cache_creation;
149
+ }
150
+ }
151
+ }
152
+ sum.totalTokens = sum.input + sum.output;
153
+ return { usage: complete && stepsSeen && Object.values(sum).every(Number.isSafeInteger) ? sum : null, observedSteps: partial,
154
+ scope: mode === 'tower' ? 'observed-agents-only; Tower has no complete tree usage API' : unscopedChildren ? 'root-only; resumed-child-history-is-not-invocation-scoped' : complete ? 'all-observed-agent-steps' : 'partial-or-unknown' };
155
+ }
156
+
157
+ function sessionBinding(input) {
158
+ const { role, scope, channel, taskDir } = input.managed;
159
+ // Go's sessionHash treats JSON objects canonically. Formatting a config or
160
+ // saving it through the editor must not invalidate equivalent Kimi history.
161
+ const canonical = value => Array.isArray(value) ? value.map(canonical) : object(value)
162
+ ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value;
163
+ return digest(canonical({ version: 1, cwd: input.cwd, workspaceCwd: input.workspaceCwd || input.cwd,
164
+ model: input.model, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode,
165
+ native: input.native, role, scope, channel, taskDir }));
166
+ }
167
+
168
+ /** Bridge emits only ready/event frames; the caller writes the returned final once. */
169
+ export async function runKimiWorker(raw, { emit = () => {}, env = process.env, signal, stderr = () => {} } = {}) {
170
+ let input = raw, runtime, socket, sessionId = '', ready = false, dispatch = 'not-dispatched', baseline = -1,
171
+ resumed = false, ownsSession = false, candidate, transfer, failure, matched = false, schema, environment, effective = {}, observedUsage,
172
+ cleanup = { confirmed: true, queueDrained: true, tasksStopped: true, remoteControlOff: true }, binding,
173
+ completion = { confirmed: false, pendingIds: [] }, lastQueue, promptMapping;
174
+ let completed = false, promptId, requestedTurnId = '', acceptingEvents = false;
175
+ const conversation = new KimiConversation(), subscribed = new Set(), subscribing = new Map();
176
+ const controller = new AbortController();
177
+ const cancel = () => controller.abort(signal?.reason);
178
+ if (signal?.aborted) cancel(); else signal?.addEventListener('abort', cancel, { once: true });
179
+ let deadlineTimer;
180
+ const send = (type, content) => emit({ protocol: 1, requestId: input?.requestId || 'invalid-request', type, sessionId, ...content });
181
+ const sessionPath = suffix => `/api/v1/sessions/${encoded(sessionId)}${suffix}`;
182
+ const observe = frame => {
183
+ if (frame.session_id !== sessionId) return;
184
+ conversation.fold(frame);
185
+ if (ready && ['task', 'turn.started', 'turn.ended', 'interaction'].includes(frame.type)) {
186
+ send('event', { event: { type: frame.type, agentId: frame.agent_id, taskId: frame.task_id,
187
+ turnId: frame.turn?.turn_id, status: frame.status ?? frame.turn?.status, outcome: frame.outcome ?? frame.turn?.outcome } });
188
+ }
189
+ const turn = promptId && conversation.matchingPrompt(promptId);
190
+ if (turn) { requestedTurnId = turn.turn_id; dispatch = 'dispatched'; }
191
+ };
192
+ const subscribe = async (agentId, timeout = 3000) => {
193
+ if (subscribed.has(agentId)) return;
194
+ if (subscribing.has(agentId)) return subscribing.get(agentId);
195
+ const promise = socket.command('subscribe', { session_id: sessionId, agent_ids: [agentId] }, timeout).then(() => { subscribed.add(agentId); });
196
+ subscribing.set(agentId, promise);
197
+ try { await promise; } finally { subscribing.delete(agentId); }
198
+ };
199
+ const syncChildren = async (deadline = Infinity) => {
200
+ if (!acceptingEvents) return;
201
+ for (const id of conversation.childIds(baseline)) {
202
+ if (Date.now() >= deadline) throw error('CLEANUP_TIMEOUT', 'Kimi child cleanup exceeded its grace');
203
+ await subscribe(id, Math.min(3000, deadline - Date.now()));
204
+ }
205
+ };
206
+ const getQueue = async options => { lastQueue = await runtime.request(sessionPath('/prompts'), undefined, options); return lastQueue; };
207
+ const idle = async options => {
208
+ const [session, queue] = await Promise.all([runtime.request(sessionPath(''), undefined, options), getQueue(options)]);
209
+ if (!object(queue) || !Array.isArray(queue.queued) || typeof session.busy !== 'boolean') throw error('RUNTIME_PROTOCOL', 'Kimi returned an invalid queue or session status');
210
+ return { idle: session.busy === false && session.main_turn_active === false && session.pending_interaction === 'none'
211
+ && !queue.active && queue.queued.length === 0 && conversation.quiescent(), session, queue };
212
+ };
213
+ const readTurns = async (agentId, options = { signal: controller.signal }) => {
214
+ let before, pages = 0;
215
+ const floor = agentId === 'main' ? baseline : -1;
216
+ while (++pages <= 64) {
217
+ checkAbort(options.signal);
218
+ const page = await runtime.request(sessionPath(`/agents/${encoded(agentId)}/turns?page_size=200${before !== undefined ? '&before_ordinal=' + before : ''}`), undefined, options);
219
+ if (!Array.isArray(page.turns)) throw error('RUNTIME_PROTOCOL', 'Kimi turn page is invalid');
220
+ for (const turn of page.turns) if (turn.ordinal > floor) conversation.mergeTurn(agentId, turn);
221
+ if (!page.has_more || !page.turns.length || Math.min(...page.turns.map(turn => turn.ordinal)) <= floor) return;
222
+ const next = Math.min(...page.turns.map(turn => turn.ordinal));
223
+ if (before !== undefined && next >= before) throw error('RUNTIME_PROTOCOL', 'Kimi turn pagination failed to advance');
224
+ before = next;
225
+ }
226
+ throw error('OBSERVATION_LIMIT', 'Kimi turn history exceeds the bounded reconciliation limit');
227
+ };
228
+ try {
229
+ input = validateKimiNativeStart(raw);
230
+ const declaredCwd = input.cwd;
231
+ input = { ...input, cwd: await realpath(input.cwd), managed: { ...input.managed, taskDir: await realpath(input.managed.taskDir) } };
232
+ deadlineTimer = setTimeout(() => controller.abort(error('TIMEOUT', 'The Kimi invocation exceeded its wall timeout')), input.timeoutMs);
233
+ checkAbort(controller.signal);
234
+ if (input.native.mode === 'tower') candidate = await createTowerCandidate(input);
235
+ runtime = new OwnedKimiRuntime(env, input.native.mode);
236
+ stderr('Starting an owned authenticated Kimi web runtime\n');
237
+ await runtime.start(controller.signal);
238
+ const [openapi, asyncapi, remote] = await Promise.all([
239
+ runtime.request('/openapi.json', undefined, { raw: true, signal: controller.signal }),
240
+ runtime.request('/asyncapi.json', undefined, { raw: true, signal: controller.signal }),
241
+ runtime.request('/api/v1/remote-control', undefined, { signal: controller.signal }),
242
+ ]);
243
+ schema = inspectKimiSchemas(openapi, asyncapi, input.native.mode);
244
+ if (remote.state !== 'off') throw error('REMOTE_CONTROL_ACTIVE', 'The owned Kimi runtime has remote control enabled');
245
+ binding = sessionBinding(input);
246
+ const cwd = candidate?.path || input.cwd;
247
+ const agentConfig = { ...(input.native.agentProfile ? { profile: input.native.agentProfile } : {}), model: input.model,
248
+ thinking: input.reasoningEffort, permission_mode: 'yolo', plan_mode: false, swarm_mode: input.native.mode === 'swarm' };
249
+ let session;
250
+ if (input.sessionId) {
251
+ sessionId = input.sessionId;
252
+ session = await runtime.request(sessionPath(''), undefined, { signal: controller.signal });
253
+ if (session.metadata?.flywheel_native_binding !== binding || session.metadata?.cwd !== cwd) throw error('RESUME_BINDING_MISMATCH', 'Kimi history belongs to a different task, role, candidate or native contract');
254
+ resumed = true; ownsSession = true;
255
+ } else {
256
+ session = await runtime.request('/api/v1/sessions', { metadata: { cwd, flywheel_native_binding: binding },
257
+ title: `Flywheel ${input.managed.role}`, agent_config: agentConfig, environment_id: 'local', environment_cwd: cwd }, { signal: controller.signal });
258
+ sessionId = session.id;
259
+ ownsSession = true;
260
+ }
261
+ if (!identifier(sessionId) || session.id !== sessionId) throw error('RUNTIME_PROTOCOL', 'Kimi returned an invalid session identity');
262
+ if (candidate) await runtime.request(sessionPath('/profile'), { agent_config: { tower_mode: true, tower_base: candidate.branch } }, { signal: controller.signal });
263
+ environment = await runtime.request(sessionPath('/environment'), undefined, { signal: controller.signal });
264
+ if (environment.environment_id !== 'local' || typeof environment.cwd !== 'string' || await realpath(environment.cwd) !== cwd) throw error('ENVIRONMENT_MISMATCH', 'Kimi did not bind the requested local execution directory');
265
+ socket = new KimiSocket(runtime, observe);
266
+ await socket.open(controller.signal);
267
+ await subscribe('main');
268
+ let waitUntil = Date.now() + 5000;
269
+ while (!conversation.agents.has('main') || !conversation.snapshots.has('main')) {
270
+ checkAbort(controller.signal); socket.check();
271
+ if (Date.now() > waitUntil) throw error('ADMISSION_TIMEOUT', 'Kimi did not provide effective agent state and initial history');
272
+ await pause(20);
273
+ }
274
+ const main = conversation.agents.get('main');
275
+ if (main.model !== input.model || main.thinking !== input.reasoningEffort || main.permission_mode !== 'yolo'
276
+ || main.plan_mode !== false || main.swarm_mode !== (input.native.mode === 'swarm') || main.tower_mode !== (input.native.mode === 'tower')) {
277
+ throw error('EFFECTIVE_CONFIG_MISMATCH', 'Kimi effective model, effort, permission or native mode differs from the request');
278
+ }
279
+ if (!(await idle()).idle) throw error('SESSION_BUSY', 'Kimi history has active work, queued input or a pending interaction');
280
+ // OpenAPI intentionally omits the session action dispatcher. Probe actual
281
+ // idempotent abort on an idle, owned session instead of inventing a schema.
282
+ const abortProbe = await runtime.request(sessionPath(':abort'), {});
283
+ if (abortProbe.aborted !== true) throw error('ABORT_UNSUPPORTED', 'Kimi did not acknowledge the owned-session abort capability');
284
+ baseline = Math.max(-1, ...[...conversation.agentTurns('main').values()].map(turn => turn.ordinal));
285
+ acceptingEvents = true;
286
+ effective = { matched: true, mode: input.native.mode, model: main.model, reasoningEffort: main.thinking,
287
+ permissionMode: input.permissionMode, permission: main.permission_mode, plan: main.plan_mode,
288
+ swarm: main.swarm_mode, tower: main.tower_mode, agentProfile: input.native.agentProfile || 'agent',
289
+ agents: input.native.agents?.enabled !== false, concurrency: { enforcement: 'native', scope: 'native-batch-or-subsystem', upperBound: null, hardTreeCap: false } };
290
+ matched = true; ready = true;
291
+ send('ready', { cwd: input.cwd, workspaceCwd: input.workspaceCwd || input.cwd, model: input.model,
292
+ reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode, persistent: Boolean(input.persistent), resumed });
293
+ promptId = 'flywheel-' + digest({ requestId: input.requestId, invocationId: input.managed.invocationId }).slice(0, 40);
294
+ checkAbort(controller.signal);
295
+ let submittedPrompt = input.prompt;
296
+ if (candidate) { const mapped = towerPrompt(input, candidate, declaredCwd); submittedPrompt = mapped.prompt; promptMapping = mapped.mapping; }
297
+ dispatch = 'unknown';
298
+ const prompt = await runtime.request(sessionPath('/prompts'), { content: [{ type: 'text', text: submittedPrompt }], prompt_id: promptId,
299
+ model: input.model, thinking: input.reasoningEffort, permission_mode: 'yolo', swarm_mode: input.native.mode === 'swarm',
300
+ disabled_tools: ['CronCreate', ...(input.native.agents?.enabled === false ? ['Agent', 'AgentSwarm'] : [])],
301
+ metadata: { flywheel_invocation_id: input.managed.invocationId } }, { signal: controller.signal, timeout: 30_000 });
302
+ if (prompt.prompt_id !== promptId || !['running', 'queued'].includes(prompt.status)) {
303
+ throw error('PROMPT_NOT_RUNNING', 'Kimi did not accept the correlated invocation as running or queued');
304
+ }
305
+ let stableSince = 0;
306
+ while (true) {
307
+ checkAbort(controller.signal); socket.check();
308
+ if (runtime.closed) throw error('RUNTIME_EXITED', 'Kimi runtime exited during the invocation');
309
+ if (conversation.pending.size) throw error('INTERACTION_REQUIRED', 'Kimi requires an interactive approval or answer; no response was fabricated');
310
+ await syncChildren();
311
+ const turn = conversation.matchingPrompt(promptId);
312
+ if (turn) { requestedTurnId = turn.turn_id; dispatch = 'dispatched'; }
313
+ if (turn?.status === 'ended') {
314
+ const state = await idle();
315
+ if (state.idle && [...conversation.newTurns('main', baseline)].every(item => item.status === 'ended')
316
+ && [...conversation.childIds(baseline)].every(id => conversation.snapshots.has(id))) {
317
+ stableSince ||= Date.now();
318
+ if (Date.now() - stableSince >= 350 && Date.now() - conversation.lastChange >= 200) break;
319
+ } else stableSince = 0;
320
+ }
321
+ await pause(70);
322
+ }
323
+ for (const agentId of subscribed) await readTurns(agentId);
324
+ await syncChildren();
325
+ if (!(await idle()).idle || [...conversation.childIds(baseline)].some(id => !conversation.snapshots.has(id))
326
+ || [...conversation.turns].some(([id]) => conversation.newTurns(id, id === 'main' ? baseline : -1).some(turn => turn.status !== 'ended'))) {
327
+ throw error('NATIVE_WORK_ACTIVE', 'Kimi revealed additional native work during final reconciliation');
328
+ }
329
+ const turns = conversation.newTurns('main', baseline);
330
+ if (!turns.length || turns.some(turn => turn.status !== 'ended' || turn.outcome !== 'completed')) throw error('TURN_FAILED', 'Kimi did not complete every root continuation successfully');
331
+ completed = true;
332
+ } catch (cause) {
333
+ failure = controller.signal.aborted && typeof controller.signal.reason?.code === 'string' ? controller.signal.reason : cause;
334
+ } finally {
335
+ // Capture completion before cleanup; stopping unfinished work must never
336
+ // turn an incomplete native result into an accepted role result.
337
+ completion = { confirmed: completed, pendingIds: [
338
+ ...[...conversation.tasks.values()].filter(task => task.status === 'running').map(task => 'task:' + task.agent_id + '/' + task.task_id),
339
+ ...[...conversation.agents].filter(([, agent]) => agent.status === 'running' || agent.compacting).map(([id]) => 'agent:' + id),
340
+ ...[...conversation.pending.keys()].map(id => 'interaction:' + id),
341
+ ...(lastQueue?.queued || []).map(prompt => 'prompt:' + prompt.prompt_id),
342
+ ...(lastQueue?.active ? ['prompt:' + lastQueue.active.prompt_id] : []),
343
+ ] };
344
+ if (completion.pendingIds.length) completion.confirmed = false;
345
+ clearTimeout(deadlineTimer);
346
+ signal?.removeEventListener('abort', cancel);
347
+ // Cleanup has a separate bounded grace. An aborted model request must not
348
+ // cancel queue withdrawal, child stop or owned-server shutdown requests.
349
+ if (runtime) {
350
+ cleanup = { confirmed: false, queueDrained: !ownsSession, tasksStopped: !ownsSession, remoteControlOff: !runtime.base };
351
+ const cleanupDeadline = Date.now() + 6000;
352
+ const cleanupSignal = AbortSignal.timeout(6000);
353
+ const options = { signal: cleanupSignal, timeout: 1000 };
354
+ try {
355
+ if (ownsSession && sessionId && runtime.base && !runtime.closed) {
356
+ do {
357
+ checkAbort(cleanupSignal);
358
+ const queue = await getQueue(options);
359
+ for (const prompt of queue.queued || []) {
360
+ try { await runtime.request(sessionPath(`/prompts/${encoded(prompt.prompt_id)}:abort`), {}, options); }
361
+ catch (cause) { if (cause.code !== 'KIMI_API_40402') throw cause; }
362
+ }
363
+ if (queue.active || !conversation.quiescent()) await runtime.request(sessionPath(':abort'), {}, options);
364
+ if (socket && !socket.closed) {
365
+ await syncChildren(cleanupDeadline);
366
+ for (const task of conversation.tasks.values()) if (task.status === 'running') {
367
+ checkAbort(cleanupSignal);
368
+ try {
369
+ const reply = await socket.command('task.stop', { session_id: sessionId, agent_id: task.agent_id, task_id: task.task_id }, Math.max(1, Math.min(1000, cleanupDeadline - Date.now())));
370
+ if (reply.task) conversation.fold({ ...reply.task, type: 'task', agent_id: task.agent_id });
371
+ } catch (cause) { if (cause.code !== 'KIMI_WS_40406') throw cause; }
372
+ }
373
+ }
374
+ const state = await idle(options);
375
+ cleanup.queueDrained = !state.queue.active && state.queue.queued.length === 0;
376
+ cleanup.tasksStopped = state.idle;
377
+ if (state.idle) break;
378
+ await pause(75);
379
+ } while (Date.now() < cleanupDeadline);
380
+ if (socket && !socket.closed && cleanup.tasksStopped) {
381
+ for (const agentId of subscribed) { try { await readTurns(agentId, options); } catch { /* Missing final usage stays unknown. */ } }
382
+ }
383
+ }
384
+ if (runtime.base && !runtime.closed) {
385
+ let remote = await runtime.request('/api/v1/remote-control', undefined, options);
386
+ if (remote.state !== 'off') {
387
+ await runtime.request('/api/v1/remote-control', { enabled: false }, options);
388
+ remote = await runtime.request('/api/v1/remote-control', undefined, options);
389
+ }
390
+ cleanup.remoteControlOff = remote.state === 'off';
391
+ }
392
+ } catch (cause) { cleanup.error = cause.code || 'CLEANUP_FAILED'; }
393
+ socket?.close();
394
+ const process = await runtime.stop();
395
+ cleanup.process = process;
396
+ cleanup.confirmed = cleanup.queueDrained && cleanup.tasksStopped && cleanup.remoteControlOff && process.exited && process.processesConfirmed;
397
+ if (!cleanup.confirmed && !failure) failure = error('CLEANUP_UNKNOWN', 'Kimi work or process cleanup could not be confirmed');
398
+ }
399
+ }
400
+ if (candidate && cleanup.confirmed) {
401
+ try {
402
+ candidate = await freezeTowerCandidate(candidate, input);
403
+ if (completed && !failure) transfer = await transferTowerCandidate(candidate, input, true);
404
+ } catch (cause) { failure ||= error('CANDIDATE_TRANSFER_FAILED', cause.message); transfer = { applied: false, reason: cause.message }; }
405
+ }
406
+ observedUsage = kimiObservedUsage(conversation, baseline, input?.native?.mode, { resumed });
407
+ let text = conversation.newTurns('main', baseline).flatMap(turn => turn.items.filter(item => item.type === 'assistant_message').map(item => item.text || '')).join('\n');
408
+ if (Buffer.byteLength(text) > 512 * 1024) { failure ||= error('OUTPUT_LIMIT', 'Kimi output exceeds the complete receipt limit'); text = ''; }
409
+ if (completed && !failure && !text.trim()) failure = error('EMPTY_OUTPUT', 'Kimi completed without a final assistant result');
410
+ const cancelled = controller.signal.aborted && controller.signal.reason?.code !== 'TIMEOUT';
411
+ const status = cancelled ? 'cancelled' : failure || !completed ? 'error' : 'ok';
412
+ const turns = conversation.newTurns('main', baseline), terminal = turns.at(-1);
413
+ const outcome = dispatch === 'not-dispatched' ? 'failed' : dispatch === 'unknown' || !terminal || terminal.status !== 'ended' ? 'unknown'
414
+ : terminal.outcome === 'completed' ? 'completed' : terminal.outcome === 'interrupted' ? 'cancelled' : 'failed';
415
+ const native = { version: 1, requested: raw?.native ?? null, matched, effective: { ...effective, matched },
416
+ observed: { server: runtime?.meta ? { id: runtime.meta.server_id, version: runtime.meta.server_version, backend: runtime.meta.backend,
417
+ ownedPid: runtime.child.pid, authenticated: true, local: true } : null, schema: schema ?? null, environment: environment ?? null,
418
+ binding: binding ?? null, managed: input?.managed ?? null, promptMapping: promptMapping ?? null, abortProbe: matched, usage: observedUsage, nativeLimits: { wholeTreeUpperBound: null, admission: 'native-runtime', hardMaxConcurrentSupported: false },
419
+ disabledTools: ready ? ['CronCreate', ...(input.native.agents?.enabled === false ? ['Agent', 'AgentSwarm'] : [])] : [], inferenceTested: dispatch === 'dispatched' },
420
+ children: [...conversation.tasks.values()].map(task => ({ taskId: task.task_id, parentAgentId: task.agent_id, agentId: task.child_agent_id ?? null,
421
+ kind: task.kind, status: task.status, outcome: task.outcome ?? null, model: task.model ?? null, thinking: task.thinking ?? null, swarmIndex: task.swarm_index ?? null })),
422
+ completion, cleanup, ...(candidate ? { candidate: towerCandidateReceipt(candidate, transfer) } : {}) };
423
+ return { protocol: 1, requestId: raw?.requestId || 'invalid-request', type: 'result', status, settled: cleanup.confirmed,
424
+ sessionId, text, usage: dispatch === 'dispatched' ? observedUsage.usage : null, costUsd: null, native,
425
+ ...(failure ? { error: { code: typeof failure.code === 'string' ? failure.code : 'KIMI_NATIVE_FAILED', message: failure.message || 'Kimi native invocation failed' } } : {}),
426
+ lifecycle: { protocol: 'kimi-server/v1', requestId: raw?.requestId || 'invalid-request', sessionId, dispatch, outcome,
427
+ cleanupConfirmed: cleanup.confirmed, resumeCandidate: status === 'ok' && Boolean(input.persistent) && input.native.mode !== 'tower',
428
+ resumeAccepted: resumed && ready, usageStatus: dispatch === 'dispatched' && observedUsage.usage !== null ? 'complete' : 'unknown', turnId: requestedTurnId } };
429
+ }