@ai-workbench/remote-agent 0.8.13 → 0.8.15

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 (40) hide show
  1. package/README.md +10 -8
  2. package/dist/acp.d.ts +12 -7
  3. package/dist/acp.js +65 -41
  4. package/dist/broker-runtime.js +46 -20
  5. package/dist/broker.d.ts +4 -2
  6. package/dist/broker.js +18 -9
  7. package/dist/claude-local.d.ts +2 -1
  8. package/dist/claude-local.js +62 -35
  9. package/dist/close-diagnostics.d.ts +56 -0
  10. package/dist/close-diagnostics.js +104 -0
  11. package/dist/conversation-details.js +67 -9
  12. package/dist/employee-endpoint.d.ts +3 -1
  13. package/dist/employee-endpoint.js +8 -6
  14. package/dist/employee-exec.d.ts +7 -4
  15. package/dist/employee-exec.js +39 -29
  16. package/dist/employee-mcp.d.ts +6 -3
  17. package/dist/employee-mcp.js +75 -39
  18. package/dist/employee-presentation.d.ts +6 -0
  19. package/dist/employee-presentation.js +16 -0
  20. package/dist/employee-provider.js +59 -36
  21. package/dist/employee-shell.d.ts +2 -0
  22. package/dist/employee-shell.js +17 -5
  23. package/dist/employee-ssh.d.ts +2 -0
  24. package/dist/employee-ssh.js +6 -4
  25. package/dist/employee-tools.d.ts +3 -1
  26. package/dist/employee-tools.js +6 -5
  27. package/dist/index.d.ts +18 -4
  28. package/dist/index.js +183 -55
  29. package/dist/model-catalog.d.ts +9 -1
  30. package/dist/model-catalog.js +63 -5
  31. package/dist/model-configuration.d.ts +14 -1
  32. package/dist/model-configuration.js +122 -89
  33. package/dist/model-presentation.d.ts +13 -0
  34. package/dist/model-presentation.js +18 -0
  35. package/dist/presentation.js +2 -1
  36. package/dist/provider.d.ts +8 -2
  37. package/dist/recovery.d.ts +4 -0
  38. package/dist/rpc-namespace.d.ts +2 -2
  39. package/dist/rpc-namespace.js +2 -1
  40. package/package.json +1 -1
@@ -94,13 +94,14 @@ export function createEmployeeTools(options) {
94
94
  throw failure('EMPLOYEE_OUTPUT_TOO_LARGE'); };
