@ai-workbench/remote-agent 0.8.19 → 0.8.23

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 (95) hide show
  1. package/README.md +27 -134
  2. package/dist/acp.d.ts +23 -16
  3. package/dist/acp.js +209 -226
  4. package/dist/agents.d.ts +0 -1
  5. package/dist/agents.js +2 -2
  6. package/dist/broker-runtime.d.ts +3 -5
  7. package/dist/broker-runtime.js +45 -342
  8. package/dist/broker.d.ts +6 -31
  9. package/dist/broker.js +42 -113
  10. package/dist/catalog-ssh.d.ts +2 -31
  11. package/dist/catalog-ssh.js +9 -39
  12. package/dist/claude-local.d.ts +1 -1
  13. package/dist/claude-local.js +41 -105
  14. package/dist/client.js +34 -126
  15. package/dist/close-diagnostics.d.ts +6 -20
  16. package/dist/close-diagnostics.js +13 -57
  17. package/dist/connect-contract.d.ts +4 -8
  18. package/dist/conversation-details.d.ts +0 -29
  19. package/dist/conversation-details.js +8 -99
  20. package/dist/employee-endpoint.d.ts +1 -1
  21. package/dist/employee-endpoint.js +21 -19
  22. package/dist/employee-exec.d.ts +4 -3
  23. package/dist/employee-exec.js +25 -13
  24. package/dist/employee-info.d.ts +7 -1
  25. package/dist/employee-info.js +24 -3
  26. package/dist/employee-location.js +3 -4
  27. package/dist/employee-mcp.js +44 -69
  28. package/dist/employee-native.d.ts +1 -3
  29. package/dist/employee-native.js +6 -27
  30. package/dist/employee-presentation.d.ts +0 -5
  31. package/dist/employee-presentation.js +0 -15
  32. package/dist/employee-provider.d.ts +3 -11
  33. package/dist/employee-provider.js +69 -205
  34. package/dist/employee-reasons.js +1 -5
  35. package/dist/employee-shell.d.ts +2 -1
  36. package/dist/employee-shell.js +10 -13
  37. package/dist/employee-ssh.d.ts +1 -3
  38. package/dist/employee-ssh.js +13 -16
  39. package/dist/employee-tools.d.ts +5 -5
  40. package/dist/failure-evidence.d.ts +2 -10
  41. package/dist/failure-evidence.js +12 -61
  42. package/dist/host-workspace.d.ts +4 -2
  43. package/dist/host-workspace.js +6 -2
  44. package/dist/index.d.ts +13 -43
  45. package/dist/index.js +192 -484
  46. package/dist/input.js +2 -3
  47. package/dist/local-user-turn.js +5 -6
  48. package/dist/mapping.d.ts +5 -3
  49. package/dist/mapping.js +31 -36
  50. package/dist/model-catalog.d.ts +2 -41
  51. package/dist/model-catalog.js +24 -282
  52. package/dist/model-configuration.d.ts +0 -3
  53. package/dist/model-configuration.js +8 -47
  54. package/dist/model-presentation.d.ts +1 -0
  55. package/dist/model-presentation.js +2 -2
  56. package/dist/permissions.d.ts +15 -0
  57. package/dist/permissions.js +30 -0
  58. package/dist/preflight-diagnostic.js +11 -22
  59. package/dist/preflight-payload.d.ts +1 -1
  60. package/dist/preflight-payload.js +2 -2
  61. package/dist/presentation.js +21 -41
  62. package/dist/provider.d.ts +12 -34
  63. package/dist/provider.js +17 -92
  64. package/dist/readiness.js +1 -7
  65. package/dist/remote-details.d.ts +41 -17
  66. package/dist/remote-details.js +84 -381
  67. package/dist/workspace-source.d.ts +1 -25
  68. package/dist/workspace-source.js +1 -138
  69. package/package.json +11 -12
  70. package/dist/catalog-diagnostics.d.ts +0 -16
  71. package/dist/catalog-diagnostics.js +0 -108
  72. package/dist/host-layout.d.ts +0 -8
  73. package/dist/host-layout.js +0 -9
  74. package/dist/job.d.ts +0 -17
  75. package/dist/job.js +0 -57
  76. package/dist/model-defaults.d.ts +0 -29
  77. package/dist/model-defaults.js +0 -62
  78. package/dist/oneshot.d.ts +0 -13
  79. package/dist/oneshot.js +0 -78
  80. package/dist/protocol-boundary.d.ts +0 -13
  81. package/dist/protocol-boundary.js +0 -49
  82. package/dist/provider-locale.d.ts +0 -19
  83. package/dist/provider-locale.js +0 -73
  84. package/dist/recovery-checkpoint.d.ts +0 -6
  85. package/dist/recovery-checkpoint.js +0 -71
  86. package/dist/recovery.d.ts +0 -51
  87. package/dist/recovery.js +0 -61
  88. package/dist/sdk-ssh-wrapper.d.ts +0 -2
  89. package/dist/sdk-ssh-wrapper.js +0 -34
  90. package/dist/stage-timing.d.ts +0 -31
  91. package/dist/stage-timing.js +0 -61
  92. package/dist/workspace-notice.d.ts +0 -47
  93. package/dist/workspace-notice.js +0 -273
  94. package/dist/workspace-source-confirmation.d.ts +0 -4
  95. package/dist/workspace-source-confirmation.js +0 -12
package/dist/acp.js CHANGED
@@ -1,14 +1,14 @@
1
- import { employeeExecutionInfo, employeeMcpSummary } from "./employee-info.js";
1
+ import { employeeExecutionInfo, employeeMcpSummary, employeeHttpNotice } from "./employee-info.js";
2
2
  import { employeeAdapterCodes, employeeOpenCauseCodes } from "./employee-reasons.js";
3
- import { CloseDiagnostics, withFailureEvidence } from "./close-diagnostics.js";
3
+ import { CloseDiagnostics, withFirstFailureEvidence } from "./close-diagnostics.js";
4
4
  import * as acp from '@agentclientprotocol/sdk/unstable';
