@ai-workbench/remote-agent 0.8.21 → 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 -156
  2. package/dist/acp.d.ts +20 -15
  3. package/dist/acp.js +197 -224
  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 +40 -105
  14. package/dist/client.js +34 -127
  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 -3
  21. package/dist/employee-endpoint.js +8 -39
  22. package/dist/employee-exec.d.ts +4 -3
  23. package/dist/employee-exec.js +25 -13
  24. package/dist/employee-info.d.ts +5 -1
  25. package/dist/employee-info.js +16 -5
  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 +1 -1
  44. package/dist/index.d.ts +13 -43
  45. package/dist/index.js +190 -489
  46. package/dist/input.js +2 -3
  47. package/dist/local-user-turn.js +5 -6
  48. package/dist/mapping.d.ts +0 -3
  49. package/dist/mapping.js +0 -34
  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
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
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,11 +220,8 @@ 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
  }
138
227
  employeeNoticeNames = new Set();
@@ -145,9 +234,9 @@ export class AcpSession {
145
234
  this.detail({ kind: 'session', severity: 'info', code: 'EMPLOYEE_MCP_INFORMATION', summary: summary || '本机MCP策略已应用:' + info.httpMcpPolicy, payload: info });
146
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);
147
236
  if (notice) {
148
- for (const name of [...(info.httpMcpPolicy === 'ignore' ? info.ignoredHttp : []), ...info.ignoredNativeHttp])
237
+ for (const name of [...info.ignoredHttp, ...info.ignoredNativeHttp])
149
238
  this.employeeNoticeNames.add(name);
150
- if ((info.httpMcpPolicy === 'ignore' && info.ignoredHttpUnshown) || info.ignoredNativeHttpUnshown || [...(info.httpMcpPolicy === 'ignore' ? info.ignoredHttp : []), ...info.ignoredNativeHttp].some(name => sanitizeDisplayLabel(name) !== name || name.includes('[已脱敏:')))
239
+ if (info.ignoredHttpUnshown || info.ignoredNativeHttpUnshown || [...info.ignoredHttp, ...info.ignoredNativeHttp].some(name => sanitizeDisplayLabel(name) !== name || name.includes('[已脱敏:')))
151
240
  this.employeeNoticeUnshown = true;
152
241
  this.hooks.onEmployeeNotice?.(notice);
153
242
  }
@@ -156,12 +245,9 @@ export class AcpSession {
156
245
  this.detail({ kind: 'need', severity: 'action-required', code: need.code, summary: need.message, payload: { requires: need.requires } }, hooks);
157
246
  hooks.onNeed?.(need);
158
247
  }
159
- checkReporting() { if (this.reportingFailure)
160
- throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能记录;本轮未继续发送', '可用的技术详情记录渠道'); }
161
248
  constructor(transport, hooks) {
162
249
  this.hooks = hooks;
163
250
  this.closeDiagnostics = hooks.closeDiagnostics ?? new CloseDiagnostics(event => this.detail(event));
164
- this.timing.bind(detail => this.detail(detail));
165
251
  const app = acp.client({ name: '@ai-workbench/remote-agent' })
166
252
  .onNotification(acp.methods.client.session.update, ({ params: notification }) => {
167
253
  if (this.creatingReplacement) {
@@ -175,7 +261,18 @@ export class AcpSession {
175
261
  return;
176
262
  }
177
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
+ }
178
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
+ }
179
276
  if (!this.sessionId)
180
277
  this.pendingConfiguration(notification.sessionId, notification.update.configOptions);
181
278
  else
@@ -189,9 +286,6 @@ export class AcpSession {
189
286
  if (meta && Object.keys(meta).length)
190
287
  this.detail({ kind: 'extension', severity: 'info', code: 'ACP_NOTIFICATION_META', summary: '远端通知扩展', method: 'session/update', payload: notification._meta });
191
288
  if (this.busy) {
192
- // Only actual turn output acknowledges prompt acceptance.
193
- if (['agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update'].includes(notification.update.sessionUpdate))
194
- this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
195
289
  this.hooks.onUpdate?.(notification);
196
290
  }
197
291
  else
@@ -201,7 +295,6 @@ export class AcpSession {
201
295
  const hooks = this.hooks, detail = (value) => this.detail(value, hooks), need = (value) => this.need(value, hooks);
202
296
  if (request.sessionId !== this.sessionId || !this.busy || this.turnSignal?.aborted)
203
297
  return { outcome: { outcome: 'cancelled' } };
204
- hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
205
298
  const toolName = request.toolCall.name ?? record(request.toolCall._meta?.claudeCode)?.toolName;
206
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 } : {}) } } });
207
300
  if (toolName === 'AskUserQuestion') {
@@ -224,29 +317,26 @@ export class AcpSession {
224
317
  const token = this.turnToken;
225
318
  const id = this.sessionId;
226
319
  let outcome = 'unavailable';
227
- 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] : []]);
228
322
  const broker = hooks.broker;
