maka-agent 0.2.0-dev.26.20260909 → 0.2.0-dev.27.20260910

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 (62) hide show
  1. package/dist/cli-core.js +21 -0
  2. package/dist/runtime-host-cli.js +43 -3
  3. package/dist/runtime-host-handoff-surface.js +10 -8
  4. package/dist/runtime-host-lifecycle-transaction.js +28 -3
  5. package/dist/runtime-host-local-source-retirement.js +1 -1
  6. package/dist/runtime-host-setup-command.js +29 -6
  7. package/dist/runtime-host-update-command.js +49 -5
  8. package/dist/session-export-command.js +72 -0
  9. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  10. package/node_modules/@maka/core/dist/usage-stats/types.js +2 -0
  11. package/node_modules/@maka/core/dist/workhub-routing.js +126 -0
  12. package/node_modules/@maka/core/package.json +2 -1
  13. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +7 -3
  14. package/node_modules/@maka/runtime/dist/builtin-tools.js +21 -2
  15. package/node_modules/@maka/runtime/dist/plugin-agent-service.js +131 -0
  16. package/node_modules/@maka/runtime/dist/plugin-approval-service.js +33 -0
  17. package/node_modules/@maka/runtime/dist/plugin-attachment-service.js +53 -0
  18. package/node_modules/@maka/runtime/dist/plugin-command-service.js +100 -0
  19. package/node_modules/@maka/runtime/dist/plugin-data-services.js +332 -0
  20. package/node_modules/@maka/runtime/dist/plugin-fs-service.js +62 -0
  21. package/node_modules/@maka/runtime/dist/plugin-goal-service.js +61 -0
  22. package/node_modules/@maka/runtime/dist/plugin-invocation-signal.js +24 -0
  23. package/node_modules/@maka/runtime/dist/plugin-llm-service.js +85 -0
  24. package/node_modules/@maka/runtime/dist/plugin-lsp-service.js +94 -0
  25. package/node_modules/@maka/runtime/dist/plugin-scope-registry.js +91 -0
  26. package/node_modules/@maka/runtime/dist/plugin-session-query-service.js +71 -0
  27. package/node_modules/@maka/runtime/dist/plugin-shell-env-service.js +87 -0
  28. package/node_modules/@maka/runtime/dist/plugin-shell-service.js +74 -0
  29. package/node_modules/@maka/runtime/dist/plugin-skill-service.js +93 -0
  30. package/node_modules/@maka/runtime/dist/plugin-system-prompt-service.js +346 -0
  31. package/node_modules/@maka/runtime/dist/plugin-tool-service.js +18 -52
  32. package/node_modules/@maka/runtime/dist/plugin-user-question-service.js +43 -0
  33. package/node_modules/@maka/runtime/dist/plugin-web-service.js +69 -0
  34. package/node_modules/@maka/runtime/dist/session-export.js +177 -0
  35. package/node_modules/@maka/runtime/package.json +18 -0
  36. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +67 -18
  37. package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +46 -12
  38. package/node_modules/@maka/runtime-host/dist/operator/setup-frame.js +31 -21
  39. package/node_modules/@maka/runtime-host/dist/protocol/index.js +9 -1
  40. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +30 -5
  41. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +25 -23
  42. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +62 -4
  43. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +452 -7
  44. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +74 -0
  45. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +2 -2
  46. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +30 -0
  47. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +79 -16
  48. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +25 -4
  49. package/node_modules/@maka/runtime-host/dist/server/plugin-data-runtime.js +234 -0
  50. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
  51. package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +12 -0
  52. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +189 -92
  53. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +44 -35
  54. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +92 -10
  55. package/node_modules/@maka/storage/dist/agent-run-store.js +54 -10
  56. package/node_modules/@maka/storage/dist/context-offload-snapshot.js +13 -6
  57. package/node_modules/@maka/storage/dist/execution-stores.js +1 -1
  58. package/node_modules/@maka/storage/dist/session-bundle-policy.js +333 -30
  59. package/node_modules/@maka/storage/dist/session-store.js +2 -2
  60. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +12 -5
  61. package/node_modules/@maka/storage/package.json +2 -0
  62. package/package.json +1 -1
