@ai-workbench/remote-agent 0.8.2 → 0.8.10

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 (194) hide show
  1. package/README.md +42 -7
  2. package/dist/acp.d.ts +46 -1
  3. package/dist/acp.js +296 -36
  4. package/dist/approval-label.d.ts +6 -0
  5. package/dist/approval-label.js +57 -0
  6. package/dist/broker-runtime.d.ts +10 -0
  7. package/dist/broker-runtime.js +334 -0
  8. package/dist/broker.d.ts +29 -6
  9. package/dist/broker.js +146 -142
  10. package/dist/claude-local.d.ts +37 -0
  11. package/dist/claude-local.js +207 -0
  12. package/dist/client.js +71 -2
  13. package/dist/conversation-details.d.ts +1 -1
  14. package/dist/conversation-details.js +88 -8
  15. package/dist/employee-endpoint.d.ts +91 -0
  16. package/dist/employee-endpoint.js +141 -0
  17. package/dist/employee-exec.d.ts +53 -0
  18. package/dist/employee-exec.js +423 -0
  19. package/dist/employee-location.d.ts +66 -0
  20. package/dist/employee-location.js +51 -0
  21. package/dist/employee-mcp.d.ts +42 -0
  22. package/dist/employee-mcp.js +181 -0
  23. package/dist/employee-native.d.ts +18 -0
  24. package/dist/employee-native.js +38 -0
  25. package/dist/employee-provider.d.ts +42 -0
  26. package/dist/employee-provider.js +326 -0
  27. package/dist/employee-relay.d.ts +18 -0
  28. package/dist/employee-relay.js +49 -0
  29. package/dist/employee-shell.d.ts +26 -0
  30. package/dist/employee-shell.js +89 -0
  31. package/dist/employee-ssh.d.ts +22 -0
  32. package/dist/employee-ssh.js +123 -0
  33. package/dist/employee-tools.d.ts +117 -0
  34. package/dist/employee-tools.js +292 -0
  35. package/dist/employee-workspace.d.ts +26 -0
  36. package/dist/employee-workspace.js +61 -0
  37. package/dist/index.d.ts +31 -14
  38. package/dist/index.js +335 -42
  39. package/dist/input.d.ts +15 -0
  40. package/dist/input.js +65 -24
  41. package/dist/local-user-turn.d.ts +24 -0
  42. package/dist/local-user-turn.js +103 -0
  43. package/dist/model-catalog.d.ts +55 -0
  44. package/dist/model-catalog.js +254 -0
  45. package/dist/model-configuration.d.ts +51 -0
  46. package/dist/model-configuration.js +212 -0
  47. package/dist/presentation.d.ts +1 -1
  48. package/dist/presentation.js +50 -2
  49. package/dist/protocol-boundary.d.ts +13 -0
  50. package/dist/protocol-boundary.js +49 -0
  51. package/dist/provider.d.ts +54 -2
  52. package/dist/provider.js +92 -29
  53. package/dist/readiness.js +1 -1
  54. package/dist/recovery-checkpoint.d.ts +6 -0
  55. package/dist/recovery-checkpoint.js +71 -0
  56. package/dist/recovery.d.ts +47 -0
  57. package/dist/recovery.js +61 -0
  58. package/dist/remote-details.d.ts +10 -0
  59. package/dist/remote-details.js +14 -4
  60. package/dist/rpc-namespace.d.ts +3 -0
  61. package/dist/rpc-namespace.js +50 -0
  62. package/dist/stage-timing.d.ts +27 -0
  63. package/dist/stage-timing.js +39 -0
  64. package/helpers/ripgrep/LICENSE-vscode +13 -0
  65. package/helpers/ripgrep/PROVENANCE.md +9 -0
  66. package/helpers/ripgrep/darwin-arm64/rg +0 -0
  67. package/helpers/ripgrep/darwin-x64/rg +0 -0
  68. package/helpers/ripgrep/manifest.json +56 -0
  69. package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
  70. package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
  71. package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
  72. package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
  73. package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
  74. package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
  75. package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
  76. package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
  77. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
  78. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
  79. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
  80. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
  81. package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
  82. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
  83. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
  84. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
  85. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
  86. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
  87. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
  88. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
  89. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
  90. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
  91. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
  92. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
  93. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
  94. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
  95. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
  96. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
  97. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
  98. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
  99. package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
  100. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
  101. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
  102. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
  103. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
  104. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
  105. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
  106. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
  107. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
  108. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
  109. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
  110. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
  111. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
  112. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
  113. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
  114. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
  115. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
  116. package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
  117. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
  118. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
  119. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
  120. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
  121. package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
  122. package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
  123. package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
  124. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
  125. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
  126. package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
  127. package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
  128. package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
  129. package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
  130. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
  131. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
  132. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
  133. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
  134. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
  135. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
  136. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
  137. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
  138. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
  139. package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
  140. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
  141. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
  142. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
  143. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
  144. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
  145. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
  146. package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
  147. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
  148. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
  149. package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
  150. package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
  151. package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
  152. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
  153. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
  154. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
  155. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
  156. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
  157. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
  158. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
  159. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
  160. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
  161. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
  162. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
  163. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
  164. package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
  165. package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
  166. package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
  167. package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
  168. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
  169. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
  170. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
  171. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
  172. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
  173. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
  174. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
  175. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
  176. package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
  177. package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
  178. package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
  179. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
  180. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  181. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
  182. package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
  183. package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
  184. package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
  185. package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
  186. package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
  187. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
  188. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
  189. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
  190. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  191. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
  192. package/locale/en.json +1 -1
  193. package/locale/zh.json +2 -2
  194. package/package.json +30 -12
package/dist/acp.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import * as acp from '@agentclientprotocol/sdk/unstable';
2
2
  import { NeedError } from "./remote.js";
3
+ import { employeeFailure } from "./presentation.js";
4
+ import { StageTiming } from "./stage-timing.js";
5
+ import { isolateRpcIds } from "./rpc-namespace.js";
6
+ import { recoveryCapabilities, supportsCodexContextPolicy } from "./recovery.js";
3
7
  import { sanitizeObservation } from "./conversation-details.js";
8
+ import { ModelConfiguration, ModelConfigurationError } from "./model-configuration.js";
4
9
  /** ACP v1 with explicitly negotiated SDK 2.0 lifecycle/context extensions. */