229
323
  let generation;
230
- // A transport disconnect withdraws only the UI question, not the ACP RPC.
231
- // 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.
232
325
  try {
233
- while (!approvalSignal.aborted) {
234
- await broker?.waitOnline(approvalSignal);
235
- generation = broker?.generation;
236
- const lease = broker?.signal;
237
- const signal = lease ? AbortSignal.any([approvalSignal, lease]) : approvalSignal;
238
- const interrupted = Promise.withResolvers();
239
- const abort = () => interrupted.resolve('cancelled');
240
- signal.addEventListener('abort', abort, { once: true });
241
- 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)
242
334
  outcome = await Promise.race([hooks.approve?.(presented, signal) ?? Promise.resolve('unavailable'), interrupted.promise]);
243
- }
244
- finally {
245
- signal.removeEventListener('abort', abort);
246
- }
247
- if (broker && !approvalSignal.aborted && (generation !== broker.generation || lease?.aborted))
248
- continue;
249
- break;
335
+ else
336
+ outcome = 'cancelled';
337
+ }
338
+ finally {
339
+ signal.removeEventListener('abort', abort);
250
340
  }
251
341
  }
252
342
  catch {
@@ -254,9 +344,26 @@ export class AcpSession {
254
344
  }
255
345
  const kind = outcome === 'allowed-once' ? 'allow_once' : outcome === 'rejected' ? 'reject_once' : undefined;
256
346
  const option = kind && eligible.find(item => item.kind === kind);
257
- if (this.busy && token === this.turnToken && id === this.sessionId && !approvalSignal.aborted && option && (!broker || (broker.online && generation === broker.generation))) {
258
- if (broker)
259
- 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
+ }
260
367
  return { outcome: { outcome: 'selected', optionId: option.optionId } };
261
368
  }