@@ -0,0 +1,177 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ /**
20
+ * Export one Session, and the subagent subtree under it, as a portable bundle.
21
+ *
22
+ * The preparation is `exportSessionBundleState()` in `@maka/storage`, which
23
+ * already takes a consistent database snapshot, keeps writers out, filters the
24
+ * copy down to the exported Sessions, carries the context-offload closure, and
25
+ * proves the result with `PRAGMA foreign_key_check`. This module is the thin
26
+ * part: point it at a workspace, ask for a quiescent Session, and seal what
27
+ * comes back with the bundle codec.
28
+ *
29
+ * What it adds on top is the manifest — the Sessions carried, the schema
30
+ * versions the SOURCE database registers, and the connection by slug and model
31
+ * so an importer can resolve it locally. No credential is carried: the state
32
+ * allow-list in the export policy decides what is copied, and configuration is
33
+ * not on it.
34
+ */
35
+ import { mkdir, mkdtemp, rm, stat } from 'node:fs/promises';
36
+ import { join } from 'node:path';
37
+ import { tmpdir } from 'node:os';
38
+ import { SessionBundleFileError, } from '@maka/storage/session-bundle-contract';
39
+ import { createSessionBundleFileService } from '@maka/storage/session-bundle-file-service';
40
+ import { exportSessionBundleState, SessionBundleExportError, SESSION_BUNDLE_OMITTED_EVENT_TYPES, } from '@maka/storage/session-bundle-policy';
41
+ export const SESSION_EXPORT_MEDIA_TYPE = 'application/vnd.maka.session-export+json;version=1';
42
+ /** Generous by design; the codec is what enforces them. */
43
+ export const SESSION_EXPORT_BUNDLE_LIMITS = {
44
+ maxCompressedBytes: 2 * 1024 ** 3,
45
+ maxDecompressedTarBytes: 8 * 1024 ** 3,
46
+ maxPayloadBytes: 8 * 1024 ** 3,
47
+ maxFileBytes: 2 * 1024 ** 3,
48
+ maxEntryCount: 1_000_000,
49
+ maxManifestBytes: 4 * 1024 ** 2,
50
+ maxStateIdentityBytes: 4 * 1024 ** 2,
51
+ maxPathBytes: 4096,
52
+ maxPathDepth: 32,
53
+ };
54
+ export async function exportSessionBundle(input) {
55
+ if (!(await isDirectory(input.workspaceRoot))) {
56
+ return {
57
+ ok: false,
58
+ reason: { kind: 'workspace_not_found', workspaceRoot: input.workspaceRoot },
59
+ };
60
+ }
61
+ if (await exists(input.destination)) {
62
+ return { ok: false, reason: { kind: 'destination_exists' } };
63
+ }
64
+ const staging = await mkdtemp(join(tmpdir(), 'maka-session-export-'));
65
+ try {
66
+ const stateRoot = join(staging, 'state');
67
+ const workspaceRoot = join(staging, 'workspace');
68
+ // The codec seals a state tree beside a workspace tree. A Session's own
69
+ // files are the state; the user's project directory is not part of the
70
+ // conversation and is deliberately absent.
71
+ await mkdir(workspaceRoot, { recursive: true });
72
+ let plan;
73
+ try {
74
+ plan = await exportSessionBundleState({
75
+ stateRoot: input.workspaceRoot,
76
+ // Maka's desktop layout keeps state and configuration in one directory,
77
+ // so these are shared on purpose. Configuration stays out of the bundle
78
+ // because the policy copies an allow-list of state entries, not because
79
+ // the roots are apart.
80
+ configRoot: input.workspaceRoot,
81
+ allowShared: true,
82
+ destinationRoot: stateRoot,
83
+ sessionId: input.sessionId,
84
+ requireQuiescent: true,
85
+ includeSubtree: true,
86
+ omitDiagnostics: true,
87
+ });
88
+ }
89
+ catch (error) {
90
+ const failure = asExportFailure(error, input.workspaceRoot);
91
+ if (failure)
92
+ return { ok: false, reason: failure };
93
+ throw error;
94
+ }
95
+ const manifest = {
96
+ format: 'maka.session-export',
97
+ formatVersion: 1,
98
+ exportedAt: (input.now ?? Date.now)(),
99
+ rootSessionId: plan.sessionId,
100
+ sessionIds: plan.sessionIds,
101
+ schema: plan.sourceSchema,
102
+ connection: plan.connection,
103
+ includedEntries: plan.includedEntries,
104
+ excludedEntries: plan.excludedEntries,
105
+ diagnosticsOmitted: true,
106
+ omittedEventTypes: SESSION_BUNDLE_OMITTED_EVENT_TYPES,
107
+ };
108
+ const stateIdentity = {
109
+ mediaType: SESSION_EXPORT_MEDIA_TYPE,
110
+ bytes: Buffer.from(`${JSON.stringify(manifest)}\n`, 'utf8'),
111
+ };
112
+ const artifact = await createSessionBundleFileService().pack({
113
+ snapshot: { stateRoot, workspaceRoot, stateIdentity },
114
+ envelope: { sessionId: plan.sessionId },
115
+ destination: input.destination,
116
+ limits: input.limits ?? SESSION_EXPORT_BUNDLE_LIMITS,
117
+ });
118
+ return { ok: true, artifact, export: manifest };
119
+ }
120
+ catch (error) {
121
+ if (error instanceof SessionBundleFileError) {
122
+ if (error.details?.quota !== undefined) {
123
+ return { ok: false, reason: { kind: 'bundle_limit', details: error.details } };
124
+ }
125
+ // The precheck fails fast, but the packer publishes atomically and can
126
+ // still lose the race. Reporting that as a generic IO failure would give
127
+ // a caller a different exit code for the same outcome.
128
+ if (error.code === 'destination_exists') {
129
+ return { ok: false, reason: { kind: 'destination_exists' } };
130
+ }
131
+ return { ok: false, reason: { kind: 'io_failed', message: error.message } };
132
+ }
133
+ const failure = asExportFailure(error, input.workspaceRoot);
134
+ if (failure)
135
+ return { ok: false, reason: failure };
136
+ return { ok: false, reason: { kind: 'io_failed', message: String(error) } };
137
+ }
138
+ finally {
139
+ await rm(staging, { recursive: true, force: true }).catch(() => { });
140
+ }
141
+ }
142
+ function asExportFailure(error, workspaceRoot) {
143
+ if (!(error instanceof SessionBundleExportError))
144
+ return undefined;
145
+ switch (error.code) {
146
+ case 'session_active':
147
+ return { kind: 'session_active', message: error.message };
148
+ case 'schema_unsupported':
149
+ return { kind: 'schema_unsupported', message: error.message };
150
+ case 'symlink':
151
+ case 'path_escape':
152
+ case 'unsupported_entry':
153
+ return { kind: 'artifact_unsafe', message: error.message };
154
+ case 'missing_entry':
155
+ return { kind: 'artifact_missing', message: error.message };
156
+ case 'invalid_root':
157
+ // The policy reports a missing Session through this code too, and only
158
+ // the message separates it from a directory that is not a workspace.
159
+ return /session does not exist/i.test(error.message)
160
+ ? { kind: 'session_not_found' }
161
+ : { kind: 'workspace_not_found', workspaceRoot };
162
+ case 'destination_not_empty':
163
+ return { kind: 'destination_exists' };
164
+ default:
165
+ return { kind: 'io_failed', message: error.message };
166
+ }
167
+ }
168
+ async function isDirectory(path) {
169
+ return stat(path)
170
+ .then((metadata) => metadata.isDirectory())
171
+ .catch(() => false);
172
+ }
173
+ async function exists(path) {
174
+ return stat(path)
175
+ .then(() => true)
176
+ .catch(() => false);
177
+ }
@@ -76,8 +76,25 @@
76
76
  "./plan-mode": "./dist/plan-mode.js",