5
10
  export class AcpSession {
6
11
  connection;
@@ -16,35 +21,136 @@ export class AcpSession {
16
21
  updating = false;
17
22
  cwd;
18
23
  systemPrompt = '';
24
+ codexNewSessionOnly = false;
25
+ creatingReplacement = false;
26
+ employeeMeta;
27
+ employeeNegotiated = false;
28
+ configuration;
29
+ configRevision = 0;
30
+ selected;
31
+ selecting = false;
32
+ resolvedEvidence;
33
+ pendingConfigurations = new Map();
34
+ get modelConfiguration() { return this.configuration?.snapshot; }
35
+ get resolvedModelEvidence() { return this.resolvedEvidence; }
36
+ get models() { if (!this.configuration)
37
+ throw new ModelConfigurationError('远端没有提供完整配置列表'); return this.configuration.models; }
38
+ receiveConfiguration(options) {
39
+ this.configRevision++;
40
+ if (this.configuration)
41
+ this.configuration.update(options);
42
+ else
43
+ this.configuration = new ModelConfiguration(options, async (configId, value) => {
44
+ const result = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId, value }, { cancellationSignal: this.turnSignal });
45
+ if (this.resolvedEvidence && (this.configuration?.snapshot.model !== this.resolvedEvidence.modelValue || (configId === this.configuration?.snapshot.modelConfigId && value !== this.resolvedEvidence.modelValue)))
46
+ this.resolvedEvidence = undefined;
47
+ this.hooks.broker?.acknowledge(acp.methods.agent.session.setConfigOption);
48
+ return result;
49
+ });
50
+ if (this.resolvedEvidence && this.configuration.snapshot.model !== this.resolvedEvidence.modelValue)
51
+ this.resolvedEvidence = undefined;
52
+ }
53
+ resetConfiguration() {
54
+ this.selected = undefined;
55
+ this.resolvedEvidence = undefined;
56
+ if (this.configuration) {
57
+ try {
58
+ this.receiveConfiguration([]);
59
+ }
60
+ catch { /* Invalidated until this lifecycle operation supplies new evidence. */ }
61
+ }
62
+ return this.configRevision;
63
+ }
64
+ lifecycleConfiguration(result, revision) {
65
+ const pending = this.pendingConfigurations.get(this.sessionId);
66
+ this.pendingConfigurations.clear();
67
+ if (pending)
68
+ this.receiveConfiguration(pending);
69
+ // A notification observed during the operation is newer than its in-flight response.
70
+ else if (this.configRevision === revision && result.configOptions)
71
+ this.receiveConfiguration(result.configOptions);
72
+ const model = this.configuration?.snapshot.model;
73
+ if (!pending && this.configRevision === revision + 1 && model && typeof result.models?.currentModelId === 'string' && result.models.currentModelId)
74
+ this.resolvedEvidence = { modelValue: model, resolvedModel: result.models.currentModelId };
75
+ }
76
+ async selectModel(model, effort, signal) {
77
+ if (!this.ready || !this.sessionId)
78
+ throw new ModelConfigurationError('远端会话尚未建立');
79
+ if (this.busy || this.updating || this.selecting)
80
+ throw new ModelConfigurationError('上一项会话操作尚未完成');
81
+ signal?.throwIfAborted();
82
+ this.checkReporting();
83
+ this.selecting = true;
84
+ this.selected = { model, ...(effort === undefined ? {} : { effort }) };
85
+ this.turnSignal = signal;
86
+ try {
87
+ if (!this.configuration)
88
+ throw new ModelConfigurationError('远端没有提供完整配置列表');
89
+ const state = await this.configuration.select(model, effort);
90
+ signal?.throwIfAborted();
91
+ this.checkReporting();
92
+ return state;
93
+ }
94
+ finally {
95
+ this.turnSignal = undefined;
96
+ this.selecting = false;
97
+ }
98
+ }
99
+ assertModelSelection(model, effort) {
100
+ if (!this.configuration)
101
+ throw new ModelConfigurationError('远端没有提供完整配置列表');
102
+ this.configuration.assertSelection(model, effort);
103
+ }
19
104
  reports = [];
20
105
  reportingFailure;
106
+ timing = new StageTiming();
107
+ stage(phase, operation) { return (this.hooks.timing ?? this.timing).run(phase, operation); }
21
108
  get diagnostics() { return this.reports.map(entry => sanitizeObservation(entry)); }
22
- detail(observation) {
109
+ detail(observation, hooks = this.hooks) {
23
110
  const safe = sanitizeObservation(observation);
24
111
  this.reports.push(safe);
25
112
  try {
26
- this.hooks.onDetail?.(safe);
113
+ hooks.onDetail?.(safe);
27
114
  }
28
115
  catch (cause) {
29
116
  this.reportingFailure ??= { cause };
30
117
  this.connection?.close();
31
118
  }
32
119
  }
33
- need(need) {
34
- this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } });
35
- this.hooks.onNeed?.(need);
120
+ need(need, hooks = this.hooks) {
121
+ this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } }, hooks);
122
+ hooks.onNeed?.(need);
36
123
  }
