@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/index.js CHANGED
@@ -1,59 +1,45 @@
1
1
  import { connectNeedMessage } from "./connect-need.js";
2
- import { CloseDiagnostics, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, closeCode, closeErrorCodes, closeFailure } from "./close-diagnostics.js";
3
- import { withPendingCleanupEvidence, withRemoteFailureEvidence } from "./failure-evidence.js";
4
- import { runCatalog } from "./catalog-ssh.js";
5
- import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
2
+ import { CloseDiagnostics, withFirstFailureEvidence, failureEvidenceSummary, closeCode } from "./close-diagnostics.js";
3
+ import { withRemoteFailureEvidence } from "./failure-evidence.js";
6
4
  import { SessionId } from '@deepseek-ai/dsh-session';
7
5
  import Schema from '@deepseek-ai/schemastery';
8
6
  import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
9
- import { fileURLToPath } from 'node:url';
10
- import { createHash, randomUUID } from 'node:crypto';
11
- import { recoveryJournal, runtimeRecoveryKey, supportsCodexContextPolicy } from "./recovery.js";
7
+ import { createHash } from 'node:crypto';
12
8
  import * as unstable from '@agentclientprotocol/sdk/unstable';
13
9
  import * as acp from '@agentclientprotocol/sdk';
14
10
  import { AcpSession } from "./acp.js";
15
- import { runOneShot } from "./oneshot.js";
16
11
  import { StreamMapper } from "./mapping.js";
17
12
  import { connectProvider } from "./provider.js";
18
13
  import { brokerFactory } from "./broker.js";
19
14
  import { AGENTS, CODEX_AGENT } from "./agents.js";
20
- import { prepareInput, prepareContinuation, projectHostWorkspaceInput } from "./input.js";
21
- import { createWorkspaceNoticeConsumer, isWorkspaceNoticeMessage } from "./workspace-notice.js";
15
+ import { prepareInput, projectHostWorkspaceInput } from "./input.js";
22
16
  import { NeedError } from "./remote.js";
23
17
  import { employeeFailure } from "./presentation.js";
24
18
  import { createRemoteDetails } from "./remote-details.js";
25
- import { PROVIDER_NAMES, ProviderLocaleRemote } from "./provider-locale.js";
26
19
  import { approvalLabel } from "./approval-label.js";
27
- import { StageTiming } from "./stage-timing.js";
28
- import { verifyRecoveryCheckpoint, currentInputIds } from "./recovery-checkpoint.js";
29
20
  import { ModelCatalog, modelInfo } from "./model-catalog.js";
30
- import { ModelPresentation, unavailableModelInfo } from "./model-presentation.js";
31
- import { EMPLOYEE_EXECUTION_NOTICE, employeeNoticePreviouslyShown } from "./employee-presentation.js";
21
+ import { ModelPresentation } from "./model-presentation.js";
22
+ import { EMPLOYEE_EXECUTION_NOTICE } from "./employee-presentation.js";
32
23
  import { ModelConfigurationError } from "./model-configuration.js";
33
24
  import { officialEmployeeWorkspace } from "./employee-workspace.js";
34
- import { nativeWorkspaceSourceConfirmation } from "./workspace-source-confirmation.js";
35
25
  import { resolveEmployeeNative } from "./employee-native.js";
36
- import { createLocalUserTurnObserver } from "./local-user-turn.js";
37
26
  import { createClaudeLocalProvider } from "./claude-local.js";
38
27
  import { readLocalUserTurn } from "./local-user-turn.js";
39
28
  import { readEmployeeWorkspace } from "./employee-workspace.js";
40
- import { createModelDefaults, createDefaultSelectionWriter } from "./model-defaults.js";
41
29
  import { createPreflightDiagnostics } from "./preflight-diagnostic.js";
42
- import { installModelInputRetention, markModelPreparationFailure } from "./model-input-retention.js";
30
+ import { installModelInputRetention } from "./model-input-retention.js";
31
+ import { dshPermissionSource, permissionMode } from "./permissions.js";
32
+ export { readDshPermissions } from "./permissions.js";
43
33
  export const name = 'ai-workbench-remote-agent';
44
34
  export const settingsNamespace = 'ai-workbench-codex';
45
35
  // L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
46
36
  export const inject = ['llm'];
47
37
  /** Owner-approved defaults; administrators may override through the existing settings. */