5
5
  import { NeedError } from "./remote.js";
6
6
  import { employeeFailure } from "./presentation.js";
7
- import { StageTiming } from "./stage-timing.js";
8
7
  import { isolateRpcIds } from "./rpc-namespace.js";
9
- import { recoveryCapabilities, supportsCodexContextPolicy } from "./recovery.js";
10
- import { sanitizeObservation } from "./conversation-details.js";
8
+ import { sanitizeObservation, sanitizeDisplayLabel } from "./conversation-details.js";
11
9
  import { ModelConfiguration, ModelConfigurationError } from "./model-configuration.js";
10
+ function employeeClaudeOptions(routing) { return { tools: [], disallowedTools: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'Agent', 'Task', 'TaskOutput', 'TaskStop'], toolAliases: structuredClone(routing.toolAliases) }; }
11
+ const permissionRanks = { claude: { plan: 0, default: 1, acceptEdits: 1, bypassPermissions: 2 }, codex: { 'read-only': 0, 'workspace-write': 1, agent: 1, 'agent-full-access': 2 } };
12
12
  /** ACP v1 with explicitly negotiated SDK 2.0 lifecycle/context extensions. */
13
13
  export class AcpSession {
14
14
  connection;
@@ -29,6 +29,7 @@ export class AcpSession {
29
29
  codexNewSessionOnly = false;
30
30
  creatingReplacement = false;
31
31
  employeeMeta;
32
+ employeeRouting;
32
33
  employeeNegotiated = false;
33
34
  configuration;
34
35
  configRevision = 0;
@@ -36,6 +37,29 @@ export class AcpSession {
36
37
  selecting = false;
37
38
  pendingConfigurations = new Map();
38
39
  dispatchedPrompts = 0;
40
+ // The fixed public SDK preserves request params in Stream messages. Keep a
41
+ // permit per object, including while its write is queued after caller abort.
42
+ // No protocol metadata, SDK counters or private connection state is used.
43
+ promptPermissions = new WeakMap();
44
+ nativeModeRevision = 0;
45
+ nativeModeId;
46
+ nativeModeSource;
47
+ selectedPermission;
48
+ validateNativePermission(expected, reported) {
49
+ const actual = this.nativeModeId;
50
+ if (!expected || !actual || actual === expected.mode)
51
+ return;
52
+ const ranks = permissionRanks[expected.brand], before = ranks[expected.mode], after = ranks[actual];
53
+ const wider = before !== undefined && after !== undefined && after > before;
54
+ if (!reported.has(actual)) {
55
+ reported.add(actual);
56
+ this.detail({ kind: 'session', severity: wider ? 'warning' : after === undefined ? 'warning' : 'info', code: wider ? 'PERMISSION_MODE_WIDENED' : 'PERMISSION_MODE_INFORMATION', summary: wider ? '远端权限已超出本会话选择,本次没有继续派发。' : '远端权限模式与原确认值不同,差异已保留,本轮继续。', payload: { brand: expected.brand, requested: expected.mode, actual, source: this.nativeModeSource ?? null } });
57
+ if (wider)
58
+ this.closeDiagnostics.record({ layer: 'acp', initiator: 'acp.native-permission-gate', reason: 'PERMISSION_MODE_CHANGED', operation: 'session/prompt', action: 'failure', changes: [{ field: 'native.permission-mode', before: expected.mode, after: actual }] });
59
+ }
60
+ if (wider)
61
+ throw new NeedError('PERMISSION_MODE_CHANGED', '远端权限已超出本会话选择,本次未发送;具体原因已保留在技术详情。', '当前已选择档位范围内的远端权限');
62
+ }
39
63
  get promptDispatchCount() { return this.dispatchedPrompts; }
40
64
  get modelConfiguration() { return this.configuration?.snapshot; }
41
65
  get models() { if (!this.configuration)
@@ -47,7 +71,6 @@ export class AcpSession {
47
71
  else
48
72
  this.configuration = new ModelConfiguration(options, async (configId, value) => {
49
73
  const result = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId, value }, { cancellationSignal: this.turnSignal });
50
- this.hooks.broker?.acknowledge(acp.methods.agent.session.setConfigOption);
51
74
  return result;
52
75
  }, reason => this.detail({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_INFORMATION', summary: ({ PICKER_INCOMPLETE: '远端菜单没有穷举当前或所选值;以本轮精确配置回执为准。', CONFIG_UI_METADATA_INVALID: '远端配置的显示资料不完整;没有改变本轮选择。', UNRELATED_CONFIG_IGNORED: '远端有未使用的界面配置;未把它当模型配置入口。', OPTIONAL_CATEGORY_OMITTED: '远端省略了显示类别;继续使用本会话已核实的配置入口。', SAME_SELECTION_UPDATE: '收到同值或无关配置更新,本轮真实配置回执仍有效。' })[reason], payload: { reason } }), routing);
53
76
  }
@@ -88,7 +111,6 @@ export class AcpSession {
88
111
  if (this.busy || this.updating || this.selecting)
89
112
  throw new ModelConfigurationError('上一项会话操作尚未完成');
90
113
  signal?.throwIfAborted();
91
- this.checkReporting();
92
114
  this.selecting = true;
93
115
  this.selected = { model, ...(effort === undefined ? {} : { effort }) };
94
116
  this.turnSignal = signal;
@@ -97,7 +119,6 @@ export class AcpSession {
97
119
  throw new ModelConfigurationError('远端没有提供完整配置列表');
98
120
  const state = await this.configuration.select(model, effort);
99
121
  signal?.throwIfAborted();
100
- this.checkReporting();
101
122
  return state;
102
123
  }
103
124
  finally {
@@ -105,16 +126,87 @@ export class AcpSession {
105
126
  this.selecting = false;
106
127
  }
107
128
  }
108
- assertModelSelection(model, effort) {
109
- if (!this.configuration)
110
- throw new ModelConfigurationError('远端没有提供完整配置列表');
111
- this.configuration.assertSelection(model, effort);
129
+ /** The permission selector controls real privilege, independently of model UX.
130
+ * Re-apply on every dispatch/restoration; a historical value is not an ACK.
131
+ * ACP permits a mode change while a prompt is active. */
132
+ async selectPermissionMode(mode, signal, brand) {
133
+ const failed = () => new NeedError('PERMISSION_MODE_UNCONFIRMED', '远程权限模式未能与本会话对齐,本次操作未继续。', '主机ACP的权限配置入口及同值回执');
134
+ if (!this.ready || !this.sessionId)
135
+ throw failed();
136
+ signal?.throwIfAborted();
137
+ if (brand === 'claude') {
138
+ const revision = this.nativeModeRevision;
139
+ let actual, confirmation;
140
+ try {
141
+ // This native setter's successful {} response acknowledges the requested
142
+ // mode. Claude 0.94.0 normally publishes config_option_update; its
143
+ // current_mode_update is conditional, so neither is a new hard gate.
144
+ await this.connection.agent.request(acp.methods.agent.session.setMode, { sessionId: this.sessionId, modeId: mode }, { cancellationSignal: signal });
145
+ signal?.throwIfAborted();
146
+ // Both notification kinds describe one mode. Their arrival order, not
147
+ // their kind, determines the latest native evidence during this setter.
148
+ if (this.nativeModeRevision > revision) {
149
+ actual = this.nativeModeId;
150
+ confirmation = this.nativeModeSource;
151
+ if (actual !== mode)
152
+ throw failed();
153
+ }
154
+ actual ??= mode;
155
+ confirmation ??= 'set_mode-response';
156
+ if (this.nativeModeRevision === revision) {
157
+ this.nativeModeId = actual;
158
+ this.nativeModeSource = 'set_mode-response';
159
+ this.nativeModeRevision++;
160
+ }
161
+ this.selectedPermission = Object.freeze({ brand, mode });
162
+ this.detail({ kind: 'session', severity: 'info', code: 'PERMISSION_MODE_CONFIRMED', summary: '远程主机已确认本轮权限配置;下一次派发使用该档位,本机工具入口核验仍开启。', payload: { method: acp.methods.agent.session.setMode, requested: mode, actual, confirmation } });
163
+ return;
164
+ }
165
+ catch (error) {
166
+ const reasons = error instanceof acp.RequestError ? [error.message, record(error.data)?.details] : [];
167
+ const unavailable = mode === 'bypassPermissions' && reasons.some(reason => typeof reason === 'string' && reason.includes('bypassPermissions') && /not available|not allowed|disabled/i.test(reason));
168
+ const code = unavailable ? 'CLAUDE_BYPASS_UNAVAILABLE' : 'PERMISSION_MODE_UNCONFIRMED';
169
+ this.detail({ kind: 'session', severity: 'warning', code: 'PERMISSION_MODE_FAILED', summary: unavailable ? '主机没有开放完全权限,本轮没有降级到其它档位。' : '远程权限模式未能对齐,未假装放行。', payload: { method: acp.methods.agent.session.setMode, requested: mode, actual: actual ?? null, confirmation: confirmation ?? null, reason: signal?.aborted ? 'ABORTED' : code } });
170
+ if (signal?.aborted)
171
+ signal.throwIfAborted();
172
+ if (unavailable)
173
+ throw new NeedError(code, '主机没有为本会话开放完全权限,本轮未发送;我需要主机允许 bypassPermissions。', '主机本会话的 bypassPermissions 能力');
174
+ throw failed();
175
+ }
176
+ }
177
+ // Both protected native adapters contract the ID "mode". Category is
178
+ // optional picker metadata, so its omission is information, not authority.
179
+ const candidates = this.configuration?.snapshot.configOptions.filter(option => option.category === 'mode' || option.id === 'mode') ?? [];
180
+ if (candidates.length !== 1 || candidates[0].type !== 'select')
181
+ throw failed();
182
+ const configId = candidates[0].id;
183
+ if (candidates[0].category !== 'mode')
184
+ this.detail({ kind: 'session', severity: 'info', code: 'PERMISSION_MODE_INFORMATION', summary: '远端省略了权限菜单类别,继续使用原生合同中的权限配置入口。', payload: { reason: 'OPTIONAL_CATEGORY_OMITTED', configId } });
185
+ if (this.configuration.snapshot.configOptions.filter(option => option.id === configId).length !== 1)
186
+ throw failed();
187
+ try {
188
+ const result = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId, value: mode }, { cancellationSignal: signal });
189
+ this.receiveConfiguration(result.configOptions);
190
+ signal?.throwIfAborted();
191
+ const actual = this.configuration?.snapshot.configOptions.filter(option => option.id === configId);
192
+ if (actual?.length !== 1 || actual[0].type !== 'select' || actual[0].currentValue !== mode)
193
+ throw failed();
194
+ this.nativeModeId = actual[0].currentValue;
195
+ this.nativeModeSource = 'set_config_option-response';
196
+ this.nativeModeRevision++;
197
+ this.selectedPermission = Object.freeze({ brand, mode });
198
+ this.detail({ kind: 'session', severity: 'info', code: 'PERMISSION_MODE_CONFIRMED', summary: '本会话权限配置已同步到远程工具;下一次派发使用该档位,特殊安全审批仍按官方规则处理。', payload: { method: acp.methods.agent.session.setConfigOption, configId, requested: mode, actual: actual[0].currentValue, confirmation: 'set_config_option-response' } });
199
+ }
200
+ catch (error) {
201
+ this.detail({ kind: 'session', severity: 'warning', code: 'PERMISSION_MODE_FAILED', summary: '远程权限模式未能对齐,未假装放行。', payload: { requested: mode, reason: signal?.aborted ? 'ABORTED' : 'PERMISSION_MODE_UNCONFIRMED' } });
202
+ if (signal?.aborted)
203
+ signal.throwIfAborted();
204
+ throw failed();
205
+ }
112
206
  }
113
207
  reports = [];
114
- reportingFailure;
115
- timing = new StageTiming();
116
208
  async stage(phase, operation) { this.closeDiagnostics.stage = phase; try {
117
- return await this.closeDiagnostics.run(phase, () => (this.hooks.timing ?? this.timing).run(phase, operation));
209
+ return await this.closeDiagnostics.run(phase, operation);
118
210
  }
119
211
  catch (error) {
120
212
  if (['acp-session-new', 'acp-session-resume', 'acp-session-load', 'acp-initialize'].includes(phase))
@@ -128,32 +220,34 @@ export class AcpSession {
128
220
  try {
129
221
  hooks.onDetail?.(safe);
130
222
  }
131
- catch (cause) {
132
- const first = !this.reportingFailure;
133
- this.reportingFailure ??= { cause };
134
- if (first)
135
- this.close({ layer: 'acp', initiator: 'acp.reporting-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' });
223
+ catch {
224
+ console.warn('[remote-agent] 会话诊断未保存');
136
225
  }
137
226
  }
227
+ employeeNoticeNames = new Set();
228
+ employeeNoticeUnshown = false;
138
229
  employeeInfo(value) {
139
230
  const info = employeeExecutionInfo(value);
140
231
  if (!info)
141
232
  return;
142
233
  const summary = employeeMcpSummary(info);
143
234
  this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_MCP_INFORMATION', summary: summary || '本机MCP策略已应用:' + info.httpMcpPolicy, payload: info });
144
- if (summary)
145
- this.hooks.onEmployeeInfo?.(summary);
235
+ const notice = employeeHttpNotice({ ...info, ignoredHttp: info.ignoredHttp.filter(name => !this.employeeNoticeNames.has(name)), ignoredNativeHttp: info.ignoredNativeHttp.filter(name => !this.employeeNoticeNames.has(name)), ignoredHttpUnshown: this.employeeNoticeUnshown ? 0 : info.ignoredHttpUnshown, ignoredNativeHttpUnshown: this.employeeNoticeUnshown ? 0 : info.ignoredNativeHttpUnshown }, sanitizeDisplayLabel, !this.employeeNoticeUnshown);
236
+ if (notice) {
237
+ for (const name of [...info.ignoredHttp, ...info.ignoredNativeHttp])
238
+ this.employeeNoticeNames.add(name);
239
+ if (info.ignoredHttpUnshown || info.ignoredNativeHttpUnshown || [...info.ignoredHttp, ...info.ignoredNativeHttp].some(name => sanitizeDisplayLabel(name) !== name || name.includes('[已脱敏:')))
240
+ this.employeeNoticeUnshown = true;
241
+ this.hooks.onEmployeeNotice?.(notice);
242
+ }
146
243
  }
147
244
  need(need, hooks = this.hooks) {
148
245
  this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } }, hooks);
149
246
  hooks.onNeed?.(need);
150
247
  }
151
- checkReporting() { if (this.reportingFailure)
152
- throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能记录;本轮未继续发送', '可用的技术详情记录渠道'); }
153
248
  constructor(transport, hooks) {
154
249
  this.hooks = hooks;
155
250
  this.closeDiagnostics = hooks.closeDiagnostics ?? new CloseDiagnostics(event => this.detail(event));
156
- this.timing.bind(detail => this.detail(detail));
157
251
  const app = acp.client({ name: '@ai-workbench/remote-agent' })
158
252
  .onNotification(acp.methods.client.session.update, ({ params: notification }) => {
159
253
  if (this.creatingReplacement) {
@@ -167,7 +261,18 @@ export class AcpSession {
167
261
  return;
168
262
  }
169
263
  const meta = record(notification._meta);
264
+ if (notification.update.sessionUpdate === 'current_mode_update') {
265
+ this.nativeModeId = notification.update.currentModeId;
266
+ this.nativeModeSource = 'current_mode_update';
267
+ this.nativeModeRevision++;
268
+ }
170
269
  if (notification.update.sessionUpdate === 'config_option_update') {
270
+ const modes = notification.update.configOptions.filter(option => option.id === 'mode');
271
+ if (modes.length === 1 && modes[0].type === 'select') {
272
+ this.nativeModeId = modes[0].currentValue;
273
+ this.nativeModeSource = 'config_option_update';
274
+ this.nativeModeRevision++;
275
+ }
171
276
  if (!this.sessionId)
172
277
  this.pendingConfiguration(notification.sessionId, notification.update.configOptions);
173
278
  else
@@ -181,9 +286,6 @@ export class AcpSession {
181
286
  if (meta && Object.keys(meta).length)
182
287
  this.detail({ kind: 'extension', severity: 'info', code: 'ACP_NOTIFICATION_META', summary: '远端通知扩展', method: 'session/update', payload: notification._meta });
183
288
  if (this.busy) {
184
- // Only actual turn output acknowledges prompt acceptance.
185
- if (['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update'].includes(notification.update.sessionUpdate))
186
- this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
187
289
  this.hooks.onUpdate?.(notification);
188
290
  }
189
291
  else
@@ -193,7 +295,6 @@ export class AcpSession {
193
295
  const hooks = this.hooks, detail = (value) => this.detail(value, hooks), need = (value) => this.need(value, hooks);
194
296
  if (request.sessionId !== this.sessionId || !this.busy || this.turnSignal?.aborted)
195
297
  return { outcome: { outcome: 'cancelled' } };
196
- hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
197
298
  const toolName = request.toolCall.name ?? record(request.toolCall._meta?.claudeCode)?.toolName;
198
299
  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 } : {}) } } });
199
300
  if (toolName === 'AskUserQuestion') {
@@ -216,29 +317,26 @@ export class AcpSession {
216
317
  const token = this.turnToken;
217
318
  const id = this.sessionId;
218
319
  let outcome = 'unavailable';
219
- const approvalSignal = this.turnSignal ? AbortSignal.any([this.turnSignal, rpcSignal]) : rpcSignal;
320
+ const policy = hooks.approvalGuard?.();
321
+ const approvalSignal = AbortSignal.any([rpcSignal, ...this.turnSignal ? [this.turnSignal] : [], ...policy ? [policy.signal] : []]);
220
322
  const broker = hooks.broker;
221
323
  let generation;
222
- // A transport disconnect withdraws only the UI question, not the ACP RPC.
223
- // Late UI answers lose their lease; a fresh online attachment asks again.
324
+ // One question per RPC. A disconnect ends this turn; it is never asked again.
224
325
  try {
225
- while (!approvalSignal.aborted) {
226
- await broker?.waitOnline(approvalSignal);
227
- generation = broker?.generation;
228
- const lease = broker?.signal;
229
- const signal = lease ? AbortSignal.any([approvalSignal, lease]) : approvalSignal;
230
- const interrupted = Promise.withResolvers();
231
- const abort = () => interrupted.resolve('cancelled');
232
- signal.addEventListener('abort', abort, { once: true });
233
- try {
326
+ generation = broker?.generation;
327
+ const lease = broker?.signal;
328
+ const signal = lease ? AbortSignal.any([approvalSignal, lease]) : approvalSignal;
329
+ const interrupted = Promise.withResolvers();
330
+ const abort = () => interrupted.resolve('cancelled');
331
+ signal.addEventListener('abort', abort, { once: true });
332
+ try {
333
+ if (!signal.aborted)
234
334
  outcome = await Promise.race([hooks.approve?.(presented, signal) ?? Promise.resolve('unavailable'), interrupted.promise]);
235
- }
236
- finally {
237
- signal.removeEventListener('abort', abort);
238
- }
239
- if (broker && !approvalSignal.aborted && (generation !== broker.generation || lease?.aborted))
240
- continue;
241
- break;
335
+ else
336
+ outcome = 'cancelled';
337
+ }
338
+ finally {
339
+ signal.removeEventListener('abort', abort);
242
340
  }
243
341
  }
244
342
  catch {
@@ -246,9 +344,26 @@ export class AcpSession {
246
344
  }
247
345
  const kind = outcome === 'allowed-once' ? 'allow_once' : outcome === 'rejected' ? 'reject_once' : undefined;
248
346
  const option = kind && eligible.find(item => item.kind === kind);
249
- if (this.busy && token === this.turnToken && id === this.sessionId && !approvalSignal.aborted && option && (!broker || (broker.online && generation === broker.generation))) {
250
- if (broker)
251
- broker.authorizeResponse(requestId, generation);
347
+ const current = () => {
348
+ if (approvalSignal.aborted)
349
+ return false;
350
+ try {
351
+ return !policy || policy.isCurrent();
352
+ }
353
+ catch {
354
+ detail({ kind: 'tool', severity: 'warning', code: 'APPROVAL_POLICY_CHANGED', summary: '当前权限无法重新核对,原审批没有继续授权。', method: 'session/request_permission', payload: { reason: 'PERMISSION_POLICY_UNAVAILABLE' } });
355
+ return false;
356
+ }
357
+ };
358
+ if (policy && !current())
359
+ detail({ kind: 'tool', severity: 'warning', code: 'APPROVAL_POLICY_CHANGED', summary: '本会话权限已变化,原审批不再授权本次动作。', method: 'session/request_permission', payload: { reason: 'PERMISSION_GENERATION_CHANGED' } });
360
+ if (this.busy && token === this.turnToken && id === this.sessionId && current() && option && (!broker || (broker.online && generation === broker.generation))) {
361
+ if (broker) {
362
+ if (policy)
363
+ broker.authorizeResponse(requestId, generation, current);
364
+ else
365
+ broker.authorizeResponse(requestId, generation);
366
+ }
252
367
  return { outcome: { outcome: 'selected', optionId: option.optionId } };
253
368
  }
254
369
  need({ code: outcome === 'allowed-once' ? 'APPROVAL_OPTION_UNSUPPORTED' : 'APPROVAL_UNAVAILABLE',
@@ -304,8 +419,28 @@ export class AcpSession {
304
419
  }
305
420
  /** A rejected request on an already-closed SDK never reaches this write.
306
421
  * Once a transport write starts, failure remains delivery-unknown. */
307
- observeDispatch(message) { if (record(message)?.method === 'session/prompt' && Object.hasOwn(message, 'id'))
308
- this.dispatchedPrompts++; }
422
+ observeDispatch(message) {
423
+ if (record(message)?.method !== 'session/prompt' || !Object.hasOwn(message, 'id'))
424
+ return;
425
+ const params = record(record(message)?.params), current = params ? this.promptPermissions.get(params) : undefined;
426
+ if (params)
427
+ this.promptPermissions.delete(params);
428
+ if (!current || !current.current()) {
429
+ this.closeDiagnostics.record({ layer: 'acp', initiator: 'acp.permission-dispatch-gate', reason: 'PERMISSION_POLICY_CHANGED', operation: 'session/prompt', action: 'failure' });
430
+ throw new NeedError('PERMISSION_POLICY_CHANGED', '当前权限已变化或无法核对,这次派发已停止;原输入没有重放。', '当前权限代次与远端同值回执');
431
+ }
432
+ current.native();
433
+ this.hooks.broker?.authorizePrompt(() => { try {
434
+ if (!current.current())
435
+ return false;
436
+ current.native();
437
+ return true;
438
+ }
439
+ catch {
440
+ return false;
441
+ } });
442
+ this.dispatchedPrompts++;
443
+ }
309
444
  observe(stream) {
310
445
  // Observe already-parsed public Stream messages: no NDJSON parsing, second
311
446
  // byte buffer, message-size policy, or transport fallback is introduced.
@@ -314,13 +449,6 @@ export class AcpSession {
314
449
  const raw = record(message);
315
450
  const params = record(raw?.params);
316
451
  const update = record(params?.update);
317
- if (raw)
318
- this.hooks.broker?.processedMessage(raw);
319
- 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)) {
320
- const error = new NeedError('RECOVERY_UNSAFE_OUTPUT', '主机会话有不属于当前输入的旧审批,未自动回答', '安全结束旧会话并新建');
321
- this.hooks.broker.fail(error);
322
- throw error;
323
- }
324
452
  if (raw?.method === acp.methods.client.session.update && typeof update?.sessionUpdate === 'string' && !knownUpdates.has(update.sessionUpdate)) {
325
453
  this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_UPDATE', summary: '收到未知远端会话事件,已保留结构', method: 'session/update', payload: params });
326
454
  return;
@@ -332,69 +460,20 @@ export class AcpSession {
332
460
  get id() { return this.sessionId; }
333
461
  get capabilities() { return this.negotiated; }
334
462
  get startMethod() { return this.method; }
335
- 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)), selectorRoles: { model: this.configuration.snapshot.modelConfigId, effort: this.configuration.snapshot.effortConfigId } } } : {}) } : undefined; }
336
- restore(cwd, saved, context, verifiedSameContext = false) {
337
- if (context.employeeRouting)
338
- throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具通道需要重新建立,旧审批没有代答。', '按原主机实例安全结束后新建本机会话');
339
- if (this.startAttempted)
340
- throw new NeedError('SESSION_ALREADY_STARTED', '此客户端已建立会话', '新的客户端连接');
341
- this.startAttempted = true;
342
- this.sessionId = saved.sessionId;
343
- this.negotiated = recoveryCapabilities(saved.capabilities);
344
- this.cwd = cwd;
345
- this.systemPrompt = context.systemPrompt ?? '';
346
- this.codexNewSessionOnly = !!context.codexNewSessionOnly;
347
- // A verified same-context reattach does not create or update native context.
348
- // Old journals may lack new-only metadata; never manufacture it for them.
349
- if (this.codexNewSessionOnly && !verifiedSameContext)
350
- this.validateNewContext(this.systemPrompt, context.systemPrompt !== undefined);
351
- // Legacy currentModelId/resolvedEvidence is not a per-turn setter ACK and
352
- // never proves the concrete model. Only restore selectable config options.
353
- if (saved.modelConfiguration) {
354
- this.receiveConfiguration(saved.modelConfiguration.configOptions, saved.modelConfiguration.selectorRoles);
355
- for (const update of saved.configUpdates ?? [])
356
- this.receiveConfiguration(update);
357
- }
358
- this.hooks.broker?.finishRecovery();
359
- this.detail({ kind: 'need', severity: 'info', code: 'ACP_CLIENT_REATTACHED', summary: '已接回原模型会话', payload: { sessionId: saved.sessionId } });
360
- this.checkReporting();
361
- this.ready = true;
362
- }
363
463
  validatePrompt(prompt) {
364
464
  const capability = this.negotiated.promptCapabilities;
365
465
  for (const block of prompt)
366
466
  if ((block.type === 'image' && !capability?.image) || (block.type === 'audio' && !capability?.audio) || (block.type === 'resource' && !capability?.embeddedContext))
367
467
  throw new NeedError('PROMPT_CONTENT_UNSUPPORTED', `远端未声明此输入内容能力:${block.type};本轮未发送`, '对应内容类型的公开 ACP 输入能力');
368
468
  }
369
- validateContext(context) {
370
- if ((context.systemPrompt ?? '') === this.systemPrompt)
371
- return;
372
- if (this.codexNewSessionOnly)
373
- throw new NeedError('SYSTEM_PROMPT_NEW_SESSION_REQUIRED', 'Codex会话要求已变化,需要保留原会话并新建会话承接', '新会话及完整公开历史承接');
374
- if (!this.negotiated.sessionCapabilities?.systemPrompt)
375
- throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文更新能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
376
- if (!this.negotiated.sessionCapabilities?.resume && !this.negotiated.loadSession)
377
- throw new NeedError('SYSTEM_PROMPT_UPDATE_UNSUPPORTED', '远端没有可更新原会话上下文的恢复或加载能力', '支持带 systemPrompt 的 session/resume 或 session/load');
378
- }
379
- validateNewSessionContext(context) {
380
- if (!this.codexNewSessionOnly)
381
- throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '此会话没有使用Codex新建政策', 'Codex新建政策');
382
- this.validateNewContext(context.systemPrompt ?? '', true);
383
- }
384
469
  setHooks(hooks) {
385
470
  if (this.busy || this.updating || this.selecting)
386
471
  throw new NeedError('TURN_BUSY', '上一轮或配置更新结束前不能更换会话钩子', '上一轮结束');
387
472
  this.hooks = hooks;
473
+ this.employeeNoticeNames.clear();
474
+ this.employeeNoticeUnshown = false;
388
475
  }
389
476
  get closed() { return this.connection.closed; }
390
- validateNewContext(text, requirePolicy = false) {
391
- if ((text || requirePolicy) && !this.negotiated.sessionCapabilities?.systemPrompt)
392
- throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
393
- if (this.codexNewSessionOnly && (text || requirePolicy)) {
394
- if (!supportsCodexContextPolicy(this.negotiated))
395
- throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '远端没有明确声明Codex上下文仅新建生效的兼容政策,本轮未发送', 'systemPrompt与精确的aiWorkbenchSystemPrompt新建政策');
396
- }
397
- }
398
477
  async start(cwd, resumeId, signal, context, prompt) {
399
478
  if (this.startAttempted)
400
479
  throw new NeedError('SESSION_ALREADY_STARTED', '此连接已用于建立会话,不能转用工作区或身份', '新的独立远端连接');
@@ -405,30 +484,26 @@ export class AcpSession {
405
484
  signal?.addEventListener('abort', abort, { once: true });
406
485
  try {
407
486
  const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
408
- protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.19' },
487
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.23' },
409
488
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1 } } } : {}) },
410
489
  }, { cancellationSignal: signal }));
411
- this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
412
490
  if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
413
491
  throw new NeedError('ACP_VERSION_MISMATCH', 'ACP 协议版本不兼容', '支持 ACP v1 的远端 agent');
414
- this.checkReporting();
415
492
  this.negotiated = hello.agentCapabilities ?? {};
416
493
  if (this.employeeMeta) {
417
494
  const policy = record(hello._meta?.awEmployeeExecution);
418
495
  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)
419
496
  throw new NeedError('EMPLOYEE_EXECUTION_UNSUPPORTED', '当前主机尚不支持在你的电脑执行Codex原生工具,本轮没有执行。', 'Codex员工端原生执行适配器与明确能力声明');
420
497
  this.employeeNegotiated = true;
421
- this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_SELECTED', summary: '文件和命令在员工电脑的选定目录执行,Git使用本机仓库;模型、登录和全局助手要求仍在主机。本机stdio MCP按原生规则忽略;HTTP 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: ['http-mcp-execution', 'hooks', 'plugins', 'cold-resume', 'slash-commands', 'host-file-reports'] } });
498
+ this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_SELECTED', summary: '文件和命令在员工电脑的选定目录执行;订阅凭据留在主机,本机扩展的忽略详情单独记录。', payload: { bindingId: this.employeeMeta.bindingId, workspaceId: this.employeeMeta.workspaceId, cwd: this.employeeMeta.cwd, nativeCliVersion: '0.160.1', noHostFallback: true, agentConfigSource: 'host CODEX_HOME', unsupportedLocalFeatures: ['http-mcp-execution', 'hooks', 'plugins', 'cold-resume', 'slash-commands', 'host-file-reports'] } });
422
499
  }