262
369
  need({ code: outcome === 'allowed-once' ? 'APPROVAL_OPTION_UNSUPPORTED' : 'APPROVAL_UNAVAILABLE',
@@ -312,8 +419,28 @@ export class AcpSession {
312
419
  }
313
420
  /** A rejected request on an already-closed SDK never reaches this write.
314
421
  * Once a transport write starts, failure remains delivery-unknown. */
315
- observeDispatch(message) { if (record(message)?.method === 'session/prompt' && Object.hasOwn(message, 'id'))
316
- 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
+ }
317
444
  observe(stream) {
318
445
  // Observe already-parsed public Stream messages: no NDJSON parsing, second
319
446
  // byte buffer, message-size policy, or transport fallback is introduced.
@@ -322,13 +449,6 @@ export class AcpSession {
322
449
  const raw = record(message);
323
450
  const params = record(raw?.params);
324
451
  const update = record(params?.update);
325
- if (raw)
326
- this.hooks.broker?.processedMessage(raw);
327
- 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)) {
328
- const error = new NeedError('RECOVERY_UNSAFE_OUTPUT', '主机会话有不属于当前输入的旧审批,未自动回答', '安全结束旧会话并新建');
329
- this.hooks.broker.fail(error);
330
- throw error;
331
- }
332
452
  if (raw?.method === acp.methods.client.session.update && typeof update?.sessionUpdate === 'string' && !knownUpdates.has(update.sessionUpdate)) {
333
453
  this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_UPDATE', summary: '收到未知远端会话事件,已保留结构', method: 'session/update', payload: params });
334
454
  return;
@@ -340,55 +460,12 @@ export class AcpSession {
340
460
  get id() { return this.sessionId; }
341
461
  get capabilities() { return this.negotiated; }
342
462
  get startMethod() { return this.method; }
343
- 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; }
344
- restore(cwd, saved, context, verifiedSameContext = false) {
345
- if (context.employeeRouting)
346
- throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具通道需要重新建立,旧审批没有代答。', '按原主机实例安全结束后新建本机会话');
347
- if (this.startAttempted)
348
- throw new NeedError('SESSION_ALREADY_STARTED', '此客户端已建立会话', '新的客户端连接');
349
- this.startAttempted = true;
350
- this.sessionId = saved.sessionId;
351
- this.negotiated = recoveryCapabilities(saved.capabilities);
352
- this.cwd = cwd;
353
- this.systemPrompt = context.systemPrompt ?? '';
354
- this.codexNewSessionOnly = !!context.codexNewSessionOnly;
355
- // A verified same-context reattach does not create or update native context.
356
- // Old journals may lack new-only metadata; never manufacture it for them.
357
- if (this.codexNewSessionOnly && !verifiedSameContext)
358
- this.validateNewContext(this.systemPrompt, context.systemPrompt !== undefined);
359
- // Legacy currentModelId/resolvedEvidence is not a per-turn setter ACK and
360
- // never proves the concrete model. Only restore selectable config options.
361
- if (saved.modelConfiguration) {
362
- this.receiveConfiguration(saved.modelConfiguration.configOptions, saved.modelConfiguration.selectorRoles);
363
- for (const update of saved.configUpdates ?? [])
364
- this.receiveConfiguration(update);
365
- }
366
- this.hooks.broker?.finishRecovery();
367
- this.detail({ kind: 'need', severity: 'info', code: 'ACP_CLIENT_REATTACHED', summary: '已接回原模型会话', payload: { sessionId: saved.sessionId } });
368
- this.checkReporting();
369
- this.ready = true;
370
- }
371
463
  validatePrompt(prompt) {
372
464
  const capability = this.negotiated.promptCapabilities;
373
465
  for (const block of prompt)
374
466
  if ((block.type === 'image' && !capability?.image) || (block.type === 'audio' && !capability?.audio) || (block.type === 'resource' && !capability?.embeddedContext))
375
467
  throw new NeedError('PROMPT_CONTENT_UNSUPPORTED', `远端未声明此输入内容能力:${block.type};本轮未发送`, '对应内容类型的公开 ACP 输入能力');
376
468
  }
377
- validateContext(context) {
378
- if ((context.systemPrompt ?? '') === this.systemPrompt)
379
- return;
380
- if (this.codexNewSessionOnly)
381
- throw new NeedError('SYSTEM_PROMPT_NEW_SESSION_REQUIRED', 'Codex会话要求已变化,需要保留原会话并新建会话承接', '新会话及完整公开历史承接');
382
- if (!this.negotiated.sessionCapabilities?.systemPrompt)
383
- throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文更新能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
384
- if (!this.negotiated.sessionCapabilities?.resume && !this.negotiated.loadSession)
385
- throw new NeedError('SYSTEM_PROMPT_UPDATE_UNSUPPORTED', '远端没有可更新原会话上下文的恢复或加载能力', '支持带 systemPrompt 的 session/resume 或 session/load');
386
- }
387
- validateNewSessionContext(context) {
388
- if (!this.codexNewSessionOnly)
389
- throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '此会话没有使用Codex新建政策', 'Codex新建政策');
390
- this.validateNewContext(context.systemPrompt ?? '', true);
391
- }
392
469
  setHooks(hooks) {
393
470
  if (this.busy || this.updating || this.selecting)
394
471
  throw new NeedError('TURN_BUSY', '上一轮或配置更新结束前不能更换会话钩子', '上一轮结束');
@@ -397,14 +474,6 @@ export class AcpSession {
397
474
  this.employeeNoticeUnshown = false;
398
475
  }
399
476
  get closed() { return this.connection.closed; }
400
- validateNewContext(text, requirePolicy = false) {
401
- if ((text || requirePolicy) && !this.negotiated.sessionCapabilities?.systemPrompt)
402
- throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
403
- if (this.codexNewSessionOnly && (text || requirePolicy)) {
404
- if (!supportsCodexContextPolicy(this.negotiated))
405
- throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '远端没有明确声明Codex上下文仅新建生效的兼容政策,本轮未发送', 'systemPrompt与精确的aiWorkbenchSystemPrompt新建政策');
406
- }
407
- }
408
477
  async start(cwd, resumeId, signal, context, prompt) {
409
478
  if (this.startAttempted)
410
479
  throw new NeedError('SESSION_ALREADY_STARTED', '此连接已用于建立会话,不能转用工作区或身份', '新的独立远端连接');
@@ -415,30 +484,26 @@ export class AcpSession {
415
484
  signal?.addEventListener('abort', abort, { once: true });
416
485
  try {
417
486
  const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
418
- protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.21' },
487
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.23' },
419
488
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1 } } } : {}) },
420
489
  }, { cancellationSignal: signal }));
421
- this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
422
490
  if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
423
491
  throw new NeedError('ACP_VERSION_MISMATCH', 'ACP 协议版本不兼容', '支持 ACP v1 的远端 agent');
424
- this.checkReporting();
425
492
  this.negotiated = hello.agentCapabilities ?? {};