48
- export const DEFAULT_CONFIG = Object.freeze({ mode: 'session', acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, oneShotSandbox: 'workspace-write', oneShotApproval: 'never', employeeShellKillGraceMs: 1000, employeeHttpMcpPolicy: 'reject' });
38
+ export const DEFAULT_CONFIG = Object.freeze({ acpMaxMessageBytes: 4194304, brokerMaxFrameBytes: 2097152, brokerControlMaxBytes: 1048576, employeeShellKillGraceMs: 1000 });
49
39
  export const Config = Schema.object({
50
- employeeHttpMcpPolicy: Schema.union([Schema.const('reject').i18n({ zh: '拒绝本机 HTTP MCP(默认)', en: '拒绝本机 HTTP MCP(默认)' }), Schema.const('ignore').i18n({ zh: '忽略本机 HTTP MCP 后继续', en: '忽略本机 HTTP MCP 后继续' })]).i18n({ zh: '本机 HTTP MCP 策略;默认拒绝;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略并说明名称', en: '本机 HTTP MCP 策略;默认拒绝;忽略仅剔除会话投影,不改员工配置。新连接生效,stdio 始终按原生规则忽略并说明名称' }).default(DEFAULT_CONFIG.employeeHttpMcpPolicy).volatile(),
51
- mode: Schema.union([Schema.const('session').i18n({ en: 'Session', zh: '会话模式' }), Schema.const('one-shot').i18n({ en: 'One-shot', zh: '单次模式' })]).i18n({ en: 'Run mode; start a new session after switching modes', zh: 'Codex运行模式;Claude固定会话模式;切换后请新建会话' }).default(DEFAULT_CONFIG.mode).volatile(),
52
40
  acpMaxMessageBytes: Schema.number().min(1).step(1).i18n({ en: 'Session message limit; default 4 MiB, adjustable by administrators', zh: '会话消息容量;默认4 MiB,管理员可调整' }).default(DEFAULT_CONFIG.acpMaxMessageBytes).volatile(),
53
41
  brokerMaxFrameBytes: Schema.number().min(1).step(1).i18n({ en: 'Host output chunk limit; default 2 MiB, adjustable by administrators', zh: '主机回传单段容量;默认2 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerMaxFrameBytes).volatile(),
54
42
  brokerControlMaxBytes: Schema.number().min(1).step(1).i18n({ en: 'Host session control reply limit; default 1 MiB, adjustable by administrators', zh: '主机会话管理回复容量;默认1 MiB,管理员可调整' }).default(DEFAULT_CONFIG.brokerControlMaxBytes).volatile(),
55
- oneShotSandbox: Schema.union(['read-only', 'workspace-write', 'danger-full-access']).i18n({ en: 'One-shot sandbox; workspace changes are allowed by default', zh: '单次任务沙箱;默认允许工作区内修改' }).default(DEFAULT_CONFIG.oneShotSandbox).volatile(),
56
- oneShotApproval: Schema.const('never').i18n({ en: 'Non-interactive approval; permissions follow the launch policy, never automatic approval', zh: '单次模式无交互审批;权限由启动策略决定,不表示自动批准' }).default(DEFAULT_CONFIG.oneShotApproval).volatile(),
57
43
  employeeShellKillGraceMs: Schema.number().min(0).max(2147483647).step(1).i18n({ en: 'Local command SSH cleanup grace in milliseconds; cancellation keeps the remote result unknown', zh: '本机命令SSH取消后的清理宽限毫秒;默认1000,取消仍保留员工端结果未知' }).default(DEFAULT_CONFIG.employeeShellKillGraceMs).volatile(),
58
44
  });
59
45
  /** DSH volatile settings are handles; adapters freeze their values once per invocation. */
@@ -65,23 +51,16 @@ function modelResolutionLabel(configuration) {
65
51
  ? `型号:${configuration.resolvedModel}(来源:主机目录映射 ${configuration.selected.value};会话未单独回报具体型号)`
66
52
  : `具体型号:${configuration.resolvedModel}`;
67
53
  }
68
- function validContextDigest(value) { return typeof value === 'string' && /^[a-fA-F0-9]{64}$/u.test(value); }
69
54
  function readPointer(options, agent) {
70
- const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
55
+ const last = options.messages.findLast(message => message.role === 'assistant');
71
56
  if (!last || last.role !== 'assistant')
72
57
  return;
73
58
  if (last.source.provider !== options.provider)
74
- throw new NeedError('SESSION_STATE_MISSING', '已有对话属于其他模型通道,不能沿用其远端进程', '当前模型的远端恢复记录');
75
- if (last.source.replayState === undefined)
76
59
  return;
77
60
  const envelope = last.source.replayState;
78
61
  const pointer = envelope?.response;
79
- if (pointer?.version !== 2 || pointer.agent !== agent.id || (pointer.mode !== 'session' && pointer.mode !== 'one-shot') || ![pointer.identity, pointer.cwd, pointer.sessionId].every(value => typeof value === 'string' && value.length > 0))
80
- throw new NeedError('SESSION_STATE_INVALID', '原对话缺少新版工作区/模型绑定,不能静默迁移,请新建会话', '有效的新一代远端会话恢复记录');
81
- if (options.sessionId && pointer.dshSessionId !== options.sessionId)
82
- throw new NeedError('SESSION_CONTEXT_MISMATCH', '此恢复记录属于另一会话,复制或分叉后不能沿用原远端进程,请新建会话', '当前DSH会话自己的远端恢复记录');
83
- if (agent.loginProvider === 'codex' && (!validContextDigest(pointer.contextSha256) || (pointer.previousRemoteSessions !== undefined && (!Array.isArray(pointer.previousRemoteSessions) || !pointer.previousRemoteSessions.every(ref => ref && typeof ref.sessionId === 'string' && ref.sessionId.length > 0 && validContextDigest(ref.contextSha256))))))
84
- throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '原Codex会话的初建上下文摘要未知或无效,不能猜测是否相同,本轮未发送', '原会话合法的64位十六进制初建上下文摘要');
62
+ if (pointer?.version !== 2 || pointer.agent !== agent.id || pointer.dshSessionId !== options.sessionId || ![pointer.identity, pointer.cwd, pointer.sessionId].every(value => typeof value === 'string' && value.length > 0))
63
+ return;
85
64
  return pointer;
86
65
  }
87
66
  // A reporting failure can wrap the original admission error. Preserve its
@@ -110,17 +89,16 @@ export class CodexRemoteAdapter extends LlmAdapter {
110
89
  reportLoginFailure;
111
90
  directory;
112
91
  preflightDiagnostics;
92
+ permissions;
113
93
  config;
114
94
  sessions = new Map();
115
95
  turns = new Map();
116
96
  lifetime = new AbortController();
117
97
  shuttingDown = false;
118
98
  detached = new Set();
119
- cleanups = new WeakMap();
120
- preparationFailures = new Map();
121
99
  modelPresentation = new ModelPresentation();
122
100
  employeeNoticeSessions = new Set();
123
- constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics) {
101
+ constructor(config, resolveRuntime, approve, providerName = () => CODEX_AGENT.name.zh, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions) {
124
102
  super();
125
103
  this.resolveRuntime = resolveRuntime;
126
104
  this.approve = approve;
@@ -130,6 +108,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
130
108
  this.reportLoginFailure = reportLoginFailure;
131
109
  this.directory = directory;
132
110
  this.preflightDiagnostics = preflightDiagnostics;
111
+ this.permissions = permissions;
133
112
  this.config = typeof config === 'function' ? config : () => config;
134
113
  }
135
114
  providerInfo(provider) { return { id: provider, name: this.providerName() }; }
@@ -142,112 +121,22 @@ export class CodexRemoteAdapter extends LlmAdapter {
142
121
  finally {
143
122
  catalog.release();
144
123
  } }
145
- async resolveModel(provider, id, signal) { try {
146
- signal?.throwIfAborted();
147
- const displayed = this.modelPresentation.resolve(provider, id);
148
- if (displayed)
149
- return displayed;
150
- const catalog = await this.catalog(signal);
151
- try {
152
- const model = catalog.models.find(model => model.id === id);
153
- return model ? modelInfo(provider, model) : unavailableModelInfo(provider, id);
154
- }
155
- finally {
156
- catalog.release();
157
- }
158
- }
159
- catch (error) {
160
- markModelPreparationFailure(provider, error, signal);
161
- throw error;
162
- } }
163
- async prepareCall(provider, id, signal) {
164
- try {
165
- const catalog = await this.catalog(signal);
166
- try {
167
- catalog.assertCurrent();
168
- const selected = catalog.models.find(model => model.id === id);
169
- if (!selected)
170
- throw new LlmError('所选模型已不在当前主机目录中,请在模型菜单重新选择后再发送;本次输入尚未发送。', 'MODEL_CATALOG_UNAVAILABLE');
171
- return { model: modelInfo(provider, selected), stream: (options) => this.stream(options) };
172
- }
173
- finally {
174
- catalog.release();
175
- }
176
- }
177
- catch (error) {
178
- markModelPreparationFailure(provider, error, signal);
179
- throw error;
180
- }
181
- }
182
- observeModelInvalidation(fields, changes = []) { for (const live of this.sessions.values())
183
- live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'catalog.signature-change', reason: 'MODEL_CATALOG_SIGNATURE_CHANGED', action: 'invalidate', changedFields: fields, changes }); }
184
- async invalidateModelState() { for (const [id, turn] of this.turns) {
185
- this.sessions.get(id)?.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.model-invalidation', reason: 'MODEL_STATE_INVALIDATED', action: 'abort', changes: [{ field: 'adapter.turn-aborted', before: turn.signal.aborted, after: true }] });
186
- turn.abort();
187
- } await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
188
- async invalidateEmployeeState() { for (const live of this.sessions.values())
189
- if (live.runtime.employeeExecutionMeta || live.runtime.execution)
190
- live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.employee-invalidation', reason: 'EMPLOYEE_SERVICE_CHANGED', action: 'invalidate' }); await Promise.all([...this.sessions].filter(([, live]) => live.runtime.employeeExecutionMeta || live.runtime.execution).map(([id]) => this.disposeSession(id))); }
124
+ async resolveModel(provider, id, _signal) { return this.modelPresentation.resolve(provider, id) ?? { provider, id, name: id }; }
125
+ async prepareCall(provider, id, _signal) { return { model: await this.resolveModel(provider, id), stream: (options) => this.stream(options) }; }
191
126
  providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
192
127
  cleanupRuntime(runtime, cause = { layer: 'adapter', initiator: 'adapter.cleanup-runtime', reason: 'RUNTIME_CLEANUP', action: 'dispose' }) {
193
128
  runtime.closeDiagnostics?.record(cause);
194
- const existing = this.cleanups.get(runtime);
195
- if (existing)
196
- return existing;
197
- const cleanup = Promise.resolve().then(() => runtime.dispose?.(cause)).catch(error => {
198
- const codes = closeErrorCodes(error, 'REMOTE_CLEANUP_UNCONFIRMED');
199
- const failure = closeFailure('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话的清理结果未确认,不能继续或重开此会话', '管理员核对原主机会话;收尾代码 ' + codes.join(','), codes);
200
- for (const live of this.sessions.values())
201
- if (live.runtime === runtime) {
202
- live.cleanupFailure = failure;
203
- live.failure ??= { cause: failure };
204
- }
205
- throw failure;
206
- });
207
- this.cleanups.set(runtime, cleanup);
208
- return cleanup;
209
- }
210
- /** Publish an already known failure without coupling the official stream to cleanup. */
211
- finishFailedTurn(key, live, primary, code, firstReason) {
212
- withPendingCleanupEvidence(primary, firstReason, code);
213
- const runtime = live.runtime;
214
- const disposal = this.cleanupRuntime(runtime).then(() => {
215
- if (this.sessions.get(key) !== live || live.disposal !== disposal)
216
- return;
217
- live.disposal = undefined;
218
- if (!live.failure) {
219
- if (typeof runtime.dispose === 'function') {
220
- this.sessions.delete(key);
221
- this.preparationFailures.set(key, code);
222
- }
223
- else
224
- live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
225
- }
226
- }, error => {
227
- if (this.sessions.get(key) === live && live.disposal === disposal)
228
- live.cleanupFailure = withFailureEvidence(primary, [error], 'cleanup', firstReason, code);
229
- throw error;
230
- });
231
- live.disposal = disposal;
232
- const report = (reason) => {
233
- runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-cleanup', reason, action: 'dispose' });
234
- // State settles before reporting. A delayed D19 write is not another release gate.
235
- void Promise.resolve().then(() => runtime.details?.flush()).catch(error => runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-cleanup-report', reason: closeCode(error, 'CLOSE_FLUSH_FAILED'), action: 'failure' }));
236
- };
237
- void disposal.then(() => report(typeof runtime.dispose === 'function' ? 'RUNTIME_CLEANUP_CONFIRMED' : 'REMOTE_CLEANUP_UNCONFIRMED'), () => report('REMOTE_CLEANUP_UNCONFIRMED'));
129
+ void Promise.resolve().then(() => runtime.dispose?.(cause)).catch(() => console.warn('[remote-agent] 旧主机会话暂未清理,将由主机自动回收'));
130
+ return Promise.resolve();
238
131
  }
239
132
  async disposeSession(sessionId) {
240
- this.sessions.get(sessionId)?.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'abort' });
241
133
  this.turns.get(sessionId)?.abort();
242
134
  const current = this.sessions.get(sessionId);
243
135
  if (!current)
244
136
  return;
245
- if (current.disposal)
246
- return current.disposal;
137
+ this.sessions.delete(sessionId);
247
138
  current.session.close({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'close' });
248
- current.disposal = this.cleanupRuntime(current.runtime).then(() => { if (this.sessions.get(sessionId) === current)
249
- this.sessions.delete(sessionId); });
250
- return current.disposal;
139
+ await this.cleanupRuntime(current.runtime);
251
140
  }
252
141
  async detachSession(sessionId) {
253
142
  const current = this.sessions.get(sessionId), turn = this.turns.get(sessionId);
@@ -286,11 +175,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
286
175
  }
287
176
  catch (error) {
288
177
  if (error instanceof ModelConfigurationError)
289
- throw new LlmError(error.message + (failureEvidenceSummary(error) ? ';' + failureEvidenceSummary(error) : ''), error.code, { cause: error });
178
+ throw new LlmError(employeeFailure(error.code, this.agent.loginProvider), error.code, { cause: error });
290
179
  if (error instanceof NeedError) {
291
180
  const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
292
181
  const evidence = failureEvidenceSummary(error);
293
- const message = (connectNeedMessage(error) ?? employeeFailure(error.code, this.agent.loginProvider)) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '') + (evidence ? ';' + evidence : '');
182
+ const message = connectNeedMessage(error) ?? employeeFailure(error.code, this.agent.loginProvider);
294
183
  const login = originalLoginFailure(error);
295
184
  let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
296
185
  if (login?.technical && this.reportLoginFailure) {
@@ -302,11 +191,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
302
191
  diagnostic += ';诊断保存失败:' + (failure instanceof NeedError ? failure.code : 'DIAGNOSTIC_WRITE_FAILED');
303
192
  }
304
193
  }
305
- throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (diagnostic ? ';' + diagnostic : '')) });
194
+ throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (evidence ? ';' + evidence : '') + (diagnostic ? ';' + diagnostic : '')) });
306
195
  }
