@ai-workbench/remote-agent 0.8.12 → 0.8.14
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 +11 -7
- 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 +61 -34
- 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-provider.js +58 -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 +4 -0
- package/dist/index.js +133 -44
- package/dist/local-user-turn.js +2 -1
- 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/presentation.js +3 -2
- 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/dist/session-header.d.ts +4 -0
- package/dist/session-header.js +12 -0
- package/package.json +3 -3
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';
|
|
@@ -54,13 +55,20 @@ export const Config = Schema.object({
|
|
|
54
55
|
export function readConfig(config) {
|
|
55
56
|
return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, typeof value === 'object' && value !== null && 'get' in value ? value.get() : value]));
|
|
56
57
|
}
|
|
58
|
+
function modelResolutionLabel(configuration) {
|
|
59
|
+
return configuration.resolvedModelSource === 'host-user-catalog'
|
|
60
|
+
? `型号:${configuration.resolvedModel}(来源:主机目录映射 ${configuration.selected.value};会话未单独回报具体型号)`
|
|
61
|
+
: `具体型号:${configuration.resolvedModel}`;
|
|
62
|
+
}
|
|
57
63
|
function validContextDigest(value) { return typeof value === 'string' && /^[a-fA-F0-9]{64}$/u.test(value); }
|
|
58
64
|
function readPointer(options, agent) {
|
|
59
65
|
const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
|
|
60
66
|
if (!last || last.role !== 'assistant')
|
|
61
67
|
return;
|
|
62
|
-
if (last.source.provider !== options.provider
|
|
63
|
-
throw new NeedError('SESSION_STATE_MISSING', '
|
|
68
|
+
if (last.source.provider !== options.provider)
|
|
69
|
+
throw new NeedError('SESSION_STATE_MISSING', '已有对话属于其他模型通道,不能沿用其远端进程', '当前模型的远端恢复记录');
|
|
70
|
+
if (last.source.replayState === undefined)
|
|
71
|
+
return;
|
|
64
72
|
const envelope = last.source.replayState;
|
|
65
73
|
const pointer = envelope?.response;
|
|
66
74
|
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 +112,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
104
112
|
shuttingDown = false;
|
|
105
113
|
detached = new Set();
|
|
106
114
|
cleanups = new WeakMap();
|
|
115
|
+
preparationFailures = new Map();
|
|
107
116
|
constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics) {
|
|
108
117
|
super();
|
|
109
118
|
this.resolveRuntime = resolveRuntime;
|
|
@@ -141,16 +150,24 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
141
150
|
markModelPreparationFailure(provider, error, signal);
|
|
142
151
|
throw error;
|
|
143
152
|
} }
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
async
|
|
153
|
+
observeModelInvalidation(fields, changes = []) { for (const live of this.sessions.values())
|
|
154
|
+
live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'catalog.signature-change', reason: 'MODEL_CATALOG_SIGNATURE_CHANGED', action: 'invalidate', changedFields: fields, changes }); }
|
|
155
|
+
async invalidateModelState() { for (const [id, turn] of this.turns) {
|
|
156
|
+
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 }] });
|
|
157
|
+
turn.abort();
|
|
158
|
+
} await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
|
|
159
|
+
async invalidateEmployeeState() { for (const live of this.sessions.values())
|
|
160
|
+
if (live.runtime.employeeExecutionMeta || live.runtime.execution)
|
|
161
|
+
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
162
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
148
|
-
cleanupRuntime(runtime) {
|
|
163
|
+
cleanupRuntime(runtime, cause = { layer: 'adapter', initiator: 'adapter.cleanup-runtime', reason: 'RUNTIME_CLEANUP', action: 'dispose' }) {
|
|
164
|
+
runtime.closeDiagnostics?.record(cause);
|
|
149
165
|
const existing = this.cleanups.get(runtime);
|
|
150
166
|
if (existing)
|
|
151
167
|
return existing;
|
|
152
|
-
const cleanup = Promise.resolve().then(() => runtime.dispose?.()).catch(
|
|
153
|
-
const
|
|
168
|
+
const cleanup = Promise.resolve().then(() => runtime.dispose?.(cause)).catch(error => {
|
|
169
|
+
const codes = closeErrorCodes(error, 'REMOTE_CLEANUP_UNCONFIRMED');
|
|
170
|
+
const failure = closeFailure('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话的清理结果未确认,不能继续或重开此会话', '管理员核对原主机会话;收尾代码 ' + codes.join(','), codes);
|
|
154
171
|
for (const live of this.sessions.values())
|
|
155
172
|
if (live.runtime === runtime) {
|
|
156
173
|
live.cleanupFailure = failure;
|
|
@@ -162,13 +179,14 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
162
179
|
return cleanup;
|
|
163
180
|
}
|
|
164
181
|
async disposeSession(sessionId) {
|
|
182
|
+
this.sessions.get(sessionId)?.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'abort' });
|
|
165
183
|
this.turns.get(sessionId)?.abort();
|
|
166
184
|
const current = this.sessions.get(sessionId);
|
|
167
185
|
if (!current)
|
|
168
186
|
return;
|
|
169
187
|
if (current.disposal)
|
|
170
188
|
return current.disposal;
|
|
171
|
-
current.session.close();
|
|
189
|
+
current.session.close({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'close' });
|
|
172
190
|
current.disposal = this.cleanupRuntime(current.runtime).then(() => { if (this.sessions.get(sessionId) === current)
|
|
173
191
|
this.sessions.delete(sessionId); });
|
|
174
192
|
return current.disposal;
|
|
@@ -177,13 +195,14 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
177
195
|
const current = this.sessions.get(sessionId), turn = this.turns.get(sessionId);
|
|
178
196
|
if (!current && !turn)
|
|
179
197
|
return;
|
|
198
|
+
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
199
|
this.detached.add(sessionId);
|
|
181
200
|
if (current) {
|
|
182
201
|
try {
|
|
183
|
-
await current.runtime.detach?.();
|
|
202
|
+
await current.runtime.detach?.({ layer: 'adapter', initiator: 'adapter.detach-session', reason: 'SESSION_DETACHED', action: 'detach' });
|
|
184
203
|
}
|
|
185
204
|
finally {
|
|
186
|
-
current.session.close();
|
|
205
|
+
current.session.close({ layer: 'adapter', initiator: 'adapter.detach-finally', reason: 'SESSION_DETACHED', action: 'close' });
|
|
187
206
|
this.sessions.delete(sessionId);
|
|
188
207
|
turn?.abort();
|
|
189
208
|
}
|
|
@@ -192,6 +211,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
192
211
|
turn?.abort();
|
|
193
212
|
}
|
|
194
213
|
async dispose() {
|
|
214
|
+
for (const live of this.sessions.values())
|
|
215
|
+
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
216
|
this.shuttingDown = true;
|
|
196
217
|
const results = await Promise.allSettled([...new Set([...this.sessions.keys(), ...this.turns.keys()])].map(id => this.detachSession(id)));
|
|
197
218
|
this.lifetime.abort();
|
|
@@ -239,6 +260,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
239
260
|
const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
|
|
240
261
|
const options = { ...original, signal };
|
|
241
262
|
const timing = new StageTiming();
|
|
263
|
+
const abortOffs = [];
|
|
242
264
|
let request;
|
|
243
265
|
let earlyStage = 'input-prepare';
|
|
244
266
|
let runtime;
|
|
@@ -246,14 +268,25 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
246
268
|
let live;
|
|
247
269
|
let succeeded = false;
|
|
248
270
|
let remoteAttempted = false;
|
|
271
|
+
let promptBaseline = 0;
|
|
272
|
+
let failureCode;
|
|
249
273
|
let preserveRecoveredRuntime = false;
|
|
250
274
|
let activeMapper;
|
|
251
275
|
let catalog;
|
|
252
276
|
let selected;
|
|
253
277
|
let modelConfiguration;
|
|
278
|
+
const information = [];
|
|
279
|
+
const recordInformation = () => { if (runtime?.details)
|
|
280
|
+
for (const entry of information.splice(0))
|
|
281
|
+
runtime.details.record(entry); };
|
|
254
282
|
const config = this.config();
|
|
255
283
|
const mode = this.agent.oneShot ? config.mode : 'session';
|
|
256
284
|
request = { sessionId: options.sessionId, mode: mode ?? 'session', timing, signal };
|
|
285
|
+
const observeAbort = (source, initiator, reason) => { if (!source)
|
|
286
|
+
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)); };
|
|
287
|
+
observeAbort(original.signal, 'signal.caller', 'CALLER_ABORTED');
|
|
288
|
+
observeAbort(this.lifetime.signal, 'signal.plugin-lifetime', 'PLUGIN_DISPOSED');
|
|
289
|
+
observeAbort(turn.signal, 'signal.adapter-turn', 'TURN_ABORTED');
|
|
257
290
|
this.preflightDiagnostics?.begin(request, this.agent.loginProvider);
|
|
258
291
|
if (this.preflightDiagnostics)
|
|
259
292
|
request.recordEarlyFailure = (error, stage) => this.preflightDiagnostics.failure(request, this.agent.loginProvider, error, stage);
|
|
@@ -261,21 +294,39 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
261
294
|
if (!mode || (mode === 'session' && (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)))
|
|
262
295
|
throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的mode/acpMaxMessageBytes');
|
|
263
296
|
const previous = readPointer(options, this.agent);
|
|
297
|
+
const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
|
|
298
|
+
const missingPointer = !!last && !previous;
|
|
299
|
+
let requireFreshSession = missingPointer || !!key && this.preparationFailures.has(key);
|
|
264
300
|
if (previous && previous.mode !== mode)
|
|
265
301
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '运行模式与原会话不一致,请新建会话', '原会话模式');
|
|
266
302
|
if (previous && !previous.modelConfiguration)
|
|
267
|
-
|
|
303
|
+
information.push({ kind: 'session', severity: 'info', code: 'TURN_GATE_INFORMATION', summary: '历史回复没有模型配置资料;本轮会重新确认你当前选择的模型。', payload: { reason: 'HISTORICAL_MODEL_CONFIGURATION_ABSENT' } });
|
|
268
304
|
live = key ? this.sessions.get(key) : undefined;
|
|
269
305
|
if (live?.disposal || live?.cleanupFailure)
|
|
270
306
|
throw live.cleanupFailure ?? new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原主机会话正在结束,清理结果尚未确认,不能继续或重开此会话', '管理员核对原主机会话清理结果');
|
|
271
307
|
if (live?.failure)
|
|
272
|
-
throw new NeedError('
|
|
273
|
-
if (live && !previous)
|
|
274
|
-
|
|
308
|
+
throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '上一轮输入已经交给远端,但结果未确认,不能自动重发', '核对已发送输入及原主机会话状态');
|
|
309
|
+
if (live && !previous) {
|
|
310
|
+
if (typeof live.runtime.dispose !== 'function')
|
|
311
|
+
throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '原会话不能确认已安全结束,本轮尚未发送', '可确认结束原实例的通道');
|
|
312
|
+
live.session.close({ layer: 'adapter', initiator: 'adapter.missing-pointer', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'close' });
|
|
313
|
+
await this.cleanupRuntime(live.runtime, { layer: 'adapter', initiator: 'adapter.missing-pointer', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'dispose' });
|
|
314
|
+
if (key && this.sessions.get(key) === live)
|
|
315
|
+
this.sessions.delete(key);
|
|
316
|
+
live = undefined;
|
|
317
|
+
requireFreshSession = true;
|
|
318
|
+
}
|
|
275
319
|
const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
276
320
|
if (live && live.configuration !== configuration)
|
|
277
321
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
|
|
278
322
|
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 }) });
|
|
323
|
+
if (requireFreshSession) {
|
|
324
|
+
request.requireFreshSession = true;
|
|
325
|
+
request.resumeSessionId = undefined;
|
|
326
|
+
request.recoveryCheckpointId = undefined;
|
|
327
|
+
information.push({ kind: 'session', severity: 'warning', code: 'TURN_GATE_INFORMATION', summary: '缺少可用的旧会话恢复资料,或上次在发送前已失败;将核对旧实例安全结束后,只处理本次接纳的输入。', payload: { reason: missingPointer ? 'PRIVATE_RECOVERY_POINTER_ABSENT' : 'PRIOR_PREPARATION_FAILED' } });
|
|
328
|
+
}
|
|
329
|
+
request.previousContextSha256 = previous?.contextSha256;
|
|
279
330
|
const after = options.messages.findLastIndex(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
|
|
280
331
|
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
332
|
request.userMessageIds = request.userInputIds;
|
|
@@ -293,8 +344,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
293
344
|
// New Codex transport scopes are independent of the native context digest.
|
|
294
345
|
if (contextChanged)
|
|
295
346
|
request.contextSha256 = undefined;
|
|
296
|
-
|
|
297
|
-
throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
|
|
347
|
+
const claudeContextChanged = !!previous && !codex && previous.contextSha256 !== contextSha256;
|
|
298
348
|
if (contextChanged)
|
|
299
349
|
input = await prepareContinuation(options, input, this.readImage);
|
|
300
350
|
if (live) {
|
|
@@ -314,16 +364,25 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
314
364
|
earlyStage = 'model-catalog';
|
|
315
365
|
catalog = await this.catalog(signal);
|
|
316
366
|
selected = catalog.models.find(model => model.id === options.model);
|
|
367
|
+
catalog.onInformation?.(entry => information.push(entry));
|
|
317
368
|
if (!selected)
|
|
318
369
|
throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
|
|
319
370
|
if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
|
|
320
371
|
throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
|
|
372
|
+
const observeSignal = (source, initiator, reason) => { const observed = () => { const diagnostic = request?.closeDiagnostics ?? runtime?.closeDiagnostics; if (diagnostic)
|
|
373
|
+
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)); };
|
|
374
|
+
observeSignal(catalog.signal, 'signal.model-catalog', 'MODEL_CATALOG_REVOKED');
|
|
375
|
+
observeSignal(signal, 'signal.turn-composite', 'ABORTED');
|
|
321
376
|
let modelSignal = AbortSignal.any([signal, catalog.signal]);
|
|
322
377
|
options.signal = modelSignal;
|
|
323
378
|
request.signal = modelSignal;
|
|
324
379
|
earlyStage = 'runtime-resolve';
|
|
325
380
|
runtime = await this.resolveRuntime(request);
|
|
381
|
+
const closeSink = runtime.details;
|
|
382
|
+
runtime.closeDiagnostics ??= new CloseDiagnostics(event => closeSink?.record(event));
|
|
326
383
|
signal.throwIfAborted();
|
|
384
|
+
if (requireFreshSession && (!runtime.freshSessionConfirmed || !runtime.restartNotice))
|
|
385
|
+
throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '未确认原实例安全结束或原位置为空,本轮没有发送', '同UUID条件结束与list消失证明,或原slot不存在证明');
|
|
327
386
|
if (runtime.execution?.signal) {
|
|
328
387
|
modelSignal = AbortSignal.any([modelSignal, runtime.execution.signal]);
|
|
329
388
|
options.signal = modelSignal;
|
|
@@ -357,6 +416,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
357
416
|
const details = runtime.details;
|
|
358
417
|
if (!details)
|
|
359
418
|
throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
|
|
419
|
+
recordInformation();
|
|
360
420
|
timing.bind(detail => details.record(detail));
|
|
361
421
|
request.recordEarlyFailure = undefined;
|
|
362
422
|
const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
|
|
@@ -390,7 +450,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
390
450
|
let done = false;
|
|
391
451
|
let failure;
|
|
392
452
|
const push = (chunks) => { queue.push(...chunks); wake?.(); wake = undefined; };
|
|
393
|
-
const hooks = { broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
|
|
453
|
+
const hooks = { closeDiagnostics: runtime.closeDiagnostics, broker: runtime.broker, employeeExecution: runtime.employeeExecution, timing, approve: async (permission, approvalSignal) => {
|
|
394
454
|
const execution = runtime.execution;
|
|
395
455
|
const toolName = permission.toolCall.name ?? permission.toolCall._meta?.claudeCode?.toolName;
|
|
396
456
|
if (execution) {
|
|
@@ -410,7 +470,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
410
470
|
}
|
|
411
471
|
catch (error) {
|
|
412
472
|
failure = error;
|
|
413
|
-
session?.close();
|
|
473
|
+
session?.close({ layer: 'adapter', initiator: 'adapter.mapper-failure', reason: closeCode(error, 'MAPPING_FAILED'), action: 'close' });
|
|
414
474
|
} }, onResponse: response => push(mapper.acpResponse(response)), onDetail: detail => details.record(detail) };
|
|
415
475
|
const fresh = !live;
|
|
416
476
|
if (fresh) {
|
|
@@ -426,6 +486,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
426
486
|
if (previous?.sessionId !== session.id)
|
|
427
487
|
throw new NeedError('SESSION_STATE_INVALID', '持久会话标识与当前进程不一致', '原会话的恢复标识');
|
|
428
488
|
}
|
|
489
|
+
promptBaseline = session.promptDispatchCount;
|
|
429
490
|
const task = (async () => {
|
|
430
491
|
remoteAttempted = true;
|
|
431
492
|
try {
|
|
@@ -436,16 +497,20 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
436
497
|
session.validatePrompt(input.prompt);
|
|
437
498
|
if (contextChanged)
|
|
438
499
|
await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
500
|
+
else if (claudeContextChanged)
|
|
501
|
+
await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal, true);
|
|
439
502
|
}
|
|
440
503
|
else if (fresh)
|
|
441
504
|
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
505
|
else if (contextChanged)
|
|
443
506
|
await session.newContextSession({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
444
507
|
else {
|
|
445
|
-
|
|
446
|
-
if (updated && this.agent.loginProvider === 'claude')
|
|
447
|
-
throw new NeedError('SYSTEM_PROMPT_UPDATE_PENDING', 'Claude原会话的系统或开发者要求已变化;主机仅承诺下次上下文整理后生效,本轮未发送,请新建会话应用新要求', '新建Claude会话以立即应用当前系统与开发者要求');
|
|
508
|
+
await session.updateContext({ systemPrompt: input.systemPrompt }, modelSignal);
|
|
448
509
|
}
|
|
510
|
+
if (fresh && claudeContextChanged && !runtime.recoveredSession && !runtime.restartNotice)
|
|
511
|
+
details.record({ kind: 'session', severity: 'warning', code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', summary: '新要求已通过原会话恢复接口传送;官方仅承诺下次上下文整理后生效,本轮不能保证已立即采用。', payload: { reason: 'OFFICIAL_CONTEXT_UPDATE_DEFERRED' } });
|
|
512
|
+
if (!codex)
|
|
513
|
+
live.contextSha256 = contextSha256;
|
|
449
514
|
if (contextChanged) {
|
|
450
515
|
if (session.id === previous.sessionId)
|
|
451
516
|
throw new NeedError('SESSION_STATE_INVALID', '新会话标识与原会话相同,无法确认独立承接,本轮未发送', '独立的新远端会话标识');
|
|
@@ -457,24 +522,23 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
457
522
|
const actual = await session.selectModel(selected.value, options.reasoningEffort, modelSignal);
|
|
458
523
|
catalog.assertCurrent();
|
|
459
524
|
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 });
|
|
525
|
+
// This lease is tied to the trusted host-user and instance/epoch.
|
|
526
|
+
// Its mapping and this turn's exact setter ACK are different facts.
|
|
527
|
+
const mapping = catalog.models.filter(model => model.value === selected.value);
|
|
528
|
+
if (mapping.length !== 1 || mapping[0].id !== selected.id)
|
|
529
|
+
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '所选模型在当前主机目录中没有唯一对应型号,本轮未发送', 'CATALOG_VALUE_MAPPING_NOT_UNIQUE');
|
|
530
|
+
const resolved = mapping[0].resolvedModel;
|
|
531
|
+
if (typeof resolved !== 'string' || !resolved.trim())
|
|
532
|
+
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '主机目录没有提供所选模型的具体型号,本轮未发送', 'CATALOG_RESOLVED_MODEL_MISSING');
|
|
533
|
+
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 } : {}) };
|
|
534
|
+
details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
|
|
472
535
|
}
|
|
473
536
|
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' } });
|
|
537
|
+
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
538
|
throw error;
|
|
476
539
|
}
|
|
477
540
|
await timing.run('details-flush', () => details.flush());
|
|
541
|
+
recordInformation();
|
|
478
542
|
await details.flush();
|
|
479
543
|
details.beginTurn(runtime.detailContext?.() ?? { turn: null, step: null });
|
|
480
544
|
catalog.assertCurrent();
|
|
@@ -519,6 +583,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
519
583
|
yield chunk;
|
|
520
584
|
}
|
|
521
585
|
}
|
|
586
|
+
recordInformation();
|
|
522
587
|
details.endTurn();
|
|
523
588
|
await timing.run('details-flush', () => details.flush());
|
|
524
589
|
await details.flush(); // The timing record is a new ordered write, not a retry.
|
|
@@ -528,10 +593,13 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
528
593
|
live.usageSnapshot = mapper.usageSnapshot;
|
|
529
594
|
if (modelConfiguration) {
|
|
530
595
|
const actual = session.modelConfiguration;
|
|
531
|
-
|
|
532
|
-
throw new ModelConfigurationError('远端会话在本轮期间改变了模型配置,不能沿用旧回执');
|
|
596
|
+
const changed = !actual?.confirmed || actual.model !== modelConfiguration.configured.model || actual.effort !== modelConfiguration.configured.effort;
|
|
533
597
|
catalog.assertCurrent();
|
|
534
|
-
|
|
598
|
+
if (changed) {
|
|
599
|
+
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 } });
|
|
600
|
+
await details.flush();
|
|
601
|
+
}
|
|
602
|
+
yield* mapper.text(`\n\n本轮所选模型:${modelConfiguration.selected.name}(${modelConfiguration.selected.value});推理等级:${modelConfiguration.selected.effort ?? '沿用远端配置'}。发送前远端配置回执:${modelConfiguration.configured.model};${modelResolutionLabel(modelConfiguration)};推理等级:${modelConfiguration.configured.effort ?? '远端未提供'}(${changed ? '本轮期间配置有变化,实际执行型号未单独回报' : '发送前配置已确认,推理效果未验证'})。`);
|
|
535
603
|
}
|
|
536
604
|
yield* mapper.flush();
|
|
537
605
|
let recoveryCheckpointId;
|
|
@@ -546,7 +614,10 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
546
614
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
547
615
|
}
|
|
548
616
|
catch (error) {
|
|
549
|
-
|
|
617
|
+
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
618
|
+
recordInformation();
|
|
619
|
+
failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
|
|
620
|
+
if (live && session && session.promptDispatchCount > promptBaseline)
|
|
550
621
|
live.failure = { cause: error };
|
|
551
622
|
let reportingFailure;
|
|
552
623
|
if (request?.recordEarlyFailure) {
|
|
@@ -580,18 +651,32 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
580
651
|
throw error;
|
|
581
652
|
}
|
|
582
653
|
finally {
|
|
654
|
+
for (const off of abortOffs)
|
|
655
|
+
off();
|
|
583
656
|
runtime?.execution?.endTurn();
|
|
584
657
|
catalog?.release();
|
|
585
658
|
try {
|
|
586
659
|
if (preserveRecoveredRuntime || this.shuttingDown || key && this.detached.has(key)) {
|
|
587
|
-
session?.close();
|
|
588
|
-
await runtime?.detach?.();
|
|
660
|
+
session?.close({ layer: 'adapter', initiator: 'adapter.turn-detach', reason: 'TURN_DETACHED', action: 'close' });
|
|
661
|
+
await runtime?.detach?.({ layer: 'adapter', initiator: 'adapter.turn-detach', reason: 'TURN_DETACHED', action: 'detach' });
|
|
589
662
|
}
|
|
590
663
|
else if (!succeeded || !key || mode !== 'session') {
|
|
591
|
-
session?.close();
|
|
592
|
-
if (runtime)
|
|
664
|
+
session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
|
|
665
|
+
if (runtime) {
|
|
593
666
|
await this.cleanupRuntime(runtime);
|
|
667
|
+
if (key && live && !live.failure && this.sessions.get(key) === live) {
|
|
668
|
+
if (typeof runtime.dispose === 'function') {
|
|
669
|
+
this.sessions.delete(key);
|
|
670
|
+
if (!succeeded)
|
|
671
|
+
this.preparationFailures.set(key, failureCode ?? 'TURN_PREPARATION_FAILED');
|
|
672
|
+
}
|
|
673
|
+
else
|
|
674
|
+
live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
|
|
675
|
+
}
|
|
676
|
+
}
|
|
594
677
|
}
|
|
678
|
+
if (succeeded && key)
|
|
679
|
+
this.preparationFailures.delete(key);
|
|
595
680
|
}
|
|
596
681
|
finally {
|
|
597
682
|
if (key && this.turns.get(key) === turn)
|
|
@@ -612,6 +697,8 @@ export class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
612
697
|
providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
|
|
613
698
|
listModels(provider) { return this.adapter(provider).listModels(provider); }
|
|
614
699
|
resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
|
|
700
|
+
observeModelInvalidation(fields, changes = []) { for (const adapter of this.adapters.values())
|
|
701
|
+
adapter.observeModelInvalidation(fields, changes); }
|
|
615
702
|
async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
|
|
616
703
|
async invalidateEmployeeState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateEmployeeState())); }
|
|
617
704
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
@@ -646,6 +733,7 @@ export function apply(ctx, live) {
|
|
|
646
733
|
};
|
|
647
734
|
const preflightDiagnostics = createPreflightDiagnostics({ ctx, getApi: get, manager: managerForProfile });
|
|
648
735
|
let changed = () => { };
|
|
736
|
+
let observeCatalogRevocation = () => { };
|
|
649
737
|
const diagnostics = new Map();
|
|
650
738
|
const diagnostic = (scope, provider, message) => {
|
|
651
739
|
let entry = diagnostics.get(provider);
|
|
@@ -667,7 +755,7 @@ export function apply(ctx, live) {
|
|
|
667
755
|
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
756
|
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
757
|
diagnostic('catalog', AGENTS.find(agent => agent.loginProvider === brand).id, message);
|
|
670
|
-
}, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent));
|
|
758
|
+
}, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent), (fields, changes) => observeCatalogRevocation(fields, changes));
|
|
671
759
|
const adapter = new RemoteAgentsAdapter(config, agent => {
|
|
672
760
|
const details = async (request, api, connection, target) => {
|
|
673
761
|
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 +846,7 @@ export function apply(ctx, live) {
|
|
|
758
846
|
child.effect(() => () => { current.dispose(); if (defaults === current)
|
|
759
847
|
defaults = undefined; });
|
|
760
848
|
});
|
|
849
|
+
observeCatalogRevocation = (fields, changes) => adapter.observeModelInvalidation(fields, changes);
|
|
761
850
|
changed = () => { registration.replace(ids); refreshDefault(); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
|
|
762
851
|
let employeeConnect = serviceValue('aiWorkbenchConnect'), employeeNetwork = serviceValue('aiWorkbenchNetwork');
|
|
763
852
|
ctx.on('internal/service', service => {
|
package/dist/local-user-turn.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
|
+
import { sameStoredSessionHeader } from "./session-header.js";
|
|
2
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
import { NeedError } from "./remote.js";
|
|
4
5
|
const failure = (code = 'REMOTE_DETAIL_SCOPE_INVALID') => new NeedError(code, '本轮输入无法核对,已停止本次处理。', '官方当前员工输入与所选远程品牌');
|
|
@@ -20,7 +21,7 @@ export async function readLocalUserTurn(ctx, request) {
|
|
|
20
21
|
let proof, firstFailure;
|
|
21
22
|
try {
|
|
22
23
|
assertCurrent();
|
|
23
|
-
if (handle.id !== id || handle.access !== 'read' || !
|
|
24
|
+
if (handle.id !== id || handle.access !== 'read' || !sameStoredSessionHeader(handle.header, header) || handle.inheritedEventCount !== session.inheritedEventCount)
|
|
24
25
|
throw failure();
|
|
25
26
|
const { events } = await handle.read();
|
|
26
27
|
assertCurrent();
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { CloseChange } from './close-diagnostics.ts';
|
|
1
2
|
import { type LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
|
|
2
3
|
import type { ModelCatalogTarget } from '@ai-workbench/connect/api';
|
|
3
4
|
import type { AgentDefinition } from './agents.ts';
|
|
4
5
|
import { type ConnectApi, type ConnectTarget } from './provider.ts';
|
|
6
|
+
import type { DetailObservation } from './conversation-details.ts';
|
|
5
7
|
export { runCatalog } from './catalog-ssh.ts';
|
|
6
8
|
export interface HostModel {
|
|
7
9
|
id: string;
|
|
@@ -24,6 +26,7 @@ export interface CatalogLease {
|
|
|
24
26
|
readonly signal: AbortSignal;
|
|
25
27
|
assertCurrent(): void;
|
|
26
28
|
release(): void;
|
|
29
|
+
onInformation?(listener: (entry: DetailObservation) => void): () => void;
|
|
27
30
|
}
|
|
28
31
|
export interface ModelDirectory {
|
|
29
32
|
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
@@ -42,6 +45,7 @@ export declare function catalogDiagnostic(issues: readonly CatalogIssue[]): stri
|
|
|
42
45
|
/** Defaults are provider declarations, never catalog order or display names. */
|
|
43
46
|
export declare function declaredDefault(agent: AgentDefinition, models: readonly HostModel[]): HostModel | undefined;
|
|
44
47
|
export declare function parseHostCatalog(input: string, provider: string, username: string): ParsedCatalog;
|
|
48
|
+
export type CatalogChangeField = 'api-instance' | 'connection-instance' | 'connection-epoch' | 'connection-phase' | 'connection-need-code' | 'catalog-capability' | 'codex-target' | 'claude-target' | 'other-metadata';
|
|
45
49
|
/** Share only pending work; settled results are never cached. */
|
|
46
50
|
export declare class ModelCatalog implements ModelDirectory {
|
|
47
51
|
private get;
|
|
@@ -50,6 +54,7 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
50
54
|
private changed;
|
|
51
55
|
private diagnostic;
|
|
52
56
|
private loaded;
|
|
57
|
+
private revoking;
|
|
53
58
|
private api?;
|
|
54
59
|
private unsubscribe?;
|
|
55
60
|
private signature?;
|
|
@@ -57,10 +62,13 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
57
62
|
private disposed;
|
|
58
63
|
private initialized;
|
|
59
64
|
private active;
|
|
65
|
+
private presentation?;
|
|
66
|
+
private presentationNeedCode?;
|
|
67
|
+
private information;
|
|
60
68
|
private flights;
|
|
61
69
|
private serial;
|
|
62
70
|
private latest;
|
|
63
|
-
constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner, changed?: () => void, diagnostic?: (provider: 'claude' | 'codex', message: string | undefined) => void, loaded?: (agent: AgentDefinition, models: readonly HostModel[], assertCurrent: () => void) => void | Promise<void
|
|
71
|
+
constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner, changed?: () => void, diagnostic?: (provider: 'claude' | 'codex', message: string | undefined) => void, loaded?: (agent: AgentDefinition, models: readonly HostModel[], assertCurrent: () => void) => void | Promise<void>, revoking?: (fields: readonly CatalogChangeField[], changes: readonly CloseChange[]) => void);
|
|
64
72
|
refresh(): void;
|
|
65
73
|
private trusted;
|
|
66
74
|
private assertFlight;
|