@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.
- package/README.md +10 -8
- package/dist/acp.d.ts +12 -7
- package/dist/acp.js +65 -41
- package/dist/broker-runtime.js +46 -20
- package/dist/broker.d.ts +4 -2
- package/dist/broker.js +18 -9
- package/dist/claude-local.d.ts +2 -1
- package/dist/claude-local.js +62 -35
- package/dist/close-diagnostics.d.ts +56 -0
- package/dist/close-diagnostics.js +104 -0
- package/dist/conversation-details.js +67 -9
- package/dist/employee-endpoint.d.ts +3 -1
- package/dist/employee-endpoint.js +8 -6
- package/dist/employee-exec.d.ts +7 -4
- package/dist/employee-exec.js +39 -29
- package/dist/employee-mcp.d.ts +6 -3
- package/dist/employee-mcp.js +75 -39
- package/dist/employee-presentation.d.ts +6 -0
- package/dist/employee-presentation.js +16 -0
- package/dist/employee-provider.js +59 -36
- package/dist/employee-shell.d.ts +2 -0
- package/dist/employee-shell.js +17 -5
- package/dist/employee-ssh.d.ts +2 -0
- package/dist/employee-ssh.js +6 -4
- package/dist/employee-tools.d.ts +3 -1
- package/dist/employee-tools.js +6 -5
- package/dist/index.d.ts +18 -4
- package/dist/index.js +183 -55
- package/dist/model-catalog.d.ts +9 -1
- package/dist/model-catalog.js +63 -5
- package/dist/model-configuration.d.ts +14 -1
- package/dist/model-configuration.js +122 -89
- package/dist/model-presentation.d.ts +13 -0
- package/dist/model-presentation.js +18 -0
- package/dist/presentation.js +2 -1
- package/dist/provider.d.ts +8 -2
- package/dist/recovery.d.ts +4 -0
- package/dist/rpc-namespace.d.ts +2 -2
- package/dist/rpc-namespace.js +2 -1
- package/package.json +1 -1
package/dist/employee-tools.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
273
|
-
if (live && !previous)
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
532
|
-
throw new ModelConfigurationError('远端会话在本轮期间改变了模型配置,不能沿用旧回执');
|
|
634
|
+
const changed = !actual?.confirmed || actual.model !== modelConfiguration.configured.model || actual.effort !== modelConfiguration.configured.effort;
|
|
533
635
|
catalog.assertCurrent();
|
|
534
|
-
|
|
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
|
|
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
|
-
|
|
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 => {
|