77
77
  "./plan-tools": "./dist/plan-tools.js",
78
78
  "./plugin-composition-loader": "./dist/plugin-composition-loader.js",
79
+ "./plugin-command-service": "./dist/plugin-command-service.js",
80
+ "./plugin-data-services": "./dist/plugin-data-services.js",
81
+ "./plugin-goal-service": "./dist/plugin-goal-service.js",
82
+ "./plugin-lsp-service": "./dist/plugin-lsp-service.js",
83
+ "./plugin-session-query-service": "./dist/plugin-session-query-service.js",
84
+ "./plugin-shell-env-service": "./dist/plugin-shell-env-service.js",
85
+ "./plugin-skill-service": "./dist/plugin-skill-service.js",
86
+ "./plugin-agent-service": "./dist/plugin-agent-service.js",
87
+ "./plugin-attachment-service": "./dist/plugin-attachment-service.js",
88
+ "./plugin-approval-service": "./dist/plugin-approval-service.js",
89
+ "./plugin-fs-service": "./dist/plugin-fs-service.js",
79
90
  "./plugin-kernel": "./dist/plugin-kernel.js",
91
+ "./plugin-llm-service": "./dist/plugin-llm-service.js",
80
92
  "./plugin-runtime": "./dist/plugin-runtime.js",