307
- if (error instanceof LlmError && failureEvidenceSummary(error))
308
- throw new LlmError(error.failure.message + ';' + failureEvidenceSummary(error), error.code, { cause: error });
309
- throw error;
196
+ throw new LlmError(employeeFailure('TURN_FAILED', this.agent.loginProvider), 'TURN_FAILED', { cause: error });
310
197
  }
311
198
  }
312
199
  async *generate(original) {
@@ -322,7 +209,6 @@ export class CodexRemoteAdapter extends LlmAdapter {
322
209
  this.turns.set(key, turn);
323
210
  const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
324
211
  const options = { ...original, signal };
325
- const timing = new StageTiming();
326
212
  const abortOffs = [];
327
213
  let request;
328
214
  let earlyStage = 'input-prepare';
@@ -335,18 +221,19 @@ export class CodexRemoteAdapter extends LlmAdapter {
335
221
  let failureCode;
336
222
  let preserveRecoveredRuntime = false;
337
223
  let activeMapper;
338
- let catalog;
339
224
  let selected;
340
225
  let modelConfiguration;
341
226
  let turnFailure;
342
227
  let employeeNoticeShown = false;
228
+ let permissionOff;
229
+ let permissionEpoch = new AbortController();
343
230
  const information = [];
344
- const recordInformation = () => { timing.summarize(); if (runtime?.details)
231
+ const recordInformation = () => { if (runtime?.details)
345
232
  for (const entry of information.splice(0))
346
233
  runtime.details.record(entry); };
347
234
  const config = this.config();
348
- const mode = this.agent.oneShot ? config.mode : 'session';
349
- request = { sessionId: options.sessionId, mode: mode ?? 'session', timing, signal };
235
+ const mode = 'session';
236
+ request = { sessionId: options.sessionId, mode: mode ?? 'session', signal };
350
237
  const observeAbort = (source, initiator, reason) => { if (!source)
351
238
  return; const record = () => { (request?.closeDiagnostics ?? runtime?.closeDiagnostics)?.record({ layer: 'adapter', initiator, reason, action: 'abort', changes: [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] }); }; source.addEventListener('abort', record, { once: true }); abortOffs.push(() => source.removeEventListener('abort', record)); };
352
239
  observeAbort(original.signal, 'signal.caller', 'CALLER_ABORTED');
@@ -356,93 +243,48 @@ export class CodexRemoteAdapter extends LlmAdapter {
356
243
  if (this.preflightDiagnostics)
357
244
  request.recordEarlyFailure = (error, stage) => this.preflightDiagnostics.failure(request, this.agent.loginProvider, error, stage);
358
245
  try {
359
- if (!mode || (mode === 'session' && (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)))
360
- throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的mode/acpMaxMessageBytes');
246
+ if (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)
247
+ throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的acpMaxMessageBytes');
361
248
  const previous = readPointer(options, this.agent);
362
- employeeNoticeShown = !!key && this.employeeNoticeSessions.has(key) || employeeNoticePreviouslyShown(options);
249
+ employeeNoticeShown = !!key && this.employeeNoticeSessions.has(key) || false;
363
250
  if (employeeNoticeShown && key)
364
251
  this.employeeNoticeSessions.add(key);
365
- const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
252
+ const last = options.messages.findLast(message => message.role === 'assistant');
366
253
  const missingPointer = !!last && !previous;
367
- let requireFreshSession = missingPointer || !!key && this.preparationFailures.has(key);
368
- if (previous && previous.mode !== mode)
369
- throw new NeedError('SESSION_CONTEXT_MISMATCH', '运行模式与原会话不一致,请新建会话', '原会话模式');
370
- if (previous && !previous.modelConfiguration)
371
- information.push({ kind: 'session', severity: 'info', code: 'TURN_GATE_INFORMATION', summary: '历史回复没有模型配置资料;本轮会重新确认你当前选择的模型。', payload: { reason: 'HISTORICAL_MODEL_CONFIGURATION_ABSENT' } });
372
- live = key ? this.sessions.get(key) : undefined;
373
- if (live?.disposal || live?.cleanupFailure)
374
- throw live.cleanupFailure ?? new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话正在结束,清理结果尚未确认,不能继续或重开此会话', '管理员核对原主机会话清理结果');
375
- if (live?.failure)
376
- throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '上一轮输入已经交给远端,但结果未确认,不能自动重发', '核对已发送输入及原主机会话状态');
377
- if (live && !previous) {
378
- if (typeof live.runtime.dispose !== 'function')
379
- throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原会话不能确认已安全结束,本轮尚未发送', '可确认结束原实例的通道');
380
- live.session.close({ layer: 'adapter', initiator: 'adapter.missing-pointer', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'close' });
381
- await this.cleanupRuntime(live.runtime, { layer: 'adapter', initiator: 'adapter.missing-pointer', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'dispose' });
382
- if (key && this.sessions.get(key) === live)
383
- this.sessions.delete(key);
384
- live = undefined;
385
- requireFreshSession = true;
386
- }
387
- const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
388
- if (live && live.configuration !== configuration)
389
- throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
390
- Object.assign(request, { timing, employeeHttpMcpPolicy: config.employeeHttpMcpPolicy ?? DEFAULT_CONFIG.employeeHttpMcpPolicy, acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, recoveryCheckpointId: previous?.recoveryCheckpointId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, retainProcess: !!key && mode === 'session', signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
391
- if (requireFreshSession) {
392
- request.requireFreshSession = true;
393
- request.resumeSessionId = undefined;
394
- request.recoveryCheckpointId = undefined;
395
- information.push({ kind: 'session', severity: 'warning', code: 'TURN_GATE_INFORMATION', summary: '缺少可用的旧会话恢复资料,或上次在发送前已失败;将核对旧实例安全结束后,只处理本次接纳的输入。', payload: { reason: missingPointer ? 'PRIVATE_RECOVERY_POINTER_ABSENT' : 'PRIOR_PREPARATION_FAILED' } });
396
- }
254
+ live = undefined;
255
+ const configuration = JSON.stringify([config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
256
+ Object.assign(request, { acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
397
257
  request.previousContextSha256 = previous?.contextSha256;
398
- const after = options.messages.findLastIndex(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
399
- request.userInputIds = Object.freeze(options.messages.slice(after + 1).flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []));
258
+ const after = options.messages.findLastIndex(message => message.role === 'assistant');
259
+ const tail = options.messages.slice(after + 1);
260
+ const humanIds = tail.flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []);
261
+ // An official approval may continue the same native turn without another
262
+ // human message. This is a CAS candidate only: live turn/L proof must still
263
+ // bind it. The actual input delta remains prepareInput's unchanged tail.
264
+ if (!humanIds.length && tail.some(message => message.role === 'user' && message.source?.kind === 'user-approval')) {
265
+ const original = options.messages.slice(0, after + 1).findLast(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string');
266
+ if (typeof original?.id === 'string')
267
+ humanIds.push(original.id);
268
+ }
269
+ request.userInputIds = Object.freeze(humanIds);
400
270
  request.userMessageIds = request.userInputIds;
401
271
  earlyStage = 'source-context';
402
272
  await this.resolveRuntime.preflight?.(request);
403
273
  earlyStage = 'input-prepare';
404
- let input = await timing.run('input-prepare', () => prepareInput(options, this.readImage));
274
+ let input = await prepareInput(options, this.readImage);
405
275
  const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
406
276
  request.contextSha256 = contextSha256;
407
277
  const codex = this.agent.loginProvider === 'codex';
408
- if (codex && live && previous && live.contextSha256 !== previous.contextSha256.toLowerCase())
409
- throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '持久上下文摘要与当前远端会话的初建摘要不一致,本轮未发送', '原会话一致的初建上下文摘要');
410
- const contextChanged = !!previous && codex && previous.contextSha256.toLowerCase() !== contextSha256;
278
+ const contextChanged = !!previous && codex && (previous.contextSha256 ?? '').toLowerCase() !== contextSha256;
411
279
  // Only identical durable context can prove a legacy context-bound journal.
412
280
  // New Codex transport scopes are independent of the native context digest.
413
281
  if (contextChanged)
414
282
  request.contextSha256 = undefined;
415
283
  const claudeContextChanged = !!previous && !codex && previous.contextSha256 !== contextSha256;
416
- if (contextChanged)
417
- input = await prepareContinuation(options, input, this.readImage);
418
- if (live) {
419
- if (contextChanged)
420
- live.session.validateNewSessionContext({ systemPrompt: input.systemPrompt });
421
- else
422
- live.session.validateContext({ systemPrompt: input.systemPrompt });
423
- live.session.validatePrompt(input.prompt);
424
- }
425
- if (mode === 'one-shot' && (input.systemPrompt || input.prompt.some(block => block.type !== 'text')))
426
- throw new NeedError('ONESHOT_CONTEXT_REQUIRED', '首版单次模式不支持图片或系统/开发者上下文,请使用会话模式', 'Codex会话模式');
427
284
  if (mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
428
285
  throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
429
- if (mode === 'one-shot')
430
- throw new NeedError('MODEL_CONFIGURATION_UNSUPPORTED', '单次模式不支持确认当前模型与推理等级,请改用会话模式', '支持模型配置确认的会话模式');
431
- request.localAdmissionCheck?.();
432
- earlyStage = 'model-catalog';
433
- catalog = await this.catalog(signal);
434
- selected = catalog.models.find(model => model.id === options.model);
435
- catalog.onInformation?.(entry => information.push(entry));
436
- if (!selected)
437
- throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
438
- if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
439
- throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
440
- catalog.bindSelection?.(selected.id, options.reasoningEffort);
441
- const observeSignal = (source, initiator, reason) => { const observed = () => { const diagnostic = request?.closeDiagnostics ?? runtime?.closeDiagnostics; if (diagnostic)
442
- diagnostic.record({ layer: 'adapter', initiator, reason: catalog?.signal === source ? catalog.revocation?.()?.reason ?? reason : reason, action: 'abort', changedFields: catalog?.signal === source ? catalog.revocation?.()?.fields : undefined, changes: catalog?.signal === source ? catalog.revocation?.()?.changes ?? [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] : [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] }); }; source.addEventListener('abort', observed, { once: true }); abortOffs.push(() => source.removeEventListener('abort', observed)); };
443
- observeSignal(catalog.signal, 'signal.model-catalog', 'MODEL_CATALOG_REVOKED');
444
- observeSignal(signal, 'signal.turn-composite', 'ABORTED');
445
- let modelSignal = AbortSignal.any([signal, catalog.signal]);
286
+ selected = this.modelPresentation.find(options.model) ?? { id: options.model, value: options.model, name: options.model, resolvedModel: null, reasoning: { options: [], defaultValue: null, defaultKnown: false }, source: null };
287
+ let modelSignal = signal;
446
288
  options.signal = modelSignal;
447
289
  request.signal = modelSignal;
448
290
  earlyStage = 'runtime-resolve';
@@ -450,8 +292,6 @@ export class CodexRemoteAdapter extends LlmAdapter {
450
292
  const closeSink = runtime.details;
451
293
  runtime.closeDiagnostics ??= new CloseDiagnostics(event => closeSink?.record(event));
452
294
  signal.throwIfAborted();
453
- if (requireFreshSession && (!runtime.freshSessionConfirmed || !runtime.restartNotice))
454
- throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '未确认原实例安全结束或原位置为空,本轮没有发送', '同UUID条件结束与list消失证明,或原slot不存在证明');
455
295
  if (runtime.execution?.signal) {
456
296
  modelSignal = AbortSignal.any([modelSignal, runtime.execution.signal]);
457
297
  options.signal = modelSignal;
@@ -464,42 +304,17 @@ export class CodexRemoteAdapter extends LlmAdapter {
464
304
  request.signal = modelSignal;
465
305
  modelSignal.throwIfAborted();
466
306
  }
467
- catalog.assertCurrent();
468
- if (runtime.connection && (runtime.connection.instanceId !== catalog.target.instanceId || runtime.connection.epoch !== catalog.target.epoch) || runtime.identity !== `${catalog.target.target.username}@${catalog.target.target.host}:${catalog.target.target.port}`)
469
- throw new ModelConfigurationError('模型目录与当前会话可信员工主机不一致');
470
307
  if (previous && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
471
308
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
472
- if (contextChanged && runtime.recoveredSession) {
473
- const capabilities = runtime.recoveredSession.capabilities;
474
- // A clean reattach has sent no ACP input. A known policy refusal must
475
- // detach it, preserving the original native session and checkpoint.
476
- if (!capabilities.sessionCapabilities?.systemPrompt || !supportsCodexContextPolicy(capabilities)) {
477
- preserveRecoveredRuntime = true;
478
- if (!capabilities.sessionCapabilities?.systemPrompt)
479
- throw new NeedError('SYSTEM_PROMPT_UNSUPPORTED', '远端未声明系统与开发者上下文能力,不能丢弃这些要求继续', '支持 systemPrompt 的公开 ACP 能力');
480
- throw new NeedError('SYSTEM_PROMPT_POLICY_UNSUPPORTED', '远端没有明确声明Codex上下文仅新建生效的兼容政策,本轮未发送', 'systemPrompt与精确的aiWorkbenchSystemPrompt新建政策');
481
- }
482
- }
483
- if (runtime.restartNotice) {
484
- const ids = runtime.restartInputIds;
485
- if (!ids?.length || new Set(ids).size !== ids.length || ids.some(id => input.users.filter(u => u.id === id).length !== 1))
486
- throw new NeedError('CURRENT_INPUT_UNCONFIRMED', '无法核对本次输入,尚未发送给模型', '官方当前步骤的已接纳用户输入');
487
- input = { ...input, prompt: ids.flatMap(id => input.users.find(u => u.id === id).prompt) };
488
- if (contextChanged)
489
- input = await timing.run('input-prepare', () => prepareContinuation(options, input, this.readImage));
490
- }
491
309
  input = projectHostWorkspaceInput(input, runtime.hostWorkspace);
492
310
  if (runtime.hostWorkspace && mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
493
311
  throw new NeedError('ACP_MESSAGE_TOO_LARGE', '本轮输入超过已设置的会话消息容量,未截断或发送', '管理员调整acpMaxMessageBytes');
494
- const details = runtime.details;
495
- if (!details)
496
- throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
312
+ const details = runtime.details ?? await createRemoteDetails({ warn: line => console.warn(line) }).open({ sessionId: options.sessionId ?? 'unscoped', provider: this.agent.loginProvider, identity: runtime.identity, connection: runtime.connection });
497
313
  recordInformation();
498
- timing.bind(detail => details.record(detail));
499
314
  request.recordEarlyFailure = undefined;
500
315
  if (runtime.hostWorkspace)
501
316
  details.record({ kind: 'session', severity: 'info', code: 'HOST_WORKSPACE_CONTEXT', summary: '本轮模型工作区上下文及审批会话目录使用已确认的主机目录。', payload: { source: 'host', cwd: runtime.hostWorkspace.cwd } });
502
- const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
317
+ const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
503
318
  activeMapper = mapper;
504
319
  let id;
505
320
  let stop;
@@ -518,17 +333,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
518
333
  }
519
334
  yield* mapper.flush();
520
335
  }
521
- if (runtime.workspaceNotice) {
522
- yield* mapper.text(runtime.workspaceNotice.text);
523
- yield* mapper.flush();
524
- }
525
- if (runtime.restartNotice) {
526
- yield* mapper.text(runtime.restartNotice + '\n\n');
527
- yield* mapper.flush();
528
- }
529
336
  if (mode === 'session') {
530
- if (live && live.runtime !== runtime)
531
- throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
532
337
  if (!runtime.acpTransport)
533
338
  throw new NeedError('DEPENDENCY_REQUIRED', '我需要该员工主机上的ACP通道', 'S提供的ACP命令与aw-session');
534
339
  const queue = [];
@@ -536,7 +341,15 @@ export class CodexRemoteAdapter extends LlmAdapter {
536
341
  let done = false;
537
342
  let failure;
538
343
  const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
539
- const hooks = { onEmployeeInfo: summary => push(mapper.text(summary + '\n\n')), closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
344
+ let promptPermit;
345
+ const hooks = { closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, ...this.permissions ? { promptGuard: () => {
346
+ if (!promptPermit)
347
+ throw new NeedError('PERMISSION_POLICY_CHANGED', '当前权限没有保持确认,本次没有继续派发。', '当前权限代次和同值回执');
348
+ return promptPermit;
349
+ }, approvalGuard: () => {
350
+ const source = this.permissions, epoch = permissionEpoch, policy = JSON.stringify(source.read(options.sessionId));
351
+ return { signal: epoch.signal, isCurrent: () => !epoch.signal.aborted && JSON.stringify(source.read(options.sessionId)) === policy };
352
+ } } : {}, approve: async (permission, approvalSignal) => {
540
353
  const execution = runtime.execution;
541
354
  const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
542
355
  if (execution) {
@@ -545,99 +358,120 @@ export class CodexRemoteAdapter extends LlmAdapter {
545
358
  push(mapper.text('\n这类操作尚不支持在本机执行,已取消本次审批,没有在主机代跑。\n'));
546
359
  return 'cancelled';
547
360
  }
548
- await execution.validate(approvalSignal);
549
361
  }
550
362
  const answer = await this.approve?.(permission, approvalSignal, options, execution && toolName?.startsWith('mcp__employee__') ? 'employee-local' : undefined, runtime.hostWorkspace) ?? 'unavailable';
551
- if (execution && answer === 'allowed-once')
552
- await execution.validate(approvalSignal);
553
363
  return answer;
554
- }, onUpdate: notification => { try {
364
+ }, onEmployeeNotice: notice => push(mapper.text(notice + '\n\n')), onUpdate: notification => { try {
555
365
  push(mapper.acp(notification.update));
556
366
  }
557
367
  catch (error) {
558
368
  failure = error;
559
369
  session?.close({ layer: 'adapter', initiator: 'adapter.mapper-failure', reason: closeCode(error, 'MAPPING_FAILED'), action: 'close' });
560
370
  } }, onResponse: response => push(mapper.acpResponse(response)), onDetail: detail => details.record(detail) };
561
- const fresh = !live;
562
- if (fresh) {
563
- const transport = runtime.acpTransport instanceof acp.AgentApp || runtime.acpTransport instanceof unstable.AgentApp ? runtime.acpTransport : acp.ndJsonStream(runtime.acpTransport.writable, runtime.acpTransport.readable, { maxMessageBytes: config.acpMaxMessageBytes });
564
- session = new AcpSession(transport, hooks);
565
- live = { session, runtime, configuration, contextSha256, usageSnapshot: mapper.usageSnapshot };
566
- if (key)
567
- this.sessions.set(key, live);
568
- }
569
- else {
570
- session = live.session;
571
- session.setHooks(hooks);
572
- if (previous?.sessionId !== session.id)
573
- throw new NeedError('SESSION_STATE_INVALID', '持久会话标识与当前进程不一致', '原会话的恢复标识');
574
- }
371
+ const transport = runtime.acpTransport instanceof acp.AgentApp || runtime.acpTransport instanceof unstable.AgentApp ? runtime.acpTransport : acp.ndJsonStream(runtime.acpTransport.writable, runtime.acpTransport.readable, { maxMessageBytes: config.acpMaxMessageBytes });
372
+ session = new AcpSession(transport, hooks);
373
+ live = { session, runtime, configuration, contextSha256, usageSnapshot: mapper.usageSnapshot };
374
+ if (key)
375
+ this.sessions.set(key, live);
575
376
  promptBaseline = session.promptDispatchCount;
377
+ let permissionTail = Promise.resolve(), permissionKey, observedPermissionKey, permissionQueued = false;
378
+ const permissionFailed = (error) => {
379
+ failure ??= error;
380
+ runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.permission-sync.failure', reason: closeCode(error, 'PERMISSION_MODE_UNCONFIRMED'), action: 'close' });
381
+ session.close({ layer: 'adapter', initiator: 'adapter.permission-sync.failure', reason: closeCode(error, 'PERMISSION_MODE_UNCONFIRMED'), action: 'close' });
382
+ wake?.();
383
+ };
384
+ const syncPermissions = () => {
385
+ if (!this.permissions)
386
+ return Promise.resolve(); // Explicit component-test port; production always supplies the official source.
387
+ return permissionTail = permissionTail.then(async () => {
388
+ const policy = this.permissions.read(options.sessionId), next = JSON.stringify(policy);
389
+ if (next === permissionKey)
390
+ return;
391
+ const remoteMode = permissionMode(this.agent.loginProvider, policy);
392
+ details.record({ kind: 'session', severity: 'info', code: 'DSH_PERMISSION_POLICY', summary: '按DSH当前会话权限同步远程工具;不会把关闭审批提示解释为自动批准。', payload: { ...policy, remoteMode } });
393
+ await session.selectPermissionMode(remoteMode, modelSignal, this.agent.loginProvider);
394
+ permissionKey = next;
395
+ });
396
+ };
397
+ const dispatchPrompt = async () => {
398
+ if (!this.permissions)
399
+ return session.prompt(input.prompt, modelSignal);
400
+ // New official policy events can append work while an ACK is awaited.
401
+ // Setters for those distinct policy changes are not retries. Dispatch
402
+ // only from one synchronous, stable epoch/ACK boundary.
403
+ for (;;) {
404
+ const barrier = syncPermissions();
405
+ await barrier;
406
+ if (barrier !== permissionTail || permissionQueued)
407
+ continue;
408
+ const source = this.permissions, policy = JSON.stringify(source.read(options.sessionId));
409
+ if (policy !== permissionKey)
410
+ continue;
411
+ const epoch = permissionEpoch;
412
+ promptPermit = { signal: epoch.signal, isCurrent: () => !epoch.signal.aborted && permissionKey === policy && JSON.stringify(source.read(options.sessionId)) === policy };
413
+ return session.prompt(input.prompt, modelSignal);
414
+ }
415
+ };
576
416
  const task = (async () => {
577
417
  remoteAttempted = true;
578
418
  try {
579
- if (fresh && runtime.recoveredSession) {
580
- if (previous && runtime.recoveredSession.sessionId !== previous.sessionId)
581
- throw new NeedError('SESSION_STATE_INVALID', '已接回的会话与持久标识不一致,本轮未发送', '原会话的恢复标识');
582
- session.restore(runtime.execution?.hostCwd ?? runtime.cwd, runtime.recoveredSession, { systemPrompt: input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}) }, codex && !!previous && !contextChanged);
583
- session.validatePrompt(input.prompt);
584
- if (contextChanged)
585
- await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
586
- else if (claudeContextChanged)
587
- await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal, true);
588
- }
589
- else if (fresh)
590
- await session.start(runtime.execution?.hostCwd ?? runtime.cwd, contextChanged || runtime.restartNotice ? undefined : previous?.sessionId, modelSignal, { systemPrompt: contextChanged ? (input.systemPrompt ?? '') : input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}), ...(runtime.employeeExecutionMeta ? { employeeExecution: runtime.employeeExecutionMeta } : {}) }, input.prompt);
591
- else if (contextChanged)
592
- await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
593
- else {
594
- await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal);
595
- }
596
- if (fresh && claudeContextChanged && !runtime.recoveredSession && !runtime.restartNotice)
597
- details.record({ kind: 'session', severity: 'warning', code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', summary: '新要求已通过原会话恢复接口传送;官方仅承诺下次上下文整理后生效,本轮不能保证已立即采用。', payload: { reason: 'OFFICIAL_CONTEXT_UPDATE_DEFERRED' } });
598
- if (!codex)
599
- live.contextSha256 = contextSha256;
600
- if (contextChanged) {
601
- if (session.id === previous.sessionId)
602
- throw new NeedError('SESSION_STATE_INVALID', '新会话标识与原会话相同,无法确认独立承接,本轮未发送', '独立的新远端会话标识');
603
- live.contextSha256 = contextSha256;
604
- live.usageSnapshot = mapper.usageSnapshot;
605
- details.record({ kind: 'session', severity: 'info', code: 'CODEX_CONTEXT_SESSION_REPLACED', summary: 'Codex要求仅在新建生效,已保留原会话;新会话承接可见用户及助手正文,内部推理不转发', payload: { reason: 'system-context-changed', oldSessionId: previous.sessionId, newSessionId: session.id, oldContextSha256: previous.contextSha256, newContextSha256: contextSha256, history: input.historyTransfer } });
606
- }
607
- catalog.assertCurrent();
419
+ await session.start(runtime.execution?.hostCwd ?? runtime.cwd, previous?.sessionId, modelSignal, { systemPrompt: input.systemPrompt, ...(runtime.execution ? { employeeRouting: runtime.execution.routing } : {}), ...(codex ? { codexNewSessionOnly: true } : {}), ...(runtime.employeeExecutionMeta ? { employeeExecution: runtime.employeeExecutionMeta } : {}) }, input.prompt);
608
420
  const actual = await session.selectModel(selected.value, options.reasoningEffort, modelSignal);
609
- catalog.assertCurrent();
610
- session.assertModelSelection(selected.value, options.reasoningEffort);
611
421
  // This lease is tied to the trusted host-user and instance/epoch.
612
422
  // Its mapping and this turn's exact setter ACK are different facts.
613
- const mapping = catalog.models.filter(model => model.value === selected.value);
614
- if (mapping.length !== 1 || mapping[0].id !== selected.id)
615
- throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '所选模型在当前主机目录中没有唯一对应型号,本轮未发送', 'CATALOG_VALUE_MAPPING_NOT_UNIQUE');
616
- const resolved = mapping[0].resolvedModel;
617
- if (typeof resolved !== 'string' || !resolved.trim())
618
- throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '主机目录没有提供所选模型的具体型号,本轮未发送', 'CATALOG_RESOLVED_MODEL_MISSING');
619
- modelConfiguration = { selected: { ...mapping[0], ...(options.reasoningEffort === undefined ? {} : { effort: options.reasoningEffort }) }, configured: actual, resolvedModel: resolved, status: 'remote-configured', ...(this.agent.loginProvider === 'claude' ? { resolvedModelSource: 'host-user-catalog', sessionModelReported: false } : {}) };
620
- details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
423
+ modelConfiguration = { selected: { ...selected, ...(options.reasoningEffort === undefined ? {} : { effort: options.reasoningEffort }) }, configured: actual, resolvedModel: selected.resolvedModel ?? selected.value, status: 'remote-configured', ...(this.agent.loginProvider === 'claude' ? { resolvedModelSource: 'host-user-catalog', sessionModelReported: false } : {}) };
424
+ details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮模型设置回应;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
621
425
  }
622
426
  catch (error) {
623
427
  details.record({ kind: 'session', severity: 'action-required', code: 'MODEL_CONFIGURATION_FAILED', summary: '本轮模型配置未获完整确认,未发送用户输入', payload: { selected, requestedEffort: options.reasoningEffort ?? null, actual: session.modelConfiguration ?? null, reason: error instanceof NeedError ? (error.code === 'MODEL_RESOLUTION_UNCONFIRMED' ? error.requires : error.code) : error instanceof ModelConfigurationError ? error.message : 'MODEL_CONFIGURATION_FAILED' } });
624
428
  throw error;
625
429
  }
626
- await timing.run('details-flush', () => details.flush());
430
+ if (this.permissions && options.sessionId) {
431
+ observedPermissionKey = JSON.stringify(this.permissions.read(options.sessionId));
432
+ permissionOff = this.permissions.subscribe(options.sessionId, () => {
433
+ let next;
434
+ try {
435
+ next = JSON.stringify(this.permissions.read(options.sessionId));
436
+ }
437
+ catch (error) {
438
+ permissionEpoch.abort();
439
+ permissionFailed(error);
440
+ return;
441
+ }
442
+ if (next === observedPermissionKey) {
443
+ details.record({ kind: 'session', severity: 'info', code: 'DSH_PERMISSION_EVENT_UNCHANGED', summary: '设置事件没有改变有效权限,原审批与执行许可保持有效。' });
444
+ return;
445
+ }
446
+ // Compare observed values, not the possibly lagging remote ACK.
447
+ // A real A→B→A still revokes the original permit twice.
448
+ observedPermissionKey = next;
449
+ permissionEpoch.abort();
450
+ permissionEpoch = new AbortController();
451
+ // One official preset writes identity and two knobs synchronously.
452
+ // Read the complete committed bundle after that append stack.
453
+ if (permissionQueued)
454
+ return;
455
+ permissionQueued = true;
456
+ queueMicrotask(() => { permissionQueued = false; if (!permissionOff)
457
+ return; void syncPermissions().catch(permissionFailed); });
458
+ });
459
+ await syncPermissions();
460
+ }
627
461
  recordInformation();
628
- await details.flush();
629
462
  details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
630
- catalog.assertCurrent();
631
- session.assertModelSelection(selected.value, options.reasoningEffort);
632
463
  await runtime.execution?.beginTurn(modelSignal);
633
464
  id = session.id;
634
465
  try {
635
- const result = await session.prompt(input.prompt, modelSignal);
636
- if (contextChanged)
637
- push(mapper.text(runtime.restartNotice ? '\n\n会话要求已变化,已把此前可见对话带入新 Codex 会话继续处理。' : '\n\n会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。'));
466
+ const result = await dispatchPrompt();
467
+ permissionOff?.();
468
+ permissionOff = undefined;
469
+ await permissionTail;
638
470
  return result;
639
471
  }
640
472
  finally {
473
+ permissionOff?.();
474
+ permissionOff = undefined;
641
475
  runtime.execution?.endTurn();
642
476
  details.endTurn();
643
477
  }
@@ -657,22 +491,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
657
491
  for (const chunk of mapper.text('\n[远端拒绝] 此请求未执行。\n'))
658
492
  yield chunk;
659
493
  }
660
- else {
661
- if (!runtime.oneShot)
662
- throw new NeedError('DEPENDENCY_REQUIRED', '我需要主机单次运行助手', 'S安装的aw-codex-once');
663
- details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
664
- const text = input.prompt.map(block => block.type === 'text' ? block.text : '').join('\n');
665
- for await (const event of runOneShot(runtime.oneShot, { wrapperPath: fileURLToPath(new URL('./sdk-ssh-wrapper.js', import.meta.url)), sandboxMode: config.oneShotSandbox, approvalPolicy: config.oneShotApproval }, text, previous?.sessionId, signal)) {
666
- if (event.type === 'thread.started')
667
- id = event.thread_id;
668
- for (const chunk of mapper.sdk(event))
669
- yield chunk;
670
- }
671
- }
494
+ mapper.flushMetadata();
672
495
  recordInformation();
673
496
  details.endTurn();
674
- await timing.run('details-flush', () => details.flush());
675
- await details.flush(); // The timing record is a new ordered write, not a retry.
676
497
  if (!id)
677
498
  throw new NeedError('SESSION_ID_MISSING', '远端没有返回会话标识', '有效的远端会话标识');
678
499
  if (live)
@@ -680,22 +501,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
680
501
  if (modelConfiguration) {
681
502
  const actual = session.modelConfiguration;
682
503
  const changed = !actual?.confirmed || actual.model !== modelConfiguration.configured.model || actual.effort !== modelConfiguration.configured.effort;
683
- catalog.assertCurrent();
684
504
  if (changed) {
685
505
  details.record({ kind: 'session', severity: 'warning', code: 'MODEL_CONFIGURATION_CHANGED_DURING_TURN', summary: '本轮回复已经完成,期间远端配置发生变化;保留真实回复,具体执行型号未单独回报。', payload: { reason: 'POST_PROMPT_CONFIGURATION_CHANGED', configured: modelConfiguration.configured, actual: actual ?? null } });
686
- await details.flush();
687
506
  }
688
507
  }
689
508
  yield* mapper.flush();
690
- let recoveryCheckpointId;
691
- if (session?.checkpoint && runtime.saveCheckpoint) {
692
- const id = randomUUID();
693
- if (await runtime.saveCheckpoint(id, session.checkpoint))
694
- recoveryCheckpointId = id;
695
- }
696
- const previousRemoteSessions = [...(previous?.previousRemoteSessions ?? []), ...(contextChanged ? [{ sessionId: previous.sessionId, contextSha256: previous.contextSha256 }] : [])];
697
- const presentation = { ...(runtime.workspaceNotice ? { workspaceNotice: runtime.workspaceNotice } : {}), ...(employeeNoticeShown ? { employeeExecutionNotice: 'local-v1' } : {}) };
698
- const pointer = { ...(previousRemoteSessions.length ? { previousRemoteSessions } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), ...(recoveryCheckpointId ? { recoveryCheckpointId } : {}), ...(Object.keys(presentation).length ? { presentation } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
509
+ const pointer = { ...(modelConfiguration ? { modelConfiguration } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
699
510
  succeeded = true;
700
511
  yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
701
512
  }
@@ -711,104 +522,44 @@ export class CodexRemoteAdapter extends LlmAdapter {
711
522
  recordInformation();
712
523
  failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
713
524
  withRemoteFailureEvidence(error, runtime?.employeeRemoteCloseReason?.(), failureCode);
714
- if (live && session && session.promptDispatchCount > promptBaseline)
715
- live.failure = { cause: error };
716
- const reporting = [];
717
- const reportFailure = async () => {
718
- if (request?.recordEarlyFailure) {
719
- const record = request.recordEarlyFailure;
720
- request.recordEarlyFailure = undefined;
721
- try {
722
- await record(Object.assign(new NeedError(failureCode ?? 'TURN_PREPARATION_FAILED', '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
723
- }
724
- catch (failure) {
725
- reporting.push(failure);
726
- }
727
- }
728
- try {
729
- await runtime?.details?.flush();
730
- }
731
- catch (failure) {
732
- reporting.push(failure);
733
- }
734
- };
735
- if (runtime && key && live && live.runtime === runtime && this.sessions.get(key) === live) {
736
- const captured = runtime;
737
- void reportFailure().then(() => { for (const failure of reporting)
738
- for (const code of closeErrorCodes(failure, 'DIAGNOSTIC_WRITE_FAILED'))
739
- captured.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-details', reason: code, action: 'failure' }); }, failure => captured.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-details', reason: closeCode(failure, 'DIAGNOSTIC_WRITE_FAILED'), action: 'failure' }));
740
- }
741
- else
742
- await reportFailure();
525
+ activeMapper?.flushMetadata();
526
+ if (request?.recordEarlyFailure)
527
+ void request.recordEarlyFailure(error, earlyStage).catch(() => console.warn('[remote-agent] 预检诊断未保存'));
743
528
  if (activeMapper)
744
529
  yield* activeMapper.flush();
745
- const firstReason = request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason;
746
- if (reporting.length) {
747
- turnFailure = withFailureEvidence(error, reporting, 'diagnostic', firstReason, failureCode);
748
- throw turnFailure;
749
- }
750
- turnFailure = firstReason && !(error instanceof NeedError || error instanceof ModelConfigurationError || error instanceof LlmError) ? withFailureEvidence(error, [], 'diagnostic', firstReason, failureCode) : withFirstFailureEvidence(error, firstReason, failureCode);
751
- throw turnFailure;
530
+ throw withFirstFailureEvidence(error, runtime?.closeDiagnostics?.first?.reason, failureCode);
752
531
  }
753
532
  finally {
533
+ permissionOff?.();
534
+ permissionOff = undefined;
535
+ permissionEpoch.abort();
754
536
  for (const off of abortOffs)
755
537
  off();
756
538
  runtime?.execution?.endTurn();
757
- catalog?.release();
758
- try {
759
- if (preserveRecoveredRuntime || this.shuttingDown || key && this.detached.has(key)) {
760
- session?.close({ layer: 'adapter', initiator: 'adapter.turn-detach', reason: 'TURN_DETACHED', action: 'close' });
761
- await runtime?.detach?.({ layer: 'adapter', initiator: 'adapter.turn-detach', reason: 'TURN_DETACHED', action: 'detach' });
762
- }
763
- else if (!succeeded || !key || mode !== 'session') {
764
- session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
765
- if (runtime) {
766
- if (!succeeded && turnFailure && key && live && live.runtime === runtime && this.sessions.get(key) === live)
767
- this.finishFailedTurn(key, live, turnFailure, failureCode ?? 'TURN_PREPARATION_FAILED', request?.closeDiagnostics?.first?.reason ?? runtime.closeDiagnostics?.first?.reason);
768
- else {
769
- await this.cleanupRuntime(runtime);
770
- if (key && live && !live.failure && this.sessions.get(key) === live) {
771
- if (typeof runtime.dispose === 'function') {
772
- this.sessions.delete(key);
773
- if (!succeeded)
774
- this.preparationFailures.set(key, failureCode ?? 'TURN_PREPARATION_FAILED');
775
- }
776
- else
777
- live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
778
- }
779
- }
780
- }
781
- }
782
- if (succeeded && key)
783
- this.preparationFailures.delete(key);
784
- }
785
- catch (cleanup) {
786
- throw withFailureEvidence(turnFailure ?? cleanup, [cleanup], 'cleanup', request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason, failureCode);
787
- }
788
- finally {
789
- if (key && this.turns.get(key) === turn)
790
- this.turns.delete(key);
791
- if (key)
792
- this.detached.delete(key);
539
+ if (runtime) {
540
+ session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
541
+ if (key && this.sessions.get(key) === live)
542
+ this.sessions.delete(key);
543
+ void this.cleanupRuntime(runtime);
793
544
  }
545
+ if (key && this.turns.get(key) === turn)
546
+ this.turns.delete(key);
547
+ if (key)
548
+ this.detached.delete(key);
794
549
  }
795
550
  }
796
551
  }
797
552
  /** One official registration owns both routes, preserving opaque replay ownership. */
798
553
  export class RemoteAgentsAdapter extends LlmAdapter {
799
554
  adapters = new Map();
800
- constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics) { super(); for (const agent of AGENTS)
801
- this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics)); }
555
+ constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions) { super(); for (const agent of AGENTS)
556
+ this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics, permissions)); }
802
557
  adapter(provider) { const adapter = this.adapters.get(provider); if (!adapter)
803
558
  throw new LlmError('远程模型未注册', 'PROVIDER_NOT_FOUND'); return adapter; }
804
559
  providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
805
560
  listModels(provider) { return this.adapter(provider).listModels(provider); }
806
561
  resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
807
562
  prepareCall(provider, model, signal) { return this.adapter(provider).prepareCall(provider, model, signal); }
808
- observeModelInvalidation(fields, changes = []) { for (const adapter of this.adapters.values())
809
- adapter.observeModelInvalidation(fields, changes); }
810
- async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
811
- async invalidateEmployeeState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateEmployeeState())); }
812
563
  providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
813
564
  stream(options) { return this.adapter(options.provider).stream(options); }
814
565
  async disposeSession(sessionId) { await Promise.all([...this.adapters.values()].map(adapter => adapter.disposeSession(sessionId))); }
@@ -818,7 +569,7 @@ export class RemoteAgentsAdapter extends LlmAdapter {
818
569
  export function apply(ctx, live) {
819
570
  const config = () => readConfig(live);
820
571
  const get = () => ctx.get('aiWorkbenchConnect');
821
- let language = 'en';
572
+ const language = 'zh';
822
573
  const readImage = async (ref, signal) => {
823
574
  const attachments = ctx.get('attachments');
824
575
  if (typeof attachments?.readImage !== 'function')
@@ -826,22 +577,19 @@ export function apply(ctx, live) {
826
577
  return attachments.readImage(ref, signal);
827
578
  };
828
579
  const openByAgent = new Map(AGENTS.map(agent => [agent.id, brokerFactory(get, undefined, config)]));
829
- const localTurns = createLocalUserTurnObserver(ctx);
830
580
  const detailManagers = new Map();
831
581
  const managerForProfile = () => {
832
582
  const profile = ctx.get('profileContext');
833
- if (typeof profile?.dir !== 'string')
834
- throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用:缺少官方profile目录。', '官方profileContext.dir');
835
- let manager = detailManagers.get(profile.dir);
583
+ const profileDir = typeof profile?.dir === 'string' ? profile.dir : undefined;
584
+ let manager = detailManagers.get(profileDir ?? '');
836
585
  if (!manager) {
837
- manager = createRemoteDetails({ profileDir: profile.dir, getApi: get });
838
- detailManagers.set(profile.dir, manager);
586
+ manager = createRemoteDetails({ profileDir, warn: line => ctx.logger('remote-agent').warn(line) });
587
+ detailManagers.set(profileDir ?? '', manager);
839
588
  }
840
589
  return manager;
841
590
  };
591
+ ctx.provide('aiWorkbenchRemoteDiagnostics', { read: (query) => managerForProfile().read(query) });
842
592
  const preflightDiagnostics = createPreflightDiagnostics({ ctx, getApi: get, manager: managerForProfile });
843
- let changed = () => { };
844
- let observeCatalogRevocation = () => { };
845
593
  const diagnostics = new Map();
846
594
  const diagnostic = (scope, provider, message) => {
847
595
  let entry = diagnostics.get(provider);
@@ -856,25 +604,14 @@ export function apply(ctx, live) {
856
604
  else
857
605
  entry.set(scope, message);
858
606
  };
859
- let defaults;
860
- const catalogDiagnostics = createCatalogDiagnostics(() => { const profile = ctx.get('profileContext'); return profile?.dir; });
861
607
  // Public Loader Entry.options.id; optional when mounted without Loader.
862
608
  const actualSettingsNamespace = ctx.fiber.entry?.options.id;
863
- new CatalogDiagnosticsRemote(ctx, async () => ({ ...await catalogDiagnostics.snapshot(), settingsNamespace: actualSettingsNamespace ?? null, providers: AGENTS.map(agent => ({ provider: agent.id, name: agent.name[language], messages: [...(diagnostics.get(agent.id)?.values() ?? [])] })) }));
864
- const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, (...args) => { const state = get()?.getConnectionState(); const connection = state ? { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase } : null; return runCatalog(...args, event => catalogDiagnostics.record({ ...event, connection })); }, () => changed(), (brand, message) => {
865
- diagnostic('catalog', AGENTS.find(agent => agent.loginProvider === brand).id, message);
866
- }, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent), (fields, changes) => observeCatalogRevocation(fields, changes));
609
+ const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes);
867
610
  const adapter = new RemoteAgentsAdapter(config, agent => {
868
611
  const details = async (request, api, connection, target) => {
869
- return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection, localAcceptanceId: request.localAcceptanceId ?? request.localAcceptance?.acceptanceId });
612
+ return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection });
870
613
  };
871
614
  const open = async (...args) => {
872
- const profile = ctx.get('profileContext');
873
- if (!profile?.dir)
874
- throw new NeedError('RECOVERY_STORAGE_REQUIRED', '会话恢复需要官方profile目录', '官方profileContext.dir');
875
- args[2].recoveryJournal = recoveryJournal(profile.dir, runtimeRecoveryKey(`${args[0].username}@${args[0].host}:${args[0].port}`, agent.id, args[2].sessionId, args[2].employeeScope));
876
- args[2].verifyRecovery = () => verifyRecoveryCheckpoint(ctx, args[2], agent.id);
877
- args[2].currentInputIds = await currentInputIds(ctx, args[2]);
878
615
  const runtime = await openByAgent.get(agent.id)(...args);
879
616
  runtime.detailContext = () => {
880
617
  const events = args[2].sessionId ? ctx.get('agents')?.get(SessionId(args[2].sessionId))?.session.snapshotEvents() : undefined;
@@ -904,14 +641,11 @@ export function apply(ctx, live) {
904
641
  const sessions = services.get('sessions');
905
642
  return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
906
643
  };
907
- return connectProvider(get, { agent, workspaceSource: {
908
- workspace, currentUserTurn: request => localTurns.current(request), confirm: nativeWorkspaceSourceConfirmation(ctx),
909
- record: (request, observation) => preflightDiagnostics.record(request, agent.loginProvider, observation),
910
- }, ...(agent.loginProvider === 'codex' ? { employee: {
911
- currentUserTurn: request => localTurns.current(request),
644
+ const provider = connectProvider(get, { agent, workspace, ...(agent.loginProvider === 'codex' ? { employee: {
912
645
  getNetwork: () => ctx.get('aiWorkbenchNetwork'),
913
646
  workspace, native: () => resolveEmployeeNative(),
914
- } } : {}), notice: createWorkspaceNoticeConsumer(ctx, agent), details, open, local });
647
+ } } : {}), details, open, local });
648
+ return provider;
915
649
  }, dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
916
650
  const events = sessionId ? ctx.get('agents')?.get(SessionId(sessionId))?.session.snapshotEvents() : undefined;
917
651
  let turn = null, step = null;
@@ -930,50 +664,24 @@ export function apply(ctx, live) {
930
664
  step = null;
931
665
  }
932
666
  return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
933
- }, modelDirectory, preflightDiagnostics);
667
+ }, modelDirectory, preflightDiagnostics, dshPermissionSource(ctx));
668
+ for (const agent of AGENTS)
669
+ void adapter.listModels(agent.id).catch(() => ctx.logger('remote-agent').warn('模型列表暂不可用,打开菜单时可重新读取'));
934
670
  const ids = AGENTS.map(agent => agent.id);
935
671
  const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: actualSettingsNamespace ?? settingsNamespace, settingsPath: [] }));