95
95
  async function run(command, args, signal, detail, timeout) {
96
96
  signal.throwIfAborted();
97
+ const diagnostics = options.closeDiagnostics?.child(detail);
97
98
  const operation = new Promise((yes, no) => {
98
99
  const child = spawn(command, args, { cwd: options.cwd, env: process.env, stdio: ['pipe', 'pipe', 'pipe'] });
99
100
  child.stdin.end();
100
101
  let bytes = 0, stdout = [], stderr = [], failed, killTimer, timeoutTimer;
101
- const kill = (which) => { if (child.exitCode === null && child.signalCode === null)
102
+ const kill = (which) => { diagnostics?.record({ layer: 'executor', initiator: 'tools.process-kill', reason: failed?.code ?? 'PROCESS_CLEANUP', action: 'terminate', signal: which }); if (child.exitCode === null && child.signalCode === null)
102
103
  child.kill(which); };
103
- const stop = (code) => { if (failed)
104
+ const stop = (code) => { diagnostics?.record({ layer: 'executor', initiator: 'tools.process-stop', reason: code, action: 'terminate', changes: [{ field: 'tools.stop-code', before: failed?.code ?? null, after: failed?.code ?? code }] }); if (failed)
104
105
  return; failed = failure(code); kill('SIGTERM'); killTimer = setTimeout(() => kill('SIGKILL'), options.killGraceMs); killTimer.unref(); };
105
106
  const aborted = () => stop('EMPLOYEE_CANCELLED_UNKNOWN');
106
107
  const collect = (dest, chunk) => { bytes += chunk.length; if (bytes > options.maxBytes) {
@@ -116,8 +117,8 @@ export function createEmployeeTools(options) {
116
117
  stop('CONVERSATION_DETAILS_FAILED');
117
118
  } if (signal.aborted)
118
119
  aborted(); });
119
- child.on('error', e => { failed ??= failure(e.code === 'ENOENT' ? 'EMPLOYEE_COMMAND_UNAVAILABLE' : 'EMPLOYEE_PROCESS_FAILED'); });
120
- child.on('close', (exitCode, exitSignal) => { signal.removeEventListener('abort', aborted); if (killTimer)
120
+ child.on('error', e => { diagnostics?.record({ layer: 'executor', initiator: 'tools.process-error', reason: e.code === 'ENOENT' ? 'EMPLOYEE_COMMAND_UNAVAILABLE' : 'EMPLOYEE_PROCESS_FAILED', action: 'failure' }); failed ??= failure(e.code === 'ENOENT' ? 'EMPLOYEE_COMMAND_UNAVAILABLE' : 'EMPLOYEE_PROCESS_FAILED'); });
121
+ child.on('close', (exitCode, exitSignal) => { diagnostics?.record({ layer: 'executor', initiator: 'tools.process-close', reason: failed?.code ?? 'PROCESS_EXIT', action: 'exit', exitCode, signal: exitSignal }); signal.removeEventListener('abort', aborted); if (killTimer)
121
122
  clearTimeout(killTimer); if (timeoutTimer)
122
123
  clearTimeout(timeoutTimer); failed ? no(failed) : yes({ stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'), exitCode, signal: exitSignal }); });
123
124
  signal.addEventListener('abort', aborted, { once: true });
@@ -287,6 +288,6 @@ export function createEmployeeTools(options) {
287
288
  }
288
289
  return {
289
290
  call(name, input, requestSignal, observer) { const operation = call(name, input, requestSignal, observer); calls.add(operation); void operation.then(() => calls.delete(operation), () => calls.delete(operation)); return operation; },
290
- async close() { lifetime.abort(); await Promise.allSettled([...calls, ...children]); },
291
+ async close(cause = { layer: 'executor', initiator: 'tools.close', reason: 'EMPLOYEE_TOOLS_CLOSED', action: 'close' }) { options.closeDiagnostics?.record({ ...cause, changes: cause.changes ?? [{ field: 'tools.aborted', before: lifetime.signal.aborted, after: true }] }); lifetime.abort(); await Promise.allSettled([...calls, ...children]); },
291
292
  };
292
293
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type CloseChange } from './close-diagnostics.ts';
1
2
  import type { Context } from '@deepseek-ai/cordis';
2
3
  import Schema from '@deepseek-ai/schemastery';
3
4
  import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm';
@@ -74,6 +75,9 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
74
75
  private shuttingDown;
75
76
  private detached;
76
77
  private cleanups;
78
+ private preparationFailures;
79
+ private modelPresentation;
80
+ private employeeNoticeSessions;
77
81
  constructor(config: PluginConfig | (() => PluginConfig), resolveRuntime: RuntimeProvider, approve?: InvocationApproval | undefined, providerName?: () => string, agent?: AgentDefinition, readImage?: ImageReader | undefined, reportLoginFailure?: LoginFailureReporter | undefined, directory?: ModelDirectory | undefined, preflightDiagnostics?: PreflightDiagnostics | undefined);
78
82
  providerInfo(provider: string): {
79
83
  id: string;
@@ -83,9 +87,14 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
83
87
  listModels(provider: string): Promise<(import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
84
88
  catalog: HostModel;
85
89
  })[]>;
86
- resolveModel(provider: string, id: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
87
- catalog: HostModel;
90
+ resolveModel(provider: string, id: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo>;
91
+ prepareCall(provider: string, id: string, signal?: AbortSignal): Promise<{
92
+ model: import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
93
+ catalog: HostModel;
94
+ };
95
+ stream: (options: GenerateOptions) => AsyncGenerator<StreamChunk, any, any>;
88
96
  }>;
97
+ observeModelInvalidation(fields: readonly string[], changes?: readonly CloseChange[]): void;
89
98
  invalidateModelState(): Promise<void>;
90
99
  invalidateEmployeeState(): Promise<void>;
91
100
  providerRetryPolicy(): {
@@ -115,9 +124,14 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
115
124
  listModels(provider: string): Promise<(import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
116
125
  catalog: HostModel;
117
126
  })[]>;
118
- resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
119
- catalog: HostModel;
127
+ resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo>;
128
+ prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<{
129
+ model: import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
130
+ catalog: HostModel;
131
+ };
132
+ stream: (options: GenerateOptions) => AsyncGenerator<StreamChunk, any, any>;
120
133
  }>;
134
+ observeModelInvalidation(fields: readonly string[], changes?: readonly CloseChange[]): void;
121
135
  invalidateModelState(): Promise<void>;
122
136
  invalidateEmployeeState(): Promise<void>;
123
137
  providerRetryPolicy(): {
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { CloseDiagnostics, closeCode, closeErrorCodes, closeFailure } from "./close-diagnostics.js";
1
2
  import { runCatalog } from "./catalog-ssh.js";
2
3
  import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
3
4
  import { SessionId } from '@deepseek-ai/dsh-session';
@@ -24,6 +25,8 @@ import { approvalLabel } from "./approval-label.js";
24
25
  import { StageTiming } from "./stage-timing.js";
25
26
  import { verifyRecoveryCheckpoint, currentInputIds } from "./recovery-checkpoint.js";
26
27
  import { ModelCatalog, modelInfo } from "./model-catalog.js";
28
+ import { ModelPresentation, unavailableModelInfo } from "./model-presentation.js";
29
+ import { EMPLOYEE_EXECUTION_NOTICE, employeeNoticePreviouslyShown } from "./employee-presentation.js";
27
30
  import { ModelConfigurationError } from "./model-configuration.js";
28
31
  import { officialEmployeeWorkspace } from "./employee-workspace.js";
29
32
  import { nativeWorkspaceSourceConfirmation } from "./workspace-source-confirmation.js";
@@ -54,13 +57,20 @@ export const Config = Schema.object({
54
57
  export function readConfig(config) {
55
58
  return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, typeof value === 'object' && value !== null && 'get' in value ? value.get() : value]));
56
59
  }
60
+ function modelResolutionLabel(configuration) {
61
+ return configuration.resolvedModelSource === 'host-user-catalog'
62
+ ? `型号:${configuration.resolvedModel}(来源:主机目录映射 ${configuration.selected.value};会话未单独回报具体型号)`
63
+ : `具体型号:${configuration.resolvedModel}`;
64
+ }
57
65
  function validContextDigest(value) { return typeof value === 'string' && /^[a-fA-F0-9]{64}$/u.test(value); }
58
66
  function readPointer(options, agent) {
59
67
  const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
60
68
  if (!last || last.role !== 'assistant')
61
69
  return;
62
- if (last.source.provider !== options.provider || last.source.replayState === undefined)
63
- throw new NeedError('SESSION_STATE_MISSING', '已有对话缺少当前模型的远端恢复标识,请新建会话', '当前模型的远端恢复记录');
70
+ if (last.source.provider !== options.provider)
71
+ throw new NeedError('SESSION_STATE_MISSING', '已有对话属于其他模型通道,不能沿用其远端进程', '当前模型的远端恢复记录');
72
+ if (last.source.replayState === undefined)
73
+ return;
64
74
  const envelope = last.source.replayState;
65
75
  const pointer = envelope?.response;
66
76
  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))
@@ -104,6 +114,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
104
114
  shuttingDown = false;
105
115
  detached = new Set();
106
116
  cleanups = new WeakMap();
117
+ preparationFailures = new Map();
118
+ modelPresentation = new ModelPresentation();
119
+ employeeNoticeSessions = new Set();
107
120
  constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics) {
108
121
  super();
109
122
  this.resolveRuntime = resolveRuntime;
@@ -120,18 +133,21 @@ export class CodexRemoteAdapter extends LlmAdapter {
120
133
  catalog(signal) { if (!this.directory)
121
134
  throw new LlmError('无法获取模型列表:缺少可信主机目录能力', 'MODEL_CATALOG_UNAVAILABLE'); return this.directory.read(this.agent, signal); }
122
135
  async listModels(provider) { const catalog = await this.catalog(); try {
136
+ this.modelPresentation.remember(catalog);
123
137
  return catalog.models.map(model => modelInfo(provider, model));
124
138
  }
125
139
  finally {
126
140
  catalog.release();
127
141
  } }
128
142
  async resolveModel(provider, id, signal) { try {
143
+ signal?.throwIfAborted();
144
+ const displayed = this.modelPresentation.resolve(provider, id);
145
+ if (displayed)
146
+ return displayed;
129
147
  const catalog = await this.catalog(signal);
130
148
  try {
131
149
  const model = catalog.models.find(model => model.id === id);
132
- if (!model)
133
- throw new LlmError(id === 'remote' ? '无法获取模型列表:这个会话保存的是旧版模型选择,请打开本会话的模型菜单,重新选择模型后再发送。' : '所选模型已不在当前主机目录中,请打开本会话的模型菜单重新选择模型。', 'MODEL_CATALOG_UNAVAILABLE');
134
- return modelInfo(provider, model);
150
+ return model ? modelInfo(provider, model) : unavailableModelInfo(provider, id);
135
151
  }
136
152
  finally {
137
153
  catalog.release();
@@ -141,16 +157,43 @@ export class CodexRemoteAdapter extends LlmAdapter {
141
157
  markModelPreparationFailure(provider, error, signal);
142
158
  throw error;
143
159
  } }
144
- async invalidateModelState() { for (const turn of this.turns.values())
145
- turn.abort(); await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
146
- async invalidateEmployeeState() { await Promise.all([...this.sessions].filter(([, live]) => live.runtime.employeeExecutionMeta || live.runtime.execution).map(([id]) => this.disposeSession(id))); }
160
+ async prepareCall(provider, id, signal) {
161
+ try {
162
+ const catalog = await this.catalog(signal);
163
+ try {
164
+ catalog.assertCurrent();
165
+ const selected = catalog.models.find(model => model.id === id);
166
+ if (!selected)
167
+ throw new LlmError('所选模型已不在当前主机目录中,请在模型菜单重新选择后再发送;本次输入尚未发送。', 'MODEL_CATALOG_UNAVAILABLE');
168
+ return { model: modelInfo(provider, selected), stream: (options) => this.stream(options) };
169
+ }
170
+ finally {
171
+ catalog.release();
172
+ }
173
+ }
174
+ catch (error) {
175
+ markModelPreparationFailure(provider, error, signal);
176
+ throw error;
177
+ }
178
+ }
179
+ observeModelInvalidation(fields, changes = []) { for (const live of this.sessions.values())
180
+ live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'catalog.signature-change', reason: 'MODEL_CATALOG_SIGNATURE_CHANGED', action: 'invalidate', changedFields: fields, changes }); }
181
+ async invalidateModelState() { for (const [id, turn] of this.turns) {
182
+ 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 }] });
183
+ turn.abort();
184
+ } await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
185
+ async invalidateEmployeeState() { for (const live of this.sessions.values())
186
+ if (live.runtime.employeeExecutionMeta || live.runtime.execution)
187
+ 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))); }
147
188
  providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
148
- cleanupRuntime(runtime) {
189
+ cleanupRuntime(runtime, cause = { layer: 'adapter', initiator: 'adapter.cleanup-runtime', reason: 'RUNTIME_CLEANUP', action: 'dispose' }) {
190
+ runtime.closeDiagnostics?.record(cause);
149
191
  const existing = this.cleanups.get(runtime);
150
192
  if (existing)
151
193
  return existing;
152
- const cleanup = Promise.resolve().then(() => runtime.dispose?.()).catch(() => {
153
- const failure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话的清理结果未确认,不能继续或重开此会话', '管理员核对并结束原主机会话');
194
+ const cleanup = Promise.resolve().then(() => runtime.dispose?.(cause)).catch(error => {
195
+ const codes = closeErrorCodes(error, 'REMOTE_CLEANUP_UNCONFIRMED');
196
+ const failure = closeFailure('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话的清理结果未确认,不能继续或重开此会话', '管理员核对原主机会话;收尾代码 ' + codes.join(','), codes);
154
197
  for (const live of this.sessions.values())
155
198
  if (live.runtime === runtime) {
156
199
  live.cleanupFailure = failure;
@@ -162,13 +205,14 @@ export class CodexRemoteAdapter extends LlmAdapter {
162
205
  return cleanup;
163
206
  }
164
207
  async disposeSession(sessionId) {
208
+ this.sessions.get(sessionId)?.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'abort' });
165
209
  this.turns.get(sessionId)?.abort();
166
210
  const current = this.sessions.get(sessionId);
167
211
  if (!current)
168
212
  return;
169
213
  if (current.disposal)
170
214
  return current.disposal;
171
- current.session.close();
215
+ current.session.close({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'close' });
172
216
  current.disposal = this.cleanupRuntime(current.runtime).then(() => { if (this.sessions.get(sessionId) === current)
173
217
  this.sessions.delete(sessionId); });
174
218
  return current.disposal;
@@ -177,13 +221,14 @@ export class CodexRemoteAdapter extends LlmAdapter {
177
221
  const current = this.sessions.get(sessionId), turn = this.turns.get(sessionId);
178
222
  if (!current && !turn)
179
223
  return;
224
+ current?.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.detach-session', reason: 'SESSION_DETACHED', action: 'detach', changes: [{ field: 'adapter.detached', before: this.detached.has(sessionId), after: true }] });
180
225
  this.detached.add(sessionId);
181
226
  if (current) {
182
227
  try {
183
- await current.runtime.detach?.();
228
+ await current.runtime.detach?.({ layer: 'adapter', initiator: 'adapter.detach-session', reason: 'SESSION_DETACHED', action: 'detach' });
184
229
  }
185
230
  finally {
186
- current.session.close();
231
+ current.session.close({ layer: 'adapter', initiator: 'adapter.detach-finally', reason: 'SESSION_DETACHED', action: 'close' });
187
232
  this.sessions.delete(sessionId);
188
233
  turn?.abort();
189
234
  }
@@ -192,7 +237,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
192
237
  turn?.abort();
193
238
  }
194
239
  async dispose() {
240
+ for (const live of this.sessions.values())
241
+ live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.dispose', reason: 'PLUGIN_DISPOSED', action: 'dispose', changes: [{ field: 'adapter.shutting-down', before: this.shuttingDown, after: true }] });
195
242
  this.shuttingDown = true;
243
+ this.modelPresentation.clear();
244
+ this.employeeNoticeSessions.clear();
196
245
  const results = await Promise.allSettled([...new Set([...this.sessions.keys(), ...this.turns.keys()])].map(id => this.detachSession(id)));
197
246
  this.lifetime.abort();
198
247
  const failure = results.find(result => result.status === 'rejected');
@@ -239,6 +288,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
239
288
  const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
240
289
  const options = { ...original, signal };
241
290
  const timing = new StageTiming();
291
+ const abortOffs = [];
242
292
  let request;
243
293
  let earlyStage = 'input-prepare';
244
294
  let runtime;
@@ -246,14 +296,26 @@ export class CodexRemoteAdapter extends LlmAdapter {
246
296
  let live;
247
297
  let succeeded = false;
248
298
  let remoteAttempted = false;
299
+ let promptBaseline = 0;
300
+ let failureCode;
249
301
  let preserveRecoveredRuntime = false;
250
302
  let activeMapper;
251
303
  let catalog;
252
304
  let selected;
253
305
  let modelConfiguration;
306
+ let employeeNoticeShown = false;
307
+ const information = [];
308
+ const recordInformation = () => { if (runtime?.details)
309
+ for (const entry of information.splice(0))
310
+ runtime.details.record(entry); };
254
311
  const config = this.config();
255
312
  const mode = this.agent.oneShot ? config.mode : 'session';
256
313
  request = { sessionId: options.sessionId, mode: mode ?? 'session', timing, signal };
314
+ const observeAbort = (source, initiator, reason) => { if (!source)
315
+ 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)); };
316
+ observeAbort(original.signal, 'signal.caller', 'CALLER_ABORTED');
317
+ observeAbort(this.lifetime.signal, 'signal.plugin-lifetime', 'PLUGIN_DISPOSED');
318
+ observeAbort(turn.signal, 'signal.adapter-turn', 'TURN_ABORTED');
257
319
  this.preflightDiagnostics?.begin(request, this.agent.loginProvider);
258
320
  if (this.preflightDiagnostics)
259
321
  request.recordEarlyFailure = (error, stage) => this.preflightDiagnostics.failure(request, this.agent.loginProvider, error, stage);
@@ -261,21 +323,42 @@ export class CodexRemoteAdapter extends LlmAdapter {
261
323
  if (!mode || (mode === 'session' && (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)))
262
324
  throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的mode/acpMaxMessageBytes');
263
325
  const previous = readPointer(options, this.agent);
326
+ employeeNoticeShown = !!key && this.employeeNoticeSessions.has(key) || employeeNoticePreviouslyShown(options);
327
+ if (employeeNoticeShown && key)
328
+ this.employeeNoticeSessions.add(key);
329
+ const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
330
+ const missingPointer = !!last && !previous;
331
+ let requireFreshSession = missingPointer || !!key && this.preparationFailures.has(key);
264
332
  if (previous && previous.mode !== mode)
265
333
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '运行模式与原会话不一致,请新建会话', '原会话模式');
266
334
  if (previous && !previous.modelConfiguration)
267
- throw new NeedError('MODEL_SELECTION_REQUIRED', '旧远端恢复记录没有明确模型选择,请新建会话并选择模型', '本轮明确选择的模型');
335
+ information.push({ kind: 'session', severity: 'info', code: 'TURN_GATE_INFORMATION', summary: '历史回复没有模型配置资料;本轮会重新确认你当前选择的模型。', payload: { reason: 'HISTORICAL_MODEL_CONFIGURATION_ABSENT' } });
268
336
  live = key ? this.sessions.get(key) : undefined;
269
337
  if (live?.disposal || live?.cleanupFailure)
270
338
  throw live.cleanupFailure ?? new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话正在结束,清理结果尚未确认,不能继续或重开此会话', '管理员核对原主机会话清理结果');
271
339
  if (live?.failure)
272
- throw new NeedError('SESSION_STATE_UNCONFIRMED', '上轮远端状态未确认,不能自动重发或重开,请新建会话并核对项目', '管理员核对原会话状态');
273
- if (live && !previous)
274
- throw new NeedError('SESSION_STATE_MISSING', '上一轮回复尚无持久恢复记录,不能继续当前远端会话', '原会话的持久回复记录');
340
+ throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '上一轮输入已经交给远端,但结果未确认,不能自动重发', '核对已发送输入及原主机会话状态');
341
+ if (live && !previous) {
342
+ if (typeof live.runtime.dispose !== 'function')
343
+ throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原会话不能确认已安全结束,本轮尚未发送', '可确认结束原实例的通道');
344
+ live.session.close({ layer: 'adapter', initiator: 'adapter.missing-pointer', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'close' });
345
+ await this.cleanupRuntime(live.runtime, { layer: 'adapter', initiator: 'adapter.missing-pointer', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'dispose' });
346
+ if (key && this.sessions.get(key) === live)
347
+ this.sessions.delete(key);
348
+ live = undefined;
349
+ requireFreshSession = true;
350
+ }
275
351
  const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
276
352
  if (live && live.configuration !== configuration)
277
353
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
278
354
  Object.assign(request, { timing, 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 }) });
355
+ if (requireFreshSession) {
356
+ request.requireFreshSession = true;
357
+ request.resumeSessionId = undefined;
358
+ request.recoveryCheckpointId = undefined;
359
+ information.push({ kind: 'session', severity: 'warning', code: 'TURN_GATE_INFORMATION', summary: '缺少可用的旧会话恢复资料,或上次在发送前已失败;将核对旧实例安全结束后,只处理本次接纳的输入。', payload: { reason: missingPointer ? 'PRIVATE_RECOVERY_POINTER_ABSENT' : 'PRIOR_PREPARATION_FAILED' } });
360
+ }
361
+ request.previousContextSha256 = previous?.contextSha256;
279
362
  const after = options.messages.findLastIndex(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
280
363
  request.userInputIds = Object.freeze(options.messages.slice(after + 1).flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []));
281
364
  request.userMessageIds = request.userInputIds;
@@ -293,8 +376,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
293
376
  // New Codex transport scopes are independent of the native context digest.
294
377
  if (contextChanged)
295
378
  request.contextSha256 = undefined;
296
- if (previous && this.agent.loginProvider === 'claude' && previous.contextSha256 !== contextSha256)
297
- throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
379
+ const claudeContextChanged = !!previous && !codex && previous.contextSha256 !== contextSha256;
298
380
  if (contextChanged)
299
381
  input = await prepareContinuation(options, input, this.readImage);
300
382
  if (live) {
@@ -314,16 +396,25 @@ export class CodexRemoteAdapter extends LlmAdapter {
314
396
  earlyStage = 'model-catalog';
315
397
  catalog = await this.catalog(signal);
316
398
  selected = catalog.models.find(model => model.id === options.model);
399
+ catalog.onInformation?.(entry => information.push(entry));
317
400
  if (!selected)
318
401
  throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
319
402
  if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
320
403
  throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
404
+ const observeSignal = (source, initiator, reason) => { const observed = () => { const diagnostic = request?.closeDiagnostics ?? runtime?.closeDiagnostics; if (diagnostic)
405
+ diagnostic.record({ layer: 'adapter', initiator, reason, action: 'abort', changes: [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] }); }; source.addEventListener('abort', observed, { once: true }); abortOffs.push(() => source.removeEventListener('abort', observed)); };
406
+ observeSignal(catalog.signal, 'signal.model-catalog', 'MODEL_CATALOG_REVOKED');
407
+ observeSignal(signal, 'signal.turn-composite', 'ABORTED');
321
408
  let modelSignal = AbortSignal.any([signal, catalog.signal]);
322
409
  options.signal = modelSignal;
323
410
  request.signal = modelSignal;
324
411
  earlyStage = 'runtime-resolve';
325
412
  runtime = await this.resolveRuntime(request);
413
+ const closeSink = runtime.details;
414
+ runtime.closeDiagnostics ??= new CloseDiagnostics(event => closeSink?.record(event));
326
415
  signal.throwIfAborted();
416
+ if (requireFreshSession && (!runtime.freshSessionConfirmed || !runtime.restartNotice))
417
+ throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '未确认原实例安全结束或原位置为空,本轮没有发送', '同UUID条件结束与list消失证明,或原slot不存在证明');
327
418
  if (runtime.execution?.signal) {
328
419
  modelSignal = AbortSignal.any([modelSignal, runtime.execution.signal]);
329
420
  options.signal = modelSignal;
@@ -357,14 +448,26 @@ export class CodexRemoteAdapter extends LlmAdapter {
357
448
  const details = runtime.details;
358
449
  if (!details)
359
450
  throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
451
+ recordInformation();
360
452
  timing.bind(detail => details.record(detail));
361
453
  request.recordEarlyFailure = undefined;
362
454
  const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
363
455
  activeMapper = mapper;
364
456
  let id;
365
457
  let stop;
366
- if (runtime.employeePresentation) {
367
- yield* mapper.text(runtime.employeePresentation + '\n\n');
458
+ if (runtime.employeePresentation)
459
+ details.record({ kind: 'session', severity: 'warning', code: 'EMPLOYEE_EXECUTION_SCOPE', summary: runtime.employeePresentation, payload: { source: 'local' } });
460
+ if (runtime.execution)
461
+ details.record({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_SCOPE', summary: runtime.execution.notice, payload: runtime.execution.scope });
462
+ if ((runtime.employeePresentation || runtime.execution) && !employeeNoticeShown) {
463
+ for (const chunk of mapper.text(EMPLOYEE_EXECUTION_NOTICE + '\n\n')) {
464
+ if (chunk.type === 'text-delta') {
465
+ employeeNoticeShown = true;
466
+ if (key)
467
+ this.employeeNoticeSessions.add(key);
468
+ }
469
+ yield chunk;
470
+ }
368
471
  yield* mapper.flush();
369
472
  }
370
473
  if (runtime.workspaceNotice) {
@@ -375,11 +478,6 @@ export class CodexRemoteAdapter extends LlmAdapter {
375
478
  yield* mapper.text(runtime.restartNotice + '\n\n');
376
479
  yield* mapper.flush();
377
480
  }
378
- if (runtime.execution) {
379
- details.record({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_SCOPE', summary: '本机工具路由及未覆盖操作范围', payload: runtime.execution.scope });
380
- yield* mapper.text(runtime.execution.notice + '\n\n');
381
- yield* mapper.flush();
382
- }
383
481
  if (mode === 'session') {
384
482
  if (live && live.runtime !== runtime)
385
483
  throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
@@ -390,7 +488,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
390
488
  let done = false;
391
489
  let failure;
392
490
  const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
393
- const hooks = { broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
491
+ const hooks = { closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
394
492
  const execution = runtime.execution;
395
493
  const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
396
494
  if (execution) {
@@ -410,7 +508,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
410
508
  }
411
509
  catch (error) {
412
510
  failure = error;
413
- session?.close();
511
+ session?.close({ layer: 'adapter', initiator: 'adapter.mapper-failure', reason: closeCode(error, 'MAPPING_FAILED'), action: 'close' });
414
512
  } }, onResponse: response => push(mapper.acpResponse(response)), onDetail: detail => details.record(detail) };
415
513
  const fresh = !live;
416
514
  if (fresh) {
@@ -426,6 +524,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
426
524
  if (previous?.sessionId !== session.id)
427
525
  throw new NeedError('SESSION_STATE_INVALID', '持久会话标识与当前进程不一致', '原会话的恢复标识');
428
526
  }
527
+ promptBaseline = session.promptDispatchCount;
429
528
  const task = (async () => {
430
529
  remoteAttempted = true;
431
530
  try {
@@ -436,16 +535,20 @@ export class CodexRemoteAdapter extends LlmAdapter {
436
535
  session.validatePrompt(input.prompt);
437
536
  if (contextChanged)
438
537
  await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
538
+ else if (claudeContextChanged)
539
+ await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal, true);
439
540
  }
440
541
  else if (fresh)
441
542
  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);
442
543
  else if (contextChanged)
443
544
  await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
444
545
  else {
445
- const updated = await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal);
446
- if (updated && this.agent.loginProvider === 'claude')
447
- throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
546
+ await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal);
448
547
  }
548
+ if (fresh && claudeContextChanged && !runtime.recoveredSession && !runtime.restartNotice)
549
+ details.record({ kind: 'session', severity: 'warning', code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', summary: '新要求已通过原会话恢复接口传送;官方仅承诺下次上下文整理后生效,本轮不能保证已立即采用。', payload: { reason: 'OFFICIAL_CONTEXT_UPDATE_DEFERRED' } });
550
+ if (!codex)
551
+ live.contextSha256 = contextSha256;
449
552
  if (contextChanged) {
450
553
  if (session.id === previous.sessionId)
451
554
  throw new NeedError('SESSION_STATE_INVALID', '新会话标识与原会话相同,无法确认独立承接,本轮未发送', '独立的新远端会话标识');
@@ -457,24 +560,23 @@ export class CodexRemoteAdapter extends LlmAdapter {
457
560
  const actual = await session.selectModel(selected.value, options.reasoningEffort, modelSignal);
458
561
  catalog.assertCurrent();
459
562
  session.assertModelSelection(selected.value, options.reasoningEffort);
460
- let resolved = selected.resolvedModel;
461
- if (this.agent.loginProvider === 'claude') {
462
- const evidence = session.resolvedModelEvidence;
463
- if (!resolved || (selected.value !== resolved && (!evidence || evidence.modelValue !== selected.value || evidence.resolvedModel !== resolved)))
464
- throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '所选模型的具体型号未获得当前会话确认,本轮未发送', '当前会话的具体型号证据');
465
- if (evidence && evidence.resolvedModel !== resolved)
466
- throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '当前会话具体型号与所选目录不一致,本轮未发送', '完全一致的具体型号证据');
467
- }
468
- if (!resolved)
469
- throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '远端没有提供具体型号证据,本轮未发送', '具体型号证据');
470
- modelConfiguration = { selected: { ...selected, ...(options.reasoningEffort === undefined ? {} : { effort: options.reasoningEffort }) }, configured: actual, resolvedModel: resolved, status: 'remote-configured' };
471
- details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;不代表推理效果已验证', payload: modelConfiguration });
563
+ // This lease is tied to the trusted host-user and instance/epoch.
564
+ // Its mapping and this turn's exact setter ACK are different facts.
565
+ const mapping = catalog.models.filter(model => model.value === selected.value);
566
+ if (mapping.length !== 1 || mapping[0].id !== selected.id)
567
+ throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '所选模型在当前主机目录中没有唯一对应型号,本轮未发送', 'CATALOG_VALUE_MAPPING_NOT_UNIQUE');
568
+ const resolved = mapping[0].resolvedModel;
569
+ if (typeof resolved !== 'string' || !resolved.trim())
570
+ throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '主机目录没有提供所选模型的具体型号,本轮未发送', 'CATALOG_RESOLVED_MODEL_MISSING');
571
+ modelConfiguration = { selected: { ...selected, ...(options.reasoningEffort === undefined ? {} : { effort: options.reasoningEffort }) }, configured: actual, resolvedModel: resolved, status: 'remote-configured', ...(this.agent.loginProvider === 'claude' ? { resolvedModelSource: 'host-user-catalog', sessionModelReported: false } : {}) };
572
+ details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
472
573
  }
473
574
  catch (error) {
474
- 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 : error instanceof ModelConfigurationError ? error.message : 'MODEL_CONFIGURATION_FAILED' } });
575
+ 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' } });
475
576
  throw error;
476
577
  }
477
578
  await timing.run('details-flush', () => details.flush());
579
+ recordInformation();
478
580
  await details.flush();
479
581
  details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
480
582
  catalog.assertCurrent();
@@ -519,6 +621,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
519
621
  yield chunk;
520
622
  }
521
623
  }
624
+ recordInformation();
522
625
  details.endTurn();
523
626
  await timing.run('details-flush', () => details.flush());
524
627
  await details.flush(); // The timing record is a new ordered write, not a retry.
@@ -528,10 +631,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
528
631
  live.usageSnapshot = mapper.usageSnapshot;
529
632
  if (modelConfiguration) {
530
633
  const actual = session.modelConfiguration;
531
- if (!actual?.confirmed || actual.model !== modelConfiguration.configured.model || actual.effort !== modelConfiguration.configured.effort)
532
- throw new ModelConfigurationError('远端会话在本轮期间改变了模型配置,不能沿用旧回执');
634
+ const changed = !actual?.confirmed || actual.model !== modelConfiguration.configured.model || actual.effort !== modelConfiguration.configured.effort;
533
635
  catalog.assertCurrent();
534
- yield* mapper.text(`\n\n本轮所选模型:${modelConfiguration.selected.name}(${modelConfiguration.selected.value});推理等级:${modelConfiguration.selected.effort ?? '沿用远端配置'}。远端已配置模型:${modelConfiguration.configured.model};具体型号:${modelConfiguration.resolvedModel};推理等级:${modelConfiguration.configured.effort ?? '远端未提供'}(配置已确认,推理效果未验证)。`);
636
+ if (changed) {
637
+ 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 } });
638
+ await details.flush();
639
+ }
535
640
  }
536
641
  yield* mapper.flush();
537
642
  let recoveryCheckpointId;
@@ -541,12 +646,16 @@ export class CodexRemoteAdapter extends LlmAdapter {
541
646
  recoveryCheckpointId = id;
542
647
  }
543
648
  const previousRemoteSessions = [...(previous?.previousRemoteSessions ?? []), ...(contextChanged ? [{ sessionId: previous.sessionId, contextSha256: previous.contextSha256 }] : [])];
544
- const pointer = { ...(previousRemoteSessions.length ? { previousRemoteSessions } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), ...(recoveryCheckpointId ? { recoveryCheckpointId } : {}), ...(runtime.workspaceNotice ? { presentation: { workspaceNotice: runtime.workspaceNotice } } : {}), 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 } : {}) };
649
+ const presentation = { ...(runtime.workspaceNotice ? { workspaceNotice: runtime.workspaceNotice } : {}), ...(employeeNoticeShown ? { employeeExecutionNotice: 'local-v1' } : {}) };
650
+ 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 } : {}) };
545
651
  succeeded = true;
546
652
  yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
547
653
  }
548
654
  catch (error) {
549
- if (live && (remoteAttempted || runtime === live.runtime))
655
+ runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
656
+ recordInformation();
657
+ failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
658
+ if (live && session && session.promptDispatchCount > promptBaseline)
550
659
  live.failure = { cause: error };
551
660
  let reportingFailure;
552
661
  if (request?.recordEarlyFailure) {
@@ -580,18 +689,32 @@ export class CodexRemoteAdapter extends LlmAdapter {
580
689
  throw error;
581
690
  }
582
691
  finally {
692
+ for (const off of abortOffs)
693
+ off();
583
694
  runtime?.execution?.endTurn();
584
695
  catalog?.release();
585
696
  try {
586
697
  if (preserveRecoveredRuntime || this.shuttingDown || key && this.detached.has(key)) {
587
- session?.close();
588
- await runtime?.detach?.();
698
+ session?.close({ layer: 'adapter', initiator: 'adapter.turn-detach', reason: 'TURN_DETACHED', action: 'close' });
699
+ await runtime?.detach?.({ layer: 'adapter', initiator: 'adapter.turn-detach', reason: 'TURN_DETACHED', action: 'detach' });
589
700
  }
590
701
  else if (!succeeded || !key || mode !== 'session') {
591
- session?.close();
592
- if (runtime)
702
+ session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
703
+ if (runtime) {
593
704
  await this.cleanupRuntime(runtime);
705
+ if (key && live && !live.failure && this.sessions.get(key) === live) {
706
+ if (typeof runtime.dispose === 'function') {
707
+ this.sessions.delete(key);
708
+ if (!succeeded)
709
+ this.preparationFailures.set(key, failureCode ?? 'TURN_PREPARATION_FAILED');
710
+ }
711
+ else
712
+ live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
713
+ }
714
+ }
594
715
  }
716
+ if (succeeded && key)
717
+ this.preparationFailures.delete(key);
595
718
  }
596
719
  finally {
597
720
  if (key && this.turns.get(key) === turn)
@@ -612,6 +735,9 @@ export class RemoteAgentsAdapter extends LlmAdapter {
612
735
  providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
613
736
  listModels(provider) { return this.adapter(provider).listModels(provider); }
614
737
  resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
738
+ prepareCall(provider, model, signal) { return this.adapter(provider).prepareCall(provider, model, signal); }
739
+ observeModelInvalidation(fields, changes = []) { for (const adapter of this.adapters.values())
740
+ adapter.observeModelInvalidation(fields, changes); }
615
741
  async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
616
742
  async invalidateEmployeeState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateEmployeeState())); }
617
743
  providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
@@ -646,6 +772,7 @@ export function apply(ctx, live) {
646
772
  };
647
773
  const preflightDiagnostics = createPreflightDiagnostics({ ctx, getApi: get, manager: managerForProfile });
648
774
  let changed = () => { };
775
+ let observeCatalogRevocation = () => { };
649
776
  const diagnostics = new Map();
650
777
  const diagnostic = (scope, provider, message) => {
651
778
  let entry = diagnostics.get(provider);
@@ -667,7 +794,7 @@ export function apply(ctx, live) {
667
794
  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() ?? [])] })) }));
668
795
  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) => {
669
796
  diagnostic('catalog', AGENTS.find(agent => agent.loginProvider === brand).id, message);
670
- }, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent));
797
+ }, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent), (fields, changes) => observeCatalogRevocation(fields, changes));
671
798
  const adapter = new RemoteAgentsAdapter(config, agent => {
672
799
  const details = async (request, api, connection, target) => {
673
800
  return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection, localAcceptanceId: request.localAcceptanceId ?? request.localAcceptance?.acceptanceId });
@@ -758,6 +885,7 @@ export function apply(ctx, live) {
758
885
  child.effect(() => () => { current.dispose(); if (defaults === current)
759
886
  defaults = undefined; });
760
887
  });
888
+ observeCatalogRevocation = (fields, changes) => adapter.observeModelInvalidation(fields, changes);
761
889
  changed = () => { registration.replace(ids); refreshDefault(); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
762
890
  let employeeConnect = serviceValue('aiWorkbenchConnect'), employeeNetwork = serviceValue('aiWorkbenchNetwork');
763
891
  ctx.on('internal/service', service => {