93
+ "./plugin-scope-registry": "./dist/plugin-scope-registry.js",
94
+ "./plugin-system-prompt-service": "./dist/plugin-system-prompt-service.js",
95
+ "./plugin-shell-service": "./dist/plugin-shell-service.js",
96
+ "./plugin-user-question-service": "./dist/plugin-user-question-service.js",
97
+ "./plugin-web-service": "./dist/plugin-web-service.js",
81
98
  "./plugin-tool-service": "./dist/plugin-tool-service.js",
82
99
  "./process-tree-terminator": "./dist/process-tree-terminator.js",
83
100
  "./provider-request-telemetry": "./dist/provider-request-telemetry.js",
@@ -88,6 +105,7 @@
88
105
  "./sandbox-boundary-tool": "./dist/sandbox-boundary-tool.js",
89
106
  "./scheduled-task-tools": "./dist/scheduled-task-tools.js",
90
107
  "./session-recap": "./dist/session-recap.js",
108
+ "./session-export": "./dist/session-export.js",
91
109
  "./session-trace-projection": "./dist/session-trace-projection.js",
92
110
  "./shell-detect": "./dist/shell-detect.js",
93
111
  "./shell-run-contract": "./dist/shell-run-contract.js",
@@ -22,6 +22,7 @@ export function formatHostHandoff(view, locale) {
22
22
  const tw = locale === 'zh-TW';
23
23
  const titles = tw
24
24
  ? {
25
+ replacement_required: '需要切換 WSL 背景服務',
25
26
  busy: '背景服務仍在使用中',
26
27
  activity_unknown: '需要確認是否停止背景服務',
27
28
  operator_required: '背景服務需要手動更新',
@@ -30,6 +31,7 @@ export function formatHostHandoff(view, locale) {
30
31
  }
31
32
  : zh
32
33
  ? {
34
+ replacement_required: '需要切换 WSL 后台服务',
33
35
  busy: '后台服务仍在使用中',
34
36
  activity_unknown: '需要确认是否停止后台服务',
35
37
  operator_required: '后台服务需要手动更新',
@@ -37,6 +39,7 @@ export function formatHostHandoff(view, locale) {
37
39
  retry_required: '暂时无法完成交接',
38
40
  }
39
41
  : {
42
+ replacement_required: 'Switch the WSL background service',
40
43
  busy: 'Your background service is still in use',
41
44
  activity_unknown: 'Confirm before stopping the service',
42
45
  operator_required: 'The background service needs a manual update',
@@ -45,6 +48,7 @@ export function formatHostHandoff(view, locale) {
45
48
  };
46
49
  const descriptions = tw
47
50
  ? {
51
+ replacement_required: '目前尚未更新。選擇繼續後,將透過已安裝的管理程式安全停止舊服務並啟動選定版本;有進行中的工作時會另行確認。',
48
52
  busy: '其他連線或正在執行的工作阻止了自動交接。停止並繼續可能中斷這些工作。',
49
53
  activity_unknown: '背景服務與目前用戶端不相容,且無法確認有哪些工作仍在執行。停止並繼續會重新啟動服務,可能中斷其他視窗或裝置上的工作。',
50
54
  operator_required: `目前無法從這裡更新背景服務。請透過 ${view.target.name} 上管理此服務的應用程式或命令更新,再重試。`,
@@ -53,6 +57,7 @@ export function formatHostHandoff(view, locale) {
53
57
  }
54
58
  : zh
55
59
  ? {
60
+ replacement_required: '当前尚未更新。选择继续后,将通过已安装的管理程序安全停止旧服务并启动选定版本;有活动工作时会另行确认。',
56
61
  busy: '其它连接或正在执行的工作阻止了自动交接。停止并继续可能中断这些工作。',
57
62
  activity_unknown: '后台服务与当前客户端不兼容,且无法确认有哪些工作仍在运行。停止并继续会重新启动服务,可能中断其他窗口或设备上的工作。',
58
63
  operator_required: `目前无法从这里更新后台服务。请通过 ${view.target.name} 上管理此服务的应用或命令更新,然后重试。`,
@@ -60,6 +65,7 @@ export function formatHostHandoff(view, locale) {
60
65
  retry_required: '服务状态发生了变化,或交接尚未完成。可以安全重试,不会默认中断工作。',
61
66
  }
62
67
  : {
68
+ replacement_required: 'No update is running. Continue to retire the old service through its installed operator and start the selected version. Interrupting active work requires a separate confirmation.',
63
69
  busy: 'Other connections or work in progress prevent an automatic handoff. Stopping the service may interrupt that work.',
64
70
  activity_unknown: 'The background service is incompatible with this client, and its active work is unknown. Stop and continue restarts it and may interrupt work in other windows or devices.',
65
71
  operator_required: `This client cannot update the background service here. Use its managing app or operator command on ${view.target.name}, then retry.`,
@@ -89,6 +95,28 @@ export function formatHostHandoff(view, locale) {
89
95
  };
90
96
  descriptions.operator_required = guidance[view.recoveryBlocker];
91
97
  }
98
+ if (view.manualRecheck && view.reason === 'busy') {
99
+ descriptions.busy = zh
100
+ ? tw
101
+ ? '舊服務拒絕安全停止;這可能是其他連線或背景工作所致。關閉其他用戶端後可再次嘗試安全停止,或明確選擇中斷工作並繼續。'
102
+ : '旧服务拒绝安全停止;这可能是其他连接或后台工作所致。关闭其他客户端后可再次尝试安全停止,或明确选择中断工作并继续。'
103
+ : 'The old service refused safe retirement. Other connections or background work may be keeping it in use. Close other clients and try stopping safely again, or explicitly interrupt work and continue.';
104
+ }
105
+ if (view.manualRecheck && view.reason === 'operator_required') {
106
+ titles.operator_required = zh
107
+ ? tw
108
+ ? '無法連線到背景服務'
109
+ : '无法连接后台服务'
110
+ : 'Cannot connect to the background service';
111
+ descriptions.operator_required = zh
112
+ ? tw
113
+ ? '目前版本無法連線,且尚未執行更新。請查看診斷或更新用戶端,完成後重新檢查。'
114
+ : '当前版本无法连接,且尚未执行更新。请查看诊断或更新客户端,完成后重新检查。'
115
+ : 'These builds cannot connect and no update is running. Check the diagnostic or update the client, then recheck.';
116
+ }
117
+ const packageChange = view.packageChange
118
+ ? `${view.target.name}: ${view.packageChange.current} → ${view.packageChange.target}`
119
+ : '';
92
120
  const activity = view.activity;
93
121
  const background = activity?.drainResidencies;
94
122
  const backgroundFacts = !activity
@@ -111,17 +139,23 @@ export function formatHostHandoff(view, locale) {
111
139
  ? `${activity.connections} 个连接 · ${activity.activeOperations} 个进行中的操作`
112
140
  : `${activity.connections} connections · ${activity.activeOperations} operations in progress`
113
141
  : '';
114
- const waiting = view.mayExitNaturally
115
- ? tw
116
- ? 'Maka 會持續檢查,並在可以安全繼續時自動繼續。'
117
- : zh
118
- ? 'Maka 会持续检查,并在可以安全继续时自动继续。'
119
- : 'Maka keeps checking and continues automatically when it is safe.'
120
- : tw
121
- ? 'Maka 會持續檢查。若狀態沒有改變,等待或重試不會解決此問題。'
122
- : zh
123
- ? 'Maka 会持续检查。若状态没有变化,等待或重试不会解决此问题。'
124
- : 'Maka keeps checking. Waiting or retrying will not resolve this unless the service state changes.';
142
+ const waiting = view.manualRecheck
143
+ ? zh
144
+ ? tw
145
+ ? '重新檢查只會重試連線,不會更新服務。'
146
+ : '重新检查只会重试连接,不会更新服务。'
147
+ : 'Recheck retries the connection without updating the service.'
148
+ : view.mayExitNaturally
149
+ ? tw
150
+ ? 'Maka 會持續檢查,並在可以安全繼續時自動繼續。'
151
+ : zh
152
+ ? 'Maka 会持续检查,并在可以安全继续时自动继续。'
153
+ : 'Maka keeps checking and continues automatically when it is safe.'
154
+ : tw
155
+ ? 'Maka 會持續檢查。若狀態沒有改變,等待或重試不會解決此問題。'
156
+ : zh
157
+ ? 'Maka 会持续检查。若状态没有变化,等待或重试不会解决此问题。'
158
+ : 'Maka keeps checking. Waiting or retrying will not resolve this unless the service state changes.';
125
159
  const repairNotice = view.operation === 'repair'
126
160
  ? tw
127
161
  ? '修復將使用目前應用程式配套的版本,可能替換現有服務版本。'
@@ -132,19 +166,34 @@ export function formatHostHandoff(view, locale) {
132
166
  const labels = tw
133
167
  ? {
134
168
  cancel: '取消',
135
- retry: '安全重試',
136
- interrupt: view.operation === 'repair' ? '中斷並修復' : '停止並繼續',
169
+ retry: view.manualRecheck ? '重新檢查' : '安全重試',
170
+ replace: '停止舊服務並繼續',
171
+ interrupt: view.manualRecheck
172
+ ? '中斷工作並繼續'
173
+ : view.operation === 'repair'
174
+ ? '中斷並修復'
175
+ : '停止並繼續',
137
176
  }
138
177
  : zh
139
178
  ? {
140
179
  cancel: '取消',
141
- retry: '安全重试',
142
- interrupt: view.operation === 'repair' ? '中断并修复' : '停止并继续',
180
+ retry: view.manualRecheck ? '重新检查' : '安全重试',
181
+ replace: '停止旧服务并继续',
182
+ interrupt: view.manualRecheck
183
+ ? '中断工作并继续'
184
+ : view.operation === 'repair'
185
+ ? '中断并修复'
186
+ : '停止并继续',
143
187
  }
144
188
  : {
145
189
  cancel: 'Cancel',
146
- retry: 'Retry safely',
147
- interrupt: view.operation === 'repair' ? 'Interrupt and repair' : 'Stop and continue',
190
+ retry: view.manualRecheck ? 'Recheck' : 'Retry safely',
191
+ replace: 'Stop old service and continue',
192
+ interrupt: view.manualRecheck
193
+ ? 'Interrupt work and continue'
194
+ : view.operation === 'repair'
195
+ ? 'Interrupt and repair'
196
+ : 'Stop and continue',
148
197
  };
149
198
  const phases = tw
150
199
  ? {
@@ -187,7 +236,7 @@ export function formatHostHandoff(view, locale) {
187
236
  : zh
188
237
  ? '正在完成交接或安全收尾,请稍候。'
189
238
  : 'Finishing the handoff or its safe recovery. Please wait.'
190
- : [facts, backgroundFacts, waiting, repairNotice, view.operatorStep]
239
+ : [packageChange, facts, backgroundFacts, waiting, repairNotice, view.operatorStep]
191
240
  .filter(Boolean)
192
241
  .join('\n'),
193
242
  actions: view.actions.map((action) => ({ action, label: labels[action] })),
@@ -55,6 +55,8 @@ export async function runHostHandoff(input) {
55
55
  // (or a broken successor) into an automatic replacement storm.
56
56
  const attempted = new Set();
57
57
  let automaticAttempts = 0;
58
+ let refusedWork;
59
+ let replacementCompleted = false;
58
60
  let recovery;
59
61
  const publish = (next) => {
60
62
  view = next;
@@ -102,7 +104,10 @@ export async function runHostHandoff(input) {
102
104
  target: view.target,
103
105
  reason: 'unavailable',
104
106
  mayExitNaturally: false,
105
- diagnostic: boundedDiagnostic(error),
107
+ ...(view.manualRecheck ? { manualRecheck: true } : {}),
108
+ diagnostic: replacementCompleted
109
+ ? `Host replacement completed, but reconnection failed: ${boundedDiagnostic(error)}`
110
+ : boundedDiagnostic(error),
106
111
  },
107
112
  };
108
113
  }
@@ -124,11 +129,13 @@ export async function runHostHandoff(input) {
124
129
  const blocker = observed.blocker;
125
130
  if (recovery?.identity !== blocker.identity)
126
131
  recovery = undefined;
132
+ if (refusedWork !== blocker.identity)
133
+ refusedWork = undefined;
127
134
  const nextSignature = blockerSignature(blocker);
128
135
  if (signature !== nextSignature) {
129
136
  signature = nextSignature;
130
137
  choice = undefined;
131
- publish(projectBlocker(blocker, randomUUID(), recovery?.diagnostic));
138
+ publish(projectBlocker(blocker, randomUUID(), recovery?.diagnostic, refusedWork === blocker.identity));
132
139
  }
133
140
  const current = view;
134
141
  const selected = choice;
@@ -142,12 +149,19 @@ export async function runHostHandoff(input) {
142
149
  const automatic = !recovery &&
143
150
  automaticAttempts < 3 &&
144
151
  !attempted.has(nextSignature) &&
152
+ !blocker.replacement?.requiresExplicitSelection &&
145
153
  blocker.replacement?.canReplaceIdle &&
146
154
  blocker.activity &&
147
155
  (isHostActivityIdle(blocker.activity) || cooperative);
148
156
  const interrupt = selected?.revision === current.revision && selected.action === 'interrupt';
157
+ const replace = selected?.revision === current.revision && selected.action === 'replace';
149
158
  const retry = selected?.revision === current.revision && selected.action === 'retry';
150
- if ((automatic || interrupt || (retry && blocker.replacement?.canReplaceIdle)) &&
159
+ if ((automatic ||
160
+ interrupt ||
161
+ replace ||
162
+ (retry &&
163
+ !blocker.replacement?.requiresExplicitSelection &&
164
+ blocker.replacement?.canReplaceIdle)) &&
151
165
  blocker.replacement) {
152
166
  if (automatic) {
153
167
  attempted.add(nextSignature);
@@ -179,6 +193,8 @@ export async function runHostHandoff(input) {
179
193
  finally {
180
194
  attemptAbort = undefined;
181
195
  }
196
+ replacementCompleted = result.kind === 'completed';
197
+ refusedWork = result.kind === 'active_work' ? blocker.identity : undefined;
182
198
  recovery =
183
199
  result.kind === 'recovery_required'
184
200
  ? { identity: blocker.identity, diagnostic: result.diagnostic.slice(0, 8_192) }
@@ -195,7 +211,7 @@ export async function runHostHandoff(input) {
195
211
  signature = undefined;
196
212
  continue;
197
213
  }
198
- const attention = projectBlocker(blocker, current.revision, recovery?.diagnostic);
214
+ const attention = projectBlocker(blocker, current.revision, recovery?.diagnostic, refusedWork === blocker.identity);
199
215
  if (!input.openSurface)
200
216
  throw new HostHandoffRequiredError(attention);
201
217
  surface ??= input.openSurface(submit);
@@ -211,7 +227,7 @@ export async function runHostHandoff(input) {
211
227
  resolve();
212
228
  };
213
229
  const abort = () => finish(input.signal?.reason);
214
- const timer = setTimeout(finish, interval);
230
+ const timer = blocker.manualRecheck ? undefined : setTimeout(finish, interval);
215
231
  wake = () => finish();
216
232
  input.signal?.addEventListener('abort', abort, { once: true });
217
233
  if (input.signal?.aborted)
@@ -225,7 +241,7 @@ export async function runHostHandoff(input) {
225
241
  surface?.close();
226
242
  }
227
243
  }
228
- function projectBlocker(blocker, revision, recovery) {
244
+ function projectBlocker(blocker, revision, recovery, activeWorkRefused = false) {
229
245
  const replacement = blocker.replacement;
230
246
  return {
231
247
  revision,
@@ -237,15 +253,30 @@ function projectBlocker(blocker, revision, recovery) {
237
253
  ? 'repair_required'
238
254
  : !replacement
239
255
  ? 'operator_required'
240
- : !blocker.activity
241
- ? 'activity_unknown'
242
- : isHostActivityIdle(blocker.activity)
243
- ? 'retry_required'
244
- : 'busy',
256
+ : replacement.requiresExplicitSelection
257
+ ? activeWorkRefused
258
+ ? 'busy'
259
+ : 'replacement_required'
260
+ : !blocker.activity
261
+ ? 'activity_unknown'
262
+ : isHostActivityIdle(blocker.activity)
263
+ ? 'retry_required'
264
+ : 'busy',
245
265
  ...(blocker.activity ? { activity: blocker.activity } : {}),
246
266
  mayExitNaturally: blocker.mayExitNaturally,
267
+ ...(blocker.manualRecheck ? { manualRecheck: true } : {}),
268
+ ...(blocker.packageChange ? { packageChange: blocker.packageChange } : {}),
247
269
  ...(replacement ? { operation: replacement.kind } : {}),
248
- actions: replacement?.canInterrupt ? ['cancel', 'retry', 'interrupt'] : ['cancel', 'retry'],
270
+ actions: replacement?.requiresExplicitSelection
271
+ ? [
272
+ 'cancel',
273
+ 'retry',
274
+ 'replace',
275
+ ...(activeWorkRefused && replacement.canInterrupt ? ['interrupt'] : []),
276
+ ]
277
+ : replacement?.canInterrupt
278
+ ? ['cancel', 'retry', 'interrupt']
279
+ : ['cancel', 'retry'],
249
280
  defaultAction: 'cancel',
250
281
  ...(blocker.recoveryBlocker ? { recoveryBlocker: blocker.recoveryBlocker } : {}),
251
282
  ...(blocker.operatorStep ? { operatorStep: blocker.operatorStep } : {}),
@@ -265,6 +296,9 @@ function blockerSignature(blocker) {
265
296
  blocker.replacement?.kind,
266
297
  blocker.replacement?.canReplaceIdle,
267
298
  blocker.replacement?.canInterrupt,
299
+ blocker.replacement?.requiresExplicitSelection,
300
+ blocker.manualRecheck,
301
+ blocker.packageChange,
268
302
  blocker.operatorStep,
269
303
  blocker.recoveryBlocker,
270
304
  blocker.diagnostic,
@@ -41,6 +41,29 @@ const frameBase = {
41
41
  schemaVersion: z.literal(1),
42
42
  sequence: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
43
43
  };
44
+ const environmentBindingFields = {
45
+ version: boundedString(128),
46
+ serviceId: z.string().regex(/^[a-f0-9]{64}$/u),
47
+ deploymentId: z.string().uuid(),
48
+ operator: z.unknown().transform((value, context) => {
49
+ try {
50
+ const command = decodeRuntimeHostOperatorCommand(value);
51
+ if (command.kind !== 'node') {
52
+ throw new Error('Runtime Host setup operator must be a Node command');
53
+ }
54
+ return command;
55
+ }
56
+ catch (error) {
57
+ context.addIssue({
58
+ code: 'custom',
59
+ message: error instanceof Error ? error.message : 'Runtime Host operator is invalid',
60
+ });
61
+ return z.NEVER;
62
+ }
63
+ }),
64
+ rootPath: boundedString(4 * 1024).refine((value) => !/[\u0000-\u001f\u007f]/u.test(value)),
65
+ rootId: z.string().regex(/^[a-f0-9]{64}$/u),
66
+ };
44
67
  const SETUP_FRAME_SCHEMA = z.discriminatedUnion('kind', [
45
68
  z
46
69
  .object({
@@ -53,27 +76,7 @@ const SETUP_FRAME_SCHEMA = z.discriminatedUnion('kind', [
53
76
  .object({
54
77
  ...frameBase,
55
78
  kind: z.literal('complete'),
56
- version: boundedString(128),
57
- serviceId: z.string().regex(/^[a-f0-9]{64}$/u),
58
- deploymentId: z.string().uuid(),
59
- operator: z.unknown().transform((value, context) => {
60
- try {
61
- const command = decodeRuntimeHostOperatorCommand(value);
62
- if (command.kind !== 'node') {
63
- throw new Error('Runtime Host setup operator must be a Node command');
64
- }
65
- return command;
66
- }
67
- catch (error) {
68
- context.addIssue({
69
- code: 'custom',
70
- message: error instanceof Error ? error.message : 'Runtime Host operator is invalid',
71
- });
72
- return z.NEVER;
73
- }
74
- }),
75
- rootPath: boundedString(4 * 1024).refine((value) => !/[\u0000-\u001f\u007f]/u.test(value)),
76
- rootId: z.string().regex(/^[a-f0-9]{64}$/u),
79
+ ...environmentBindingFields,
77
80
  endpoint: boundedString(SETUP_FIELD_MAX_BYTES).refine((value) => parseRuntimeHostSetupEndpoint(value) !== undefined),
78
81
  credentialId: boundedString(SETUP_FIELD_MAX_BYTES),
79
82
  credential: boundedString(SETUP_CREDENTIAL_MAX_BYTES),
@@ -87,6 +90,13 @@ const SETUP_FRAME_SCHEMA = z.discriminatedUnion('kind', [
87
90
  .optional(),
88
91
  })
89
92
  .strict(),
93
+ z
94
+ .object({
95
+ ...frameBase,
96
+ kind: z.literal('existing_environment'),
97
+ ...environmentBindingFields,
98
+ })
99
+ .strict(),
90
100
  z
91
101
  .object({
92
102
  ...frameBase,
@@ -62,7 +62,15 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1;
62
62
  export const RUNTIME_HOST_PROTOCOL_VERSION = 0;
63
63
  // Increment when the same protocol version no longer guarantees safe Client-Host
64
64
  // interoperability. Mismatches are rejected before domain commands are admitted.
65
- export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 135;
65
+ export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 141;
66
+ // 141: WorkHub root admissions bind model Intent/Recall decisions before actions.
67
+ // 140: Plugin Platform queries expose scoped Command contribution projections.
68
+ // Epoch-139 peers reject the added query view and result shape.
69
+ // 139: WorkHub recovery preserves the Host-authenticated Desktop capability binding.
70
+ // 138: Removes the unused steering display anchor from canonical MessageContent.
71
+ // 137: Reserved by the former display anchor contract.
72
+ // 136: WorkHub transient proposals distinguish routing dispositions from linked
73
+ // operations. Older peers expect replace/stop_work/resume_work dispositions.
66
74
  // 135: WorkHub model Turns replace direct action proposals with active-Turn task tools.
67
75
  // 134: Coordination actions own real Runtime Turns. Removes the synthetic record
68
76
  // operation, projects typed action receipts and admitted action identities, and