936
672
  const directory = ctx.llm.registerConfigurableProviders(entries());
937
673
  const registration = ctx.llm.registerAdapter(ids, adapter);
938
674
  installModelInputRetention(ctx, { isOurProvider: provider => ids.includes(provider), diagnosticForAttempt: position => preflightDiagnostics.forPreparation(position.sessionId, position), preflightDiagnostic: (provider, message) => diagnostic('preflight', provider, message), diagnostic: (provider, message) => diagnostic('input', provider, message) });
939
- const serviceValue = (name) => ctx.get(name);
940
- const refreshDefault = () => {
941
- const service = serviceValue('agentDefaultModel');
942
- const selected = service?.currentSelection();
943
- const agent = AGENTS.find(agent => agent.id === selected?.provider);
944
- if (!defaults || !agent)
945
- return;
946
- // Read through the same trusted, fresh catalog. Failures are already recorded there.
947
- void modelDirectory.read(agent).then(lease => lease.release(), () => { });
948
- };
949
- ctx.inject(['agentDefaultModel'], child => {
950
- const service = child.get('agentDefaultModel');
951
- const current = createModelDefaults(service, (provider, message) => diagnostic('default', provider, message), createDefaultSelectionWriter(service, () => serviceValue('configEditor')));
952
- defaults = current;
953
- refreshDefault();
954
- child.effect(() => () => { current.dispose(); if (defaults === current)
955
- defaults = undefined; });
956
- });
957
- observeCatalogRevocation = (fields, changes) => adapter.observeModelInvalidation(fields, changes);
958
- changed = () => { registration.replace(ids); refreshDefault(); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
959
- let employeeConnect = serviceValue('aiWorkbenchConnect'), employeeNetwork = serviceValue('aiWorkbenchNetwork');
960
- ctx.on('internal/service', service => {
961
- if (service === 'aiWorkbenchConnect')
962
- modelDirectory.refresh();
963
- if (service !== 'aiWorkbenchConnect' && service !== 'aiWorkbenchNetwork')
964
- return;
965
- const nextConnect = serviceValue('aiWorkbenchConnect'), nextNetwork = serviceValue('aiWorkbenchNetwork');
966
- if (nextConnect === employeeConnect && nextNetwork === employeeNetwork)
967
- return;
968
- employeeConnect = nextConnect;
969
- employeeNetwork = nextNetwork;
970
- void adapter.invalidateEmployeeState().catch(() => { ctx.logger('remote-agent').warn('本机执行连接的清理结果未确认,已阻断继续发送;请联系管理员核对。'); });
971
- });
972
- new ProviderLocaleRemote(ctx, next => { language = next; directory.replace(entries()); registration.replace(ids); });
973
- const cleanup = (id) => adapter.detachSession(id);
675
+ const cleanup = async (id) => { try {
676
+ await adapter.detachSession(id);
677
+ }
678
+ finally {
679
+ for (const manager of detailManagers.values())
680
+ manager.releaseSession(id);
681
+ } };
974
682
  ctx.on('agent/disposed', ({ agent }) => cleanup(agent.session.id));
975
683
  ctx.on('session/disposed', session => cleanup(session.id));
976
- ctx.effect(() => () => { modelDirectory.dispose(); return adapter.dispose(); });
684
+ ctx.effect(() => async () => { modelDirectory.dispose(); await adapter.dispose(); await Promise.all([...detailManagers.values()].map(manager => manager.dispose())); });
977
685
  }
978
686
  export function dshApproval(ctx) {
979
687
  return async (request, signal, options, context, hostWorkspace) => {