423
500
  const systemPrompt = context?.systemPrompt;
424
501
  this.codexNewSessionOnly = !!context?.codexNewSessionOnly;
425
- this.validateNewContext(systemPrompt ?? '', this.codexNewSessionOnly && systemPrompt !== undefined);
426
502
  if (prompt)
427
503
  this.validatePrompt(prompt);
428
- if (context?.employeeRouting && resumeId)
429
- throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具连接已变化,不能沿用旧会话。', '安全结束原主机实例并重新建立本机工具通道');
430
- const routing = context?.employeeRouting;
431
- 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 }] } } : {}) };
504
+ const routing = context?.employeeRouting ? structuredClone(context.employeeRouting) : undefined;
505
+ this.employeeRouting = routing;
506
+ const params = { cwd, mcpServers: routing ? structuredClone(routing.mcpServers) : [], ...((routing || this.employeeMeta) ? { _meta: { ...(this.employeeMeta ? { awEmployeeExecution: this.employeeMeta } : {}), ...(routing ? { claudeCode: { options: employeeClaudeOptions(routing) } } : {}) } } : {}), ...(systemPrompt && !(resumeId && this.codexNewSessionOnly) ? { systemPrompt: { mode: 'append', content: [{ type: 'text', text: systemPrompt }] } } : {}) };
432
507
  const configRevision = this.resetConfiguration();