37
124
  checkReporting() { if (this.reportingFailure)
38
125
  throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能记录;本轮未继续发送', '可用的技术详情记录渠道'); }
39
126
  constructor(transport, hooks) {
40
127
  this.hooks = hooks;
128
+ this.timing.bind(detail => this.detail(detail));
41
129
  const app = acp.client({ name: '@ai-workbench/remote-agent' })
42
130
  .onNotification(acp.methods.client.session.update, ({ params: notification }) => {
131
+ if (this.creatingReplacement) {
132
+ if (notification.sessionId !== this.sessionId && notification.update.sessionUpdate === 'config_option_update')
133
+ this.pendingConfigurations.set(notification.sessionId, notification.update.configOptions);
134
+ this.detail({ kind: 'session', severity: 'info', code: 'ACP_IDLE_UPDATE', summary: '新会话建立期间的轨迹,未作为正文处理', method: 'session/update', payload: notification });
135
+ return;
136
+ }
43
137
  if (this.sessionId && notification.sessionId !== this.sessionId) {
44
138
  this.detail({ kind: 'session', severity: 'warning', code: 'ACP_FOREIGN_SESSION_UPDATE', summary: '收到不属于当前远端会话的通知,未作为正文处理', method: 'session/update', payload: notification });
45
139
  return;
46
140
  }
47
141
  const meta = record(notification._meta);
142
+ if (notification.update.sessionUpdate === 'config_option_update') {
143
+ this.resolvedEvidence = undefined;
144
+ if (!this.sessionId)
145
+ this.pendingConfigurations.set(notification.sessionId, notification.update.configOptions);
146
+ else
147
+ try {
148
+ this.receiveConfiguration(notification.update.configOptions);
149
+ }
150
+ catch (error) {
151
+ this.detail({ kind: 'session', severity: 'action-required', code: 'MODEL_CONFIGURATION_INVALID', summary: '远端模型配置无效,本轮不可发送', method: 'session/update', payload: { reason: error instanceof ModelConfigurationError ? error.message : '配置无法解析' } });
152
+ }
153
+ }
48
154
  if (meta && Object.keys(meta).length)
49
155
  this.detail({ kind: 'extension', severity: 'info', code: 'ACP_NOTIFICATION_META', summary: '远端通知扩展', method: 'session/update', payload: notification._meta });
50
156
  if (this.busy) {
@@ -57,12 +163,14 @@ export class AcpSession {
57
163
  this.detail({ kind: notification.update.sessionUpdate === 'available_commands_update' ? 'commands' : 'session', severity: 'info', code: 'ACP_IDLE_UPDATE', summary: '远端会话建立、恢复或空闲时的轨迹', method: 'session/update', payload: notification });
58
164
  })
59
165
  .onRequest(acp.methods.client.session.requestPermission, async ({ params: request, signal: rpcSignal, requestId }) => {
166
+ const hooks = this.hooks, detail = (value) => this.detail(value, hooks), need = (value) => this.need(value, hooks);
60
167
  if (request.sessionId !== this.sessionId || !this.busy || this.turnSignal?.aborted)
61
168
  return { outcome: { outcome: 'cancelled' } };
62
- this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
169
+ hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
63
170
  const toolName = request.toolCall.name ?? record(request.toolCall._meta?.claudeCode)?.toolName;
171
+ detail({ kind: 'tool', severity: 'info', code: 'ACP_PERMISSION_REQUEST', summary: '远端工具审批标识', method: 'session/request_permission', payload: { sessionId: request.sessionId, toolCall: { toolCallId: request.toolCall.toolCallId, ...(typeof toolName === 'string' ? { name: toolName } : {}) } } });
64
172
  if (toolName === 'AskUserQuestion') {
65
- this.need({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问;已取消,未提交答案', requires: '可承载此提问的官方交互接口' });
173
+ need({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问;已取消,未提交答案', requires: '可承载此提问的官方交互接口' });
66
174
  return { outcome: { outcome: 'cancelled' } };
67
175
  }
68
176
  const eligible = request.options.filter(option => {
@@ -70,11 +178,11 @@ export class AcpSession {
70
178
  const sameKind = request.options.filter(candidate => candidate.kind === option.kind);
71
179
  const unique = single && sameKind.length === 1 && request.options.filter(candidate => candidate.optionId === option.optionId).length === 1;
72
180
  if (!unique)
73
- this.detail({ kind: 'tool', severity: 'info', code: 'APPROVAL_OPTION_NOT_OFFERED', summary: '此远端审批选项未映射到单次审批', method: 'session/request_permission', payload: option });
181
+ detail({ kind: 'tool', severity: 'info', code: 'APPROVAL_OPTION_NOT_OFFERED', summary: '此远端审批选项未映射到单次审批', method: 'session/request_permission', payload: option });
74
182
  return unique;
75
183
  });
76
184
  if (!eligible.length) {
77
- this.need({ code: 'APPROVAL_OPTION_UNSUPPORTED', message: '远端没有可唯一映射的单次审批选项,此动作未获授权', requires: '唯一对应的允许一次或拒绝一次选项' });
185
+ need({ code: 'APPROVAL_OPTION_UNSUPPORTED', message: '远端没有可唯一映射的单次审批选项,此动作未获授权', requires: '唯一对应的允许一次或拒绝一次选项' });
78
186
  return { outcome: { outcome: 'cancelled' } };
79
187
  }
80
188
  const presented = { ...request, options: eligible };
@@ -82,7 +190,7 @@ export class AcpSession {
82
190
  const id = this.sessionId;
83
191
  let outcome = 'unavailable';
84
192
  const approvalSignal = this.turnSignal ? AbortSignal.any([this.turnSignal, rpcSignal]) : rpcSignal;
85
- const broker = this.hooks.broker;
193
+ const broker = hooks.broker;
86
194
  let generation;
87
195
  // A transport disconnect withdraws only the UI question, not the ACP RPC.
88
196
  // Late UI answers lose their lease; a fresh online attachment asks again.
@@ -96,7 +204,7 @@ export class AcpSession {
96
204
  const abort = () => interrupted.resolve('cancelled');
97
205
  signal.addEventListener('abort', abort, { once: true });
98
206
  try {
99
- outcome = await Promise.race([this.hooks.approve?.(presented, signal) ?? Promise.resolve('unavailable'), interrupted.promise]);
207
+ outcome = await Promise.race([hooks.approve?.(presented, signal) ?? Promise.resolve('unavailable'), interrupted.promise]);
100
208
  }
101
209
  finally {
102
210
  signal.removeEventListener('abort', abort);
@@ -107,7 +215,7 @@ export class AcpSession {
107
215
  }
108
216
  }
109
217
  catch {
110
- this.need({ code: 'APPROVAL_FAILED', message: '审批渠道失败或连接已结束;此动作未获授权', requires: '可用的 DSH 审批渠道和原连接' });
218
+ need({ code: 'APPROVAL_FAILED', message: '审批渠道失败或连接已结束;此动作未获授权', requires: '可用的 DSH 审批渠道和原连接' });
111
219
  }
112
220
  const kind = outcome === 'allowed-once' ? 'allow_once' : outcome === 'rejected' ? 'reject_once' : undefined;
113
221
  const option = kind && eligible.find(item => item.kind === kind);
@@ -116,7 +224,7 @@ export class AcpSession {
116
224
  broker.authorizeResponse(requestId, generation);
117
225
  return { outcome: { outcome: 'selected', optionId: option.optionId } };
118
226
  }
119
- this.need({ code: outcome === 'allowed-once' ? 'APPROVAL_OPTION_UNSUPPORTED' : 'APPROVAL_UNAVAILABLE',
227
+ need({ code: outcome === 'allowed-once' ? 'APPROVAL_OPTION_UNSUPPORTED' : 'APPROVAL_UNAVAILABLE',
120
228
  message: '此动作未获授权;审批已取消、不可用或没有可映射的单次选项', requires: '本次动作的有效审批' });
121
229
  return { outcome: { outcome: 'cancelled' } };
122
230
  })
@@ -126,6 +234,20 @@ export class AcpSession {
126
234
  })
127
235
  .onNotification(acp.methods.client.elicitation.complete, () => {
128
236
  this.need({ code: 'INTERACTION_UNSUPPORTED', message: '首版不支持:远端提问完成通知;未提交答案', requires: '可承载此提问的官方交互接口' });
237
+ })
238
+ .onRequest('_aw/employee-exec', (params) => params, async ({ params, signal }) => {
239
+ if (!this.employeeNegotiated || !this.employeeMeta || !this.hooks.employeeExecution)
240
+ throw new acp.RequestError(-32601, 'EMPLOYEE_EXECUTION_UNSUPPORTED');
241
+ try {
242
+ return await this.hooks.employeeExecution(params, signal);
243
+ }
244
+ catch (error) {
245
+ if (error instanceof NeedError) {
246
+ this.need(error);
247
+ throw new acp.RequestError(-32603, error.code);
248
+ }
249
+ throw error;
250
+ }
129
251
  })
130
252
  .onRequest('*', ({ method, params }) => {
131
253
  this.detail({ kind: 'extension', severity: 'action-required', code: 'ACP_UNKNOWN_REQUEST', summary: '未知远端交互请求未获授权', method, payload: params });
@@ -136,14 +258,14 @@ export class AcpSession {
136
258
  this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_NOTIFICATION', summary: '收到未知远端扩展通知,已保留技术记录', method, payload: params });
137
259
  });
138
260
  if ('readable' in transport && 'writable' in transport)
139
- this.connection = app.connect(this.observe(transport));
261
+ this.connection = app.connect(isolateRpcIds(this.observe(transport)));
140
262
  else {
141
263
  // Stable and unstable apps use distinct private symbols. Bridge only their
142
264
  // public stream interfaces so either SDK entry point remains supported.
143
265
  const outward = new TransformStream();
144
266
  const inward = new TransformStream();
145
267
  const peer = transport.connect({ readable: outward.readable, writable: inward.writable });
146
- this.connection = app.connect(this.observe({ readable: inward.readable, writable: outward.writable }));
268
+ this.connection = app.connect(isolateRpcIds(this.observe({ readable: inward.readable, writable: outward.writable })));
147
269
  void this.connection.closed.then(() => peer.close());
148
270
  void peer.closed.then(() => this.connection.close());
149
271
  }
@@ -155,6 +277,13 @@ export class AcpSession {
155
277
  const raw = record(message);
156
278
  const params = record(raw?.params);
157
279
  const update = record(params?.update);
280
+ if (raw)
281
+ this.hooks.broker?.processedMessage(raw);
282
+ if (typeof raw?.method === 'string' && Object.hasOwn(raw, 'id') && this.hooks.broker && !this.hooks.broker.promptInFlight && !(raw.method === '_aw/employee-exec' && this.hooks.employeeExecution && this.employeeMeta)) {
283
+ const error = new NeedError('RECOVERY_UNSAFE_OUTPUT', '主机会话有不属于当前输入的旧审批,未自动回答', '安全结束旧会话并新建');
284
+ this.hooks.broker.fail(error);
285
+ throw error;
286
+ }
158
287
  if (raw?.method === acp.methods.client.session.update && typeof update?.sessionUpdate === 'string' && !knownUpdates.has(update.sessionUpdate)) {
159
288
  this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_UPDATE', summary: '收到未知远端会话事件,已保留结构', method: 'session/update', payload: params });
160
289
  return;
@@ -166,6 +295,35 @@ export class AcpSession {
166
295
  get id() { return this.sessionId; }
167
296
  get capabilities() { return this.negotiated; }
168
297
  get startMethod() { return this.method; }
298
+ get checkpoint() { return this.ready && !this.busy && !this.updating && !this.selecting && this.sessionId ? { sessionId: this.sessionId, capabilities: recoveryCapabilities(this.negotiated), ...(this.configuration ? { modelConfiguration: { configOptions: JSON.parse(JSON.stringify(this.configuration.snapshot.configOptions)), ...(this.resolvedEvidence ? { resolvedEvidence: { ...this.resolvedEvidence } } : {}) } } : {}) } : undefined; }
299
+ restore(cwd, saved, context, verifiedSameContext = false) {
300
+ if (context.employeeRouting)
301
+ throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具通道需要重新建立,旧审批没有代答。', '按原主机实例安全结束后新建本机会话');
302
+ if (this.startAttempted)
303
+ throw new NeedError('SESSION_ALREADY_STARTED', '此客户端已建立会话', '新的客户端连接');
304
+ this.startAttempted = true;
305
+ this.sessionId = saved.sessionId;
306
+ this.negotiated = recoveryCapabilities(saved.capabilities);
307
+ this.cwd = cwd;
308
+ this.systemPrompt = context.systemPrompt ?? '';
309
+ this.codexNewSessionOnly = !!context.codexNewSessionOnly;
310
+ // A verified same-context reattach does not create or update native context.
311
+ // Old journals may lack new-only metadata; never manufacture it for them.
312
+ if (this.codexNewSessionOnly && !verifiedSameContext)
313
+ this.validateNewContext(this.systemPrompt, context.systemPrompt !== undefined);
314
+ if (saved.modelConfiguration) {
315
+ this.receiveConfiguration(saved.modelConfiguration.configOptions);
316
+ this.resolvedEvidence = saved.modelConfiguration.resolvedEvidence;
317
+ for (const update of saved.configUpdates ?? []) {
318
+ this.resolvedEvidence = undefined;
319
+ this.receiveConfiguration(update);
320
+ }
321
+ }
322
+ this.hooks.broker?.finishRecovery();
323
+ this.detail({ kind: 'need', severity: 'info', code: 'ACP_CLIENT_REATTACHED', summary: '已接回原模型会话', payload: { sessionId: saved.sessionId } });
324
+ this.checkReporting();
325
+ this.ready = true;
326
+ }
169
327
  validatePrompt(prompt) {
170
328
  const capability = this.negotiated.promptCapabilities;
171
329
  for (const block of prompt)
@@ -175,55 +333,86 @@ export class AcpSession {
175
333
  validateContext(context) {
176
334
  if ((context.systemPrompt ?? '') === this.systemPrompt)
177
335
  return;
336
+ if (this.codexNewSessionOnly)
337
+ throw new NeedError('SYSTEM_PROMPT_NEW_SESSION_REQUIRED', 'Codex会话要求已变化,需要保留原会话并新建会话承接', '新会话及完整公开历史承接');
178
338
  if (!this.negotiated.sessionCapabilities?.systemPrompt)
179
339
  throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文更新能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
180
340
  if (!this.negotiated.sessionCapabilities?.resume && !this.negotiated.loadSession)
181
341
  throw new NeedError('SYSTEM_PROMPT_UPDATE_UNSUPPORTED', '远端没有可更新原会话上下文的恢复或加载能力', '支持带 systemPrompt 的 session/resume 或 session/load');
182
342
  }
343
+ validateNewSessionContext(context) {
344
+ if (!this.codexNewSessionOnly)
345
+ throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '此会话没有使用Codex新建政策', 'Codex新建政策');
346
+ this.validateNewContext(context.systemPrompt ?? '', true);
347
+ }
183
348
  setHooks(hooks) {
184
- if (this.busy || this.updating)
185
- throw new NeedError('TURN_BUSY', '上一轮或上下文更新结束前不能更换会话钩子', '上一轮结束');
349
+ if (this.busy || this.updating || this.selecting)
350
+ throw new NeedError('TURN_BUSY', '上一轮或配置更新结束前不能更换会话钩子', '上一轮结束');
186
351
  this.hooks = hooks;
187
352
  }
188
353
  get closed() { return this.connection.closed; }
189
- async start(cwd, resumeId, signal, context) {
354
+ validateNewContext(text, requirePolicy = false) {
355
+ if ((text || requirePolicy) && !this.negotiated.sessionCapabilities?.systemPrompt)
356
+ throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
357
+ if (this.codexNewSessionOnly && (text || requirePolicy)) {
358
+ if (!supportsCodexContextPolicy(this.negotiated))
359
+ throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '远端没有明确声明Codex上下文仅新建生效的兼容政策,本轮未发送', 'systemPrompt与精确的aiWorkbenchSystemPrompt新建政策');
360
+ }
361
+ }
362
+ async start(cwd, resumeId, signal, context, prompt) {
190
363
  if (this.startAttempted)
191
364
  throw new NeedError('SESSION_ALREADY_STARTED', '此连接已用于建立会话,不能转用工作区或身份', '新的独立远端连接');
192
365
  this.startAttempted = true;
366
+ this.employeeMeta = context?.employeeExecution;
193
367
  const abort = () => this.connection.close(new NeedError('ABORTED', '会话建立已取消', '重新连接主机'));
194
368
  signal?.throwIfAborted();
195
369
  signal?.addEventListener('abort', abort, { once: true });
196
370
  try {
197
- const hello = await this.connection.agent.request(acp.methods.agent.initialize, {
198
- protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.1' },
199
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} } },
200
- }, { cancellationSignal: signal });
371
+ const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
372
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.10' },
373
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1 } } } : {}) },
374
+ }, { cancellationSignal: signal }));
201
375
  this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
202
376
  if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
203
377
  throw new NeedError('ACP_VERSION_MISMATCH', 'ACP 协议版本不兼容', '支持 ACP v1 的远端 agent');
204
378
  this.checkReporting();
205
379
  this.negotiated = hello.agentCapabilities ?? {};
206
- const systemPrompt = context?.systemPrompt;
207
- if (systemPrompt && !this.negotiated.sessionCapabilities?.systemPrompt) {
208
- throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
380
+ if (this.employeeMeta) {
381
+ const policy = record(hello._meta?.awEmployeeExecution);
382
+ if (!this.hooks.employeeExecution || !policy || Object.keys(policy).length !== 5 || policy.version !== 1 || policy.nativeCliVersion !== '0.160.1' || policy.native !== true || policy.perRpcGate !== true || policy.noHostFallback !== true)
383
+ throw new NeedError('EMPLOYEE_EXECUTION_UNSUPPORTED', '当前主机尚不支持在你的电脑执行Codex原生工具,本轮没有执行。', 'Codex员工端原生执行适配器与明确能力声明');
384
+ this.employeeNegotiated = true;
385
+ this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_SELECTED', summary: '文件和命令在员工电脑的选定目录执行,Git使用本机仓库;模型、登录和全局助手要求仍在主机。本机MCP、hooks、plugins、冷恢复、快捷命令和主机文件报告尚不支持。外层ACP冷恢复已拒绝;原生子代理回收/缺少存活父会话时的恢复仅做源码检查,未实测。', payload: { bindingId: this.employeeMeta.bindingId, workspaceId: this.employeeMeta.workspaceId, cwd: this.employeeMeta.cwd, nativeCliVersion: '0.160.1', noHostFallback: true, agentConfigSource: 'host CODEX_HOME', unsupportedLocalFeatures: ['mcp', 'hooks', 'plugins', 'cold-resume', 'slash-commands', 'host-file-reports'] } });
209
386
  }
210
- const params = { cwd, mcpServers: [], ...(systemPrompt ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text: systemPrompt }] } } : {}) };
387
+ const systemPrompt = context?.systemPrompt;
388
+ this.codexNewSessionOnly = !!context?.codexNewSessionOnly;
389
+ this.validateNewContext(systemPrompt ?? '', this.codexNewSessionOnly && systemPrompt !== undefined);
390
+ if (prompt)
391
+ this.validatePrompt(prompt);
392
+ if (context?.employeeRouting && resumeId)
393
+ throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具连接已变化,不能沿用旧会话。', '安全结束原主机实例并重新建立本机工具通道');
394
+ const routing = context?.employeeRouting;
395
+ const params = { cwd, mcpServers: routing ? structuredClone(routing.mcpServers) : [], ...((routing || this.employeeMeta) ? { _meta: { ...(this.employeeMeta ? { awEmployeeExecution: this.employeeMeta } : {}), ...(routing ? { claudeCode: { options: { toolAliases: structuredClone(routing.toolAliases) } } } : {}) } } : {}), ...(systemPrompt && !(resumeId && this.codexNewSessionOnly) ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text: systemPrompt }] } } : {}) };
396
+ const configRevision = this.resetConfiguration();
211
397
  if (resumeId) {
212
398
  this.sessionId = resumeId;
213
399
  if (this.negotiated.sessionCapabilities?.resume) {
214
- await this.connection.agent.request(acp.methods.agent.session.resume, { ...params, sessionId: resumeId }, { cancellationSignal: signal });
400
+ const result = await this.stage('acp-session-resume', () => this.connection.agent.request(acp.methods.agent.session.resume, { ...params, sessionId: resumeId }, { cancellationSignal: signal }));
401
+ this.lifecycleConfiguration(result, configRevision);
215
402
  this.method = acp.methods.agent.session.resume;
216
403
  }
217
404
  else {
218
405
  if (!this.negotiated.loadSession)
219
406
  throw new NeedError('RESUME_UNSUPPORTED', '远端未声明会话恢复或加载能力,不能恢复原会话', '支持 session/resume 或 session/load 的 agent');
220
- await this.connection.agent.request(acp.methods.agent.session.load, { ...params, sessionId: resumeId }, { cancellationSignal: signal });
407
+ const result = await this.stage('acp-session-load', () => this.connection.agent.request(acp.methods.agent.session.load, { ...params, sessionId: resumeId }, { cancellationSignal: signal }));
408
+ this.lifecycleConfiguration(result, configRevision);
221
409
  this.method = acp.methods.agent.session.load;
222
410
  }
223
411
  }
224
412
  else {
225
- const result = await this.connection.agent.request(acp.methods.agent.session.new, params, { cancellationSignal: signal });
413
+ const result = await this.stage('acp-session-new', () => this.connection.agent.request(acp.methods.agent.session.new, params, { cancellationSignal: signal }));
226
414
  this.sessionId = result.sessionId;
415
+ this.lifecycleConfiguration(result, configRevision);
227
416
  this.method = acp.methods.agent.session.new;
228
417
  }
229
418
  this.hooks.broker?.acknowledge(this.method);
@@ -236,10 +425,59 @@ export class AcpSession {
236
425
  catch (error) {
237
426
  if (signal?.aborted)
238
427
  throw new NeedError('ABORTED', '会话建立已取消;尚无本轮输出', '重新连接主机');
428
+ if (error instanceof ModelConfigurationError) {
429
+ this.checkReporting();
430
+ throw error;
431
+ }
432
+ this.checkReporting();
433
+ throw acpNeed(error);
434
+ }
435
+ finally {
436
+ signal?.removeEventListener('abort', abort);
437
+ }
438
+ }
439
+ /** Codex context is immutable after creation; keep the old remote session intact. */
440
+ async newContextSession(context, signal) {
441
+ this.checkReporting();
442
+ if (!this.ready || !this.sessionId || !this.cwd)
443
+ throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
444
+ if (!this.codexNewSessionOnly)
445
+ throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '此会话没有使用Codex新建政策', 'Codex新建政策');
446
+ if (this.busy || this.updating || this.selecting)
447
+ throw new NeedError('TURN_BUSY', '上一项会话操作尚未完成', '上一项操作结束');
448
+ const text = context.systemPrompt ?? '';
449
+ this.validateNewSessionContext(context);
450
+ signal?.throwIfAborted();
451
+ const oldId = this.sessionId;
452
+ this.updating = true;
453
+ this.ready = false;
454
+ this.creatingReplacement = true;
455
+ this.pendingConfigurations.clear();
456
+ const abort = () => this.connection.close(new NeedError('ABORTED', '新会话建立已取消,远端状态需确认', '管理员核对远端状态'));
457
+ signal?.addEventListener('abort', abort, { once: true });
458
+ try {
459
+ const revision = this.resetConfiguration();
460
+ const result = await this.stage('acp-session-new', () => this.connection.agent.request(acp.methods.agent.session.new, { cwd: this.cwd, mcpServers: [], ...(this.employeeMeta ? { _meta: { awEmployeeExecution: this.employeeMeta } } : {}), ...(text ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text }] } } : {}) }, { cancellationSignal: signal }));
461
+ if (!result.sessionId || result.sessionId === oldId)
462
+ throw new NeedError('SESSION_STATE_INVALID', '新会话没有返回独立的远端标识,原会话状态需确认', '独立的新远端会话标识');
463
+ this.sessionId = result.sessionId;
464
+ this.lifecycleConfiguration(result, revision);
465
+ this.hooks.broker?.acknowledge(acp.methods.agent.session.new);
466
+ this.checkReporting();
467
+ this.systemPrompt = text;
468
+ this.method = acp.methods.agent.session.new;
469
+ this.ready = true;
470
+ return this.sessionId;
471
+ }
472
+ catch (error) {
239
473
  this.checkReporting();
474
+ if (error instanceof ModelConfigurationError)
475
+ throw error;
240
476
  throw acpNeed(error);
241
477
  }