426
493
  if (this.employeeMeta) {
427
494
  const policy = record(hello._meta?.awEmployeeExecution);
428
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)
429
496
  throw new NeedError('EMPLOYEE_EXECUTION_UNSUPPORTED', '当前主机尚不支持在你的电脑执行Codex原生工具,本轮没有执行。', 'Codex员工端原生执行适配器与明确能力声明');
430
497
  this.employeeNegotiated = true;
431
- 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'] } });
432
499
  }
433
500
  const systemPrompt = context?.systemPrompt;
434
501
  this.codexNewSessionOnly = !!context?.codexNewSessionOnly;
435
- this.validateNewContext(systemPrompt ?? '', this.codexNewSessionOnly && systemPrompt !== undefined);
436
502
  if (prompt)
437
503
  this.validatePrompt(prompt);
438
- if (context?.employeeRouting && resumeId)
439
- throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具连接已变化,不能沿用旧会话。', '安全结束原主机实例并重新建立本机工具通道');
440
- const routing = context?.employeeRouting;
441
- 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 }] } } : {}) };
442
507
  const configRevision = this.resetConfiguration();
443
508
  if (resumeId) {
444
509
  this.sessionId = resumeId;
@@ -463,8 +528,6 @@ export class AcpSession {
463
528
  this.lifecycleConfiguration(result, configRevision);
464
529
  this.method = acp.methods.agent.session.new;
465
530
  }
466
- this.hooks.broker?.acknowledge(this.method);
467
- this.checkReporting();
468
531
  this.ready = true;
469
532
  this.cwd = cwd;
470
533
  this.systemPrompt = systemPrompt ?? '';
@@ -474,10 +537,8 @@ export class AcpSession {
474
537
  if (signal?.aborted)
475
538
  throw new NeedError('ABORTED', '会话建立已取消;尚无本轮输出', '重新连接主机');
476
539
  if (error instanceof ModelConfigurationError) {
477
- this.checkReporting();
478
540
  throw error;
479
541
  }
480
- this.checkReporting();
481
542
  throw acpNeed(error);
482
543
  }
483
544
  finally {
@@ -485,98 +546,6 @@ export class AcpSession {
485
546
  }
486
547
  }
487
548
  /** Codex context is immutable after creation; keep the old remote session intact. */
488
- async newContextSession(context, signal) {
489
- this.checkReporting();
490
- if (!this.ready || !this.sessionId || !this.cwd)
491
- throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
492
- if (!this.codexNewSessionOnly)
493
- throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '此会话没有使用Codex新建政策', 'Codex新建政策');
494
- if (this.busy || this.updating || this.selecting)
495
- throw new NeedError('TURN_BUSY', '上一项会话操作尚未完成', '上一项操作结束');
496
- const text = context.systemPrompt ?? '';
497
- this.validateNewSessionContext(context);
498
- signal?.throwIfAborted();
499
- const oldId = this.sessionId;
500
- this.updating = true;
501
- this.ready = false;
502
- this.creatingReplacement = true;
503
- this.pendingConfigurations.clear();
504
- const abort = () => this.close({ layer: 'acp', initiator: 'acp.new-context-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '新会话建立已取消,远端状态需确认', '管理员核对远端状态'));
505
- signal?.addEventListener('abort', abort, { once: true });
506
- try {
507
- const revision = this.resetConfiguration();
508
- 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 }));
509
- if (!result.sessionId || result.sessionId === oldId)
510
- throw new NeedError('SESSION_STATE_INVALID', '新会话没有返回独立的远端标识,原会话状态需确认', '独立的新远端会话标识');
511
- this.sessionId = result.sessionId;
512
- if (this.employeeMeta)
513
- this.employeeInfo(result._meta?.awEmployeeExecutionInfo);
514
- this.lifecycleConfiguration(result, revision);
515
- this.hooks.broker?.acknowledge(acp.methods.agent.session.new);
516
- this.checkReporting();
517
- this.systemPrompt = text;
518
- this.method = acp.methods.agent.session.new;
519
- this.ready = true;
520
- return this.sessionId;
521
- }
522
- catch (error) {
523
- this.checkReporting();
524
- if (error instanceof ModelConfigurationError)
525
- throw error;
526
- throw acpNeed(error);
527
- }
528
- finally {
529
- this.creatingReplacement = false;
530
- this.updating = false;
531
- signal?.removeEventListener('abort', abort);
532
- }
533
- }
534
- async updateContext(context, signal, force = false) {
535
- this.checkReporting();
536
- if (!this.ready || !this.sessionId || !this.cwd)
537
- throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
538
- if (this.busy || this.updating || this.selecting)
539
- throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新配置', '上一轮结束');
540
- const text = context.systemPrompt ?? '';
541
- if (!force && text === this.systemPrompt)
542
- return undefined;
543
- this.validateContext(context);
544
- if (force && (!this.negotiated.sessionCapabilities?.systemPrompt || !this.negotiated.sessionCapabilities?.resume && !this.negotiated.loadSession))
545
- throw new NeedError('SYSTEM_PROMPT_UPDATE_UNSUPPORTED', '远端没有可传送更新要求的公开恢复能力', 'session/resume 或 session/load 的 systemPrompt 能力');
546
- const resume = !!this.negotiated.sessionCapabilities?.resume;
547
- signal?.throwIfAborted();
548
- this.updating = true;
549
- this.ready = false;
550
- const abort = () => this.close({ layer: 'acp', initiator: 'acp.update-context-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '上下文更新已取消,原会话状态需确认', '重新连接原会话'));
551
- signal?.addEventListener('abort', abort, { once: true });
552
- try {
553
- const method = resume ? acp.methods.agent.session.resume : acp.methods.agent.session.load;
554
- const params = { sessionId: this.sessionId, cwd: this.cwd, mcpServers: [], systemPrompt: { mode: 'append', content: [{ type: 'text', text }] } };
555
- const configRevision = this.resetConfiguration(false);
556
- const result = await this.stage(resume ? 'acp-session-resume' : 'acp-session-load', () => this.connection.agent.request(method, params, { cancellationSignal: signal }));
557
- this.lifecycleConfiguration(result, configRevision);
558
- this.hooks.broker?.acknowledge(method);
559
- this.systemPrompt = text;
560
- this.ready = true;
561
- this.detail({ kind: 'session', severity: 'warning', code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', summary: '系统与开发者要求已通过原会话恢复接口传送;新要求在下次上下文整理后生效,本轮不能保证已立即采用。', payload: { reason: 'OFFICIAL_CONTEXT_UPDATE_DEFERRED' } });
562
- this.checkReporting();
563
- return method;
564
- }
565
- catch (error) {
566
- if (signal?.aborted)
567
- throw new NeedError('ABORTED', '上下文更新已取消;原会话状态需确认', '重新连接原会话');
568
- if (error instanceof ModelConfigurationError) {
569
- this.checkReporting();
570
- throw error;
571
- }
572
- this.checkReporting();
573
- throw acpNeed(error);
574
- }
575
- finally {
576
- this.updating = false;
577
- signal?.removeEventListener('abort', abort);
578
- }
579
- }
580
549
  async prompt(input, signal) {
581
550
  if (!this.ready || !this.sessionId)
582
551
  throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
@@ -585,9 +554,16 @@ export class AcpSession {
585
554
  signal?.throwIfAborted();
586
555
  const prompt = typeof input === 'string' ? [{ type: 'text', text: input }] : input;
587
556
  this.validatePrompt(prompt);
588
- this.checkReporting();
589
- if (this.selected)
590
- 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);
591
567
  this.busy = true;
592
568
  this.turnSignal = signal;
593
569
  this.turnToken = Symbol('turn');
@@ -603,20 +579,17 @@ export class AcpSession {
603
579
  signal?.addEventListener('abort', cancel, { once: true });
604
580
  try {
605
581
  const result = await this.stage('acp-prompt', () => {
606
- 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 });
607
584
  return signal ? Promise.race([request, interrupted]) : request;
608
585
  });
609
- this.hooks.broker?.acknowledge(acp.methods.agent.session.prompt);
610
586
  signal?.throwIfAborted();
611
- this.hooks.broker?.completePrompt();
612
- this.checkReporting();
613
587
  this.hooks.onResponse?.(result);
614
588
  return result.stopReason;
615
589
  }
616
590
  catch (error) {
617
591
  if (signal?.aborted)
618
592
  throw new NeedError('ABORTED', '本轮已取消;已请求主机停止', '确认主机取消状态');
619
- this.checkReporting();
620
593
  throw acpNeed(error);
621
594
  }
622
595
  finally {
@@ -643,8 +616,8 @@ export function acpNeed(error) {
643
616
  if (code === 'AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED' && value.startsWith(code + ': ')) {
644
617
  const reason = value.slice(code.length + 2);
645
618
  if (employeeAdapterCodes.has(reason) || employeeOpenCauseCodes.has(reason)) {
646
- const retained = withFailureEvidence(Object.assign(failure, { cause: new NeedError(reason, '员工执行通道开通失败', '核对固定开通原因') }), [], 'diagnostic');
647
- 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}`;
648
621
  return retained;
649
622
  }
650
623
  }