433
508
  if (resumeId) {
434
509
  this.sessionId = resumeId;
@@ -453,8 +528,6 @@ export class AcpSession {
453
528
  this.lifecycleConfiguration(result, configRevision);
454
529
  this.method = acp.methods.agent.session.new;
455
530
  }
456
- this.hooks.broker?.acknowledge(this.method);
457
- this.checkReporting();
458
531
  this.ready = true;
459
532
  this.cwd = cwd;
460
533
  this.systemPrompt = systemPrompt ?? '';
@@ -464,10 +537,8 @@ export class AcpSession {
464
537
  if (signal?.aborted)
465
538
  throw new NeedError('ABORTED', '会话建立已取消;尚无本轮输出', '重新连接主机');
466
539
  if (error instanceof ModelConfigurationError) {
467
- this.checkReporting();
468
540
  throw error;
469
541
  }
470
- this.checkReporting();
471
542
  throw acpNeed(error);
472
543
  }
473
544
  finally {
@@ -475,98 +546,6 @@ export class AcpSession {
475
546
  }
476
547
  }
477
548
  /** Codex context is immutable after creation; keep the old remote session intact. */
478
- async newContextSession(context, signal) {
479
- this.checkReporting();
480
- if (!this.ready || !this.sessionId || !this.cwd)
481
- throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
482
- if (!this.codexNewSessionOnly)
483
- throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '此会话没有使用Codex新建政策', 'Codex新建政策');
484
- if (this.busy || this.updating || this.selecting)
485
- throw new NeedError('TURN_BUSY', '上一项会话操作尚未完成', '上一项操作结束');
486
- const text = context.systemPrompt ?? '';
487
- this.validateNewSessionContext(context);
488
- signal?.throwIfAborted();
489
- const oldId = this.sessionId;
490
- this.updating = true;
491
- this.ready = false;
492
- this.creatingReplacement = true;
493
- this.pendingConfigurations.clear();
494
- const abort = () => this.close({ layer: 'acp', initiator: 'acp.new-context-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '新会话建立已取消,远端状态需确认', '管理员核对远端状态'));
495
- signal?.addEventListener('abort', abort, { once: true });
496
- try {
497
- const revision = this.resetConfiguration();
498
- 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 }));
499
- if (!result.sessionId || result.sessionId === oldId)
500
- throw new NeedError('SESSION_STATE_INVALID', '新会话没有返回独立的远端标识,原会话状态需确认', '独立的新远端会话标识');
501
- this.sessionId = result.sessionId;
502
- if (this.employeeMeta)
503
- this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
504
- this.lifecycleConfiguration(result, revision);
505
- this.hooks.broker?.acknowledge(acp.methods.agent.session.new);
506
- this.checkReporting();
507
- this.systemPrompt = text;
508
- this.method = acp.methods.agent.session.new;
509
- this.ready = true;
510
- return this.sessionId;
511
- }
512
- catch (error) {
513
- this.checkReporting();
514
- if (error instanceof ModelConfigurationError)
515
- throw error;
516
- throw acpNeed(error);
517
- }
518
- finally {
519
- this.creatingReplacement = false;
520
- this.updating = false;
521
- signal?.removeEventListener('abort', abort);
522
- }
523
- }
524
- async updateContext(context, signal, force = false) {
525
- this.checkReporting();
526
- if (!this.ready || !this.sessionId || !this.cwd)
527
- throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
528
- if (this.busy || this.updating || this.selecting)
529
- throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新配置', '上一轮结束');
530
- const text = context.systemPrompt ?? '';
531
- if (!force && text === this.systemPrompt)
532
- return undefined;
533
- this.validateContext(context);
534
- if (force && (!this.negotiated.sessionCapabilities?.systemPrompt || !this.negotiated.sessionCapabilities?.resume && !this.negotiated.loadSession))
535
- throw new NeedError('SYSTEM_PROMPT_UPDATE_UNSUPPORTED', '远端没有可传送更新要求的公开恢复能力', 'session/resume 或 session/load 的 systemPrompt 能力');
536
- const resume = !!this.negotiated.sessionCapabilities?.resume;
537
- signal?.throwIfAborted();
538
- this.updating = true;
539
- this.ready = false;
540
- const abort = () => this.close({ layer: 'acp', initiator: 'acp.update-context-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '上下文更新已取消,原会话状态需确认', '重新连接原会话'));
541
- signal?.addEventListener('abort', abort, { once: true });
542
- try {
543
- const method = resume ? acp.methods.agent.session.resume : acp.methods.agent.session.load;
544
- const params = { sessionId: this.sessionId, cwd: this.cwd, mcpServers: [], systemPrompt: { mode: 'append', content: [{ type: 'text', text }] } };
545
- const configRevision = this.resetConfiguration(false);
546
- const result = await this.stage(resume ? 'acp-session-resume' : 'acp-session-load', () => this.connection.agent.request(method, params, { cancellationSignal: signal }));
547
- this.lifecycleConfiguration(result, configRevision);
548
- this.hooks.broker?.acknowledge(method);
549
- this.systemPrompt = text;
550
- this.ready = true;
551
- this.detail({ kind: 'session', severity: 'warning', code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', summary: '系统与开发者要求已通过原会话恢复接口传送;新要求在下次上下文整理后生效,本轮不能保证已立即采用。', payload: { reason: 'OFFICIAL_CONTEXT_UPDATE_DEFERRED' } });
552
- this.checkReporting();
553
- return method;
554
- }
555
- catch (error) {
556
- if (signal?.aborted)
557
- throw new NeedError('ABORTED', '上下文更新已取消;原会话状态需确认', '重新连接原会话');
558
- if (error instanceof ModelConfigurationError) {
559
- this.checkReporting();
560
- throw error;
561
- }
562
- this.checkReporting();
563
- throw acpNeed(error);
564
- }
565
- finally {
566
- this.updating = false;
567
- signal?.removeEventListener('abort', abort);
568
- }
569
- }
570
549
  async prompt(input, signal) {
571
550
  if (!this.ready || !this.sessionId)
572
551
  throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
@@ -575,9 +554,16 @@ export class AcpSession {
575
554
  signal?.throwIfAborted();
576
555
  const prompt = typeof input === 'string' ? [{ type: 'text', text: input }] : input;
577
556
  this.validatePrompt(prompt);
578
- this.checkReporting();
579
- if (this.selected)
580
- this.assertModelSelection(this.selected.model, this.selected.effort);
557
+ const permit = this.hooks.promptGuard?.();
558
+ const params = { sessionId: this.sessionId, prompt };
559
+ const current = () => { try {
560
+ return !signal?.aborted && !this.closeRequested && (!permit || !permit.signal.aborted && permit.isCurrent());
561
+ }
562
+ catch {
563
+ return false;
564
+ } };
565
+ const expectedPermission = this.selectedPermission, reportedModes = new Set();
566
+ const native = () => this.validateNativePermission(expectedPermission, reportedModes);
581
567
  this.busy = true;
582
568
  this.turnSignal = signal;
583
569
  this.turnToken = Symbol('turn');
@@ -593,20 +579,17 @@ export class AcpSession {
593
579
  signal?.addEventListener('abort', cancel, { once: true });
594
580
  try {
595
581
  const result = await this.stage('acp-prompt', () => {
596
- const request = this.connection.agent.request(acp.methods.agent.session.prompt, { sessionId: this.sessionId, prompt }, { cancellationSignal: signal });
582
+ this.promptPermissions.set(params, { current, native });
583
+ const request = this.connection.agent.request(acp.methods.agent.session.prompt, params, { cancellationSignal: signal });
597
584
  return signal ? Promise.race([request, interrupted]) : request;
598
585
  });
599
- this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
600
586
  signal?.throwIfAborted();
601
- this.hooks.broker?.completePrompt();
602
- this.checkReporting();
603
587
  this.hooks.onResponse?.(result);
604
588
  return result.stopReason;
605
589
  }
606
590
  catch (error) {
607
591
  if (signal?.aborted)
608
592
  throw new NeedError('ABORTED', '本轮已取消;已请求主机停止', '确认主机取消状态');
609
- this.checkReporting();
610
593
  throw acpNeed(error);
611
594
  }
612
595
  finally {
@@ -633,8 +616,8 @@ export function acpNeed(error) {
633
616
  if (code === 'AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED' && value.startsWith(code + ': ')) {
634
617
  const reason = value.slice(code.length + 2);
635
618
  if (employeeAdapterCodes.has(reason) || employeeOpenCauseCodes.has(reason)) {
636
- const retained = withFailureEvidence(Object.assign(failure, { cause: new NeedError(reason, '员工执行通道开通失败', '核对固定开通原因') }), [], 'diagnostic');
637
- retained.requires += `;RPC 错误码 ${error.code}`;
619
+ const retained = withFirstFailureEvidence(Object.assign(failure, { cause: new NeedError(reason, '员工执行通道开通失败', '核对固定开通原因') }), reason, code);
620
+ retained.requires += `;开通原因 ${reason};RPC 错误码 ${error.code}`;
638
621
  return retained;
639
622
  }
640
623
  }