242
478
  finally {
479
+ this.creatingReplacement = false;
480
+ this.updating = false;
243
481
  signal?.removeEventListener('abort', abort);
244
482
  }
245
483
  }
@@ -247,8 +485,8 @@ export class AcpSession {
247
485
  this.checkReporting();
248
486
  if (!this.ready || !this.sessionId || !this.cwd)
249
487
  throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
250
- if (this.busy || this.updating)
251
- throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新上下文', '上一轮结束');
488
+ if (this.busy || this.updating || this.selecting)
489
+ throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新配置', '上一轮结束');
252
490
  const text = context.systemPrompt ?? '';
253
491
  if (text === this.systemPrompt)
254
492
  return undefined;
@@ -262,7 +500,9 @@ export class AcpSession {
262
500
  try {
263
501
  const method = resume ? acp.methods.agent.session.resume : acp.methods.agent.session.load;
264
502
  const params = { sessionId: this.sessionId, cwd: this.cwd, mcpServers: [], systemPrompt: { mode: 'append', content: [{ type: 'text', text }] } };
265
- await this.connection.agent.request(method, params, { cancellationSignal: signal });
503
+ const configRevision = this.resetConfiguration();
504
+ const result = await this.stage(resume ? 'acp-session-resume' : 'acp-session-load', () => this.connection.agent.request(method, params, { cancellationSignal: signal }));
505
+ this.lifecycleConfiguration(result, configRevision);
266
506
  this.hooks.broker?.acknowledge(method);
267
507
  this.systemPrompt = text;
268
508
  this.ready = true;
@@ -273,6 +513,10 @@ export class AcpSession {
273
513
  catch (error) {
274
514
  if (signal?.aborted)
275
515
  throw new NeedError('ABORTED', '上下文更新已取消;原会话状态需确认', '重新连接原会话');
516
+ if (error instanceof ModelConfigurationError) {
517
+ this.checkReporting();
518
+ throw error;
519
+ }
276
520
  this.checkReporting();
277
521
  throw acpNeed(error);
278
522
  }
@@ -284,12 +528,14 @@ export class AcpSession {
284
528
  async prompt(input, signal) {
285
529
  if (!this.ready || !this.sessionId)
286
530
  throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
287
- if (this.busy || this.updating)
288
- throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮', '上一轮结束');
531
+ if (this.busy || this.updating || this.selecting)
532
+ throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新配置', '上一轮结束');
289
533
  signal?.throwIfAborted();
290
534
  const prompt = typeof input === 'string' ? [{ type: 'text', text: input }] : input;
291
535
  this.validatePrompt(prompt);
292
536
  this.checkReporting();
537
+ if (this.selected)
538
+ this.assertModelSelection(this.selected.model, this.selected.effort);
293
539
  this.busy = true;
294
540
  this.turnSignal = signal;
295
541
  this.turnToken = Symbol('turn');
@@ -303,8 +549,10 @@ export class AcpSession {
303
549
  };
304
550
  signal?.addEventListener('abort', cancel, { once: true });
305
551
  try {
306
- const request = this.connection.agent.request(acp.methods.agent.session.prompt, { sessionId: this.sessionId, prompt }, { cancellationSignal: signal });
307
- const result = await (signal ? Promise.race([request, interrupted]) : request);
552
+ const result = await this.stage('acp-prompt', () => {
553
+ const request = this.connection.agent.request(acp.methods.agent.session.prompt, { sessionId: this.sessionId, prompt }, { cancellationSignal: signal });
554
+ return signal ? Promise.race([request, interrupted]) : request;
555
+ });
308
556
  this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
309
557
  signal?.throwIfAborted();
310
558
  this.hooks.broker?.completePrompt();
@@ -330,6 +578,17 @@ export class AcpSession {
330
578
  export function acpNeed(error) {
331
579
  if (error instanceof NeedError)
332
580
  return error;
581
+ if (error instanceof acp.RequestError) {
582
+ // Host SDK1.9 uses Internal error + data.details for thrown adapter errors.
583
+ // Only fixed codes cross this boundary; arbitrary detail strings are never copied.
584
+ for (const value of [error.message, record(error.data)?.details]) {
585
+ if (typeof value !== 'string')
586
+ continue;
587
+ const code = value.split(':', 1)[0];
588
+ if (employeeAdapterCodes.has(code))
589
+ return new NeedError(code, employeeFailure(code), `员工电脑执行适配器明确拒绝:${code};RPC 错误码 ${error.code}`);
590
+ }
591
+ }
333
592
  if (error instanceof acp.RequestError && error.code === -32000)
334
593
  return new NeedError('LOGIN_REQUIRED', '需要登录,请到设置 → 远程主机连接 点登录', '该员工自己的远端模型登录');
335
594
  if (error instanceof acp.RequestError)
@@ -339,3 +598,4 @@ export function acpNeed(error) {
339
598
  function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; }
340
599
  const knownUpdates = new Set(['user_message_chunk', 'agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan', 'plan_update', 'plan_removed', 'available_commands_update', 'current_mode_update', 'config_option_update', 'session_info_update', 'usage_update', 'notice', 'compaction_update', 'compaction_summary_chunk', 'subagent_update', 'session_message', 'session_message_chunk']);
341
600
  function safeLabel(value) { return value.replaceAll(/[\r\n\x00-\x1f\x7f]/g, ' '); }
601
+ const employeeAdapterCodes = new Set(["AW_EMPLOYEE_ADDITIONAL_DIRECTORIES_UNSUPPORTED", "AW_EMPLOYEE_SLASH_COMMAND_UNSUPPORTED", "AW_EMPLOYEE_HOST_FILE_REPORT_UNSUPPORTED", "AW_EMPLOYEE_CONFIG_DISCOVERY_OVERRIDE_UNSUPPORTED", "AW_EMPLOYEE_CONFIG_ADMISSION_INVALID", "AW_EMPLOYEE_CONFIG_ADMISSION_FAILED", "AW_EMPLOYEE_HOST_PROJECT_CONFIG_UNSUPPORTED", "AW_EMPLOYEE_EXECUTOR_MCP_UNSUPPORTED", "AW_EMPLOYEE_EXECUTOR_CONFIG_PROTOCOL_INVALID", "AW_EMPLOYEE_EXECUTOR_CONFIG_READ_FAILED", "AW_EMPLOYEE_AGENT_CONFIG_UNAVAILABLE", "AW_EMPLOYEE_BINDING_INVALID", "AW_EMPLOYEE_BINDING_MISMATCH", "AW_EMPLOYEE_CAPABILITY_REQUIRED", "AW_EMPLOYEE_CLOSE_FAILED", "AW_EMPLOYEE_CLOSE_RESPONSE_INVALID", "AW_EMPLOYEE_COLD_RESUME_UNSUPPORTED", "AW_EMPLOYEE_CWD_MISMATCH", "AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED", "AW_EMPLOYEE_ENVIRONMENT_REQUIRED", "AW_EMPLOYEE_EXECUTION_CLOSED", "AW_EMPLOYEE_EXECUTION_ID_REQUIRED", "AW_EMPLOYEE_HOST_AUXILIARY_CONFIG_UNSUPPORTED", "AW_EMPLOYEE_HOST_AUXILIARY_UNSUPPORTED", "AW_EMPLOYEE_HOST_FALLBACK_FORBIDDEN", "AW_EMPLOYEE_MCP_UNSUPPORTED", "AW_EMPLOYEE_MESSAGE_INVALID", "AW_EMPLOYEE_MESSAGE_TOO_LARGE", "AW_EMPLOYEE_OPEN_RESPONSE_INVALID", "AW_EMPLOYEE_READ_MESSAGE_INVALID", "AW_EMPLOYEE_READ_RESPONSE_INVALID", "AW_EMPLOYEE_RELAY_ADDRESS_INVALID", "AW_EMPLOYEE_RELAY_FAILED", "AW_EMPLOYEE_REQUEST_INVALID", "AW_EMPLOYEE_SESSION_UNKNOWN", "AW_EMPLOYEE_WIRE_FAILED", "AW_EMPLOYEE_WRITE_RESPONSE_INVALID"]);
@@ -0,0 +1,6 @@
1
+ import type { RequestPermissionRequest } from '@agentclientprotocol/sdk/unstable';
2
+ /** ACP diff.oldText=null means a new file. Write alone may overwrite a file. */
3
+ export declare function approvalLabel(request: RequestPermissionRequest, location?: 'host' | 'local'): {
4
+ toolName: string;
5
+ reason: string;
6
+ } | undefined;
@@ -0,0 +1,57 @@
1
+ import { sanitizeDisplayLabel } from "./conversation-details.js";
2
+ function display(value) {
3
+ const safe = sanitizeDisplayLabel(sanitizeDisplayLabel(value).replace(/[\u202a-\u202e\u2066-\u2069]/g, '')).trim();
4
+ // Redacted instructions/targets cannot support an informed approval.
5
+ return safe && !safe.includes('[已脱敏:') ? safe : undefined;
6
+ }
7
+ /** ACP diff.oldText=null means a new file. Write alone may overwrite a file. */
8
+ export function approvalLabel(request, location = 'host') {
9
+ const tool = request.toolCall;
10
+ const diffs = tool.content?.filter(item => item.type === 'diff') ?? [];
11
+ const originalName = tool.name ?? tool._meta?.claudeCode?.toolName ?? /^(Write|Edit|MultiEdit) /.exec(tool.title ?? '')?.[1];
12
+ const employee = location === 'local' && originalName?.startsWith('mcp__employee__');
13
+ const name = employee ? originalName.slice('mcp__employee__'.length) : originalName;
14
+ const fileAction = diffs.length > 0 || name === 'Write' || name === 'Edit' || name === 'MultiEdit';
15
+ const paths = diffs.length ? diffs.map(diff => diff.path) : tool.locations?.map(location => location.path) ?? [];
16
+ let title = tool.title ?? '';
17
+ const raw = employee && tool.rawInput && typeof tool.rawInput === 'object' && !Array.isArray(tool.rawInput) ? tool.rawInput : undefined;
18
+ if (employee && raw) {
19
+ if ((fileAction || name === 'Read') && !paths.length && typeof raw.file_path === 'string')
20
+ paths.push(raw.file_path);
21
+ if ((name === 'Glob' || name === 'Grep') && !paths.length)
22
+ paths.push(typeof raw.path === 'string' ? raw.path : '当前文件夹');
23
+ title = typeof raw.description === 'string' ? raw.description : typeof raw.command === 'string' ? raw.command : '';
24
+ if ((name === 'Glob' || name === 'Grep') && typeof raw.pattern === 'string')
25
+ title = raw.pattern;
26
+ }
27
+ if (name && title.startsWith(name + ' '))
28
+ title = title.slice(name.length + 1);
29
+ else if (title === name || title === tool.toolCallId)
30
+ title = '';
31
+ if (fileAction) {
32
+ const action = diffs.length && diffs.every(diff => diff.oldText === null) ? '新建文件'
33
+ : (diffs.length && diffs.every(diff => typeof diff.oldText === 'string')) || name === 'Edit' || name === 'MultiEdit' ? '修改文件' : '写入文件';
34
+ // Observed Write/Edit title is a display hint, never used as a host path.
35
+ if (!paths.length && name && ['Write', 'Edit', 'MultiEdit'].includes(name) && title)
36
+ paths.push(title);
37
+ const unique = [...new Set(paths)];
38
+ const safe = unique.map(display);
39
+ if (!safe.length || safe.some(path => path === undefined) || new Set(safe).size !== safe.length)
40
+ return;
41
+ const names = safe.map(path => path.split(/[\\/]/).at(-1)?.trim());
42
+ if (names.some(name => !name))
43
+ return;
44
+ const files = safe.map((path, index) => names.filter(name => name === names[index]).length > 1 ? path : names[index]);
45
+ return { toolName: action, reason: action + ' ' + files.join('、') };
46
+ }
47
+ const action = (employee ? { Read: '读取文件', Glob: '查找文件', Grep: '搜索内容', Bash: '执行本机命令' }[name] : undefined) ?? { read: '读取文件', edit: '修改文件', delete: '删除文件', move: '移动文件', search: '搜索', execute: employee ? '执行本机命令' : '执行主机命令', think: '分析', fetch: '获取内容', other: '远程操作' }[tool.kind ?? ''] ?? '远程操作';
48
+ const safePaths = paths.map(display);
49
+ if (safePaths.some(path => path === undefined))
50
+ return;
51
+ const description = title ? display(title) : undefined;
52
+ if (title && !description)
53
+ return;
54
+ if (!safePaths.length && !description)
55
+ return;
56
+ return { toolName: action, reason: action + (safePaths.length ? ' ' + safePaths.join('、') : '') + (description ? ':' + description : '') };
57
+ }
@@ -0,0 +1,10 @@
1
+ import { type SshArgv, type BrokerIo, type BrokerSettings } from './broker.ts';
2
+ import { type ConnectApi, type ConnectTarget, type Project, type Runtime, type RuntimeRequest } from './provider.ts';
3
+ import type { Interaction } from './interaction.ts';
4
+ import { type RecoveryJournal } from './recovery.ts';
5
+ /** Test ports are explicit; deployed code always supplies the official-profile journal. */
6
+ export declare function brokerFactory(get: () => ConnectApi | undefined, _ask: Interaction | undefined, configuration: BrokerSettings | (() => BrokerSettings), provided?: {
7
+ argv?: SshArgv;
8
+ io?: (target: ConnectTarget) => BrokerIo;
9
+ journal?: (key: string) => RecoveryJournal;
10
+ }): (target: ConnectTarget, project: Project, initialRequest: RuntimeRequest) => Promise<Runtime>;