@ai-workbench/remote-agent 0.8.15 → 0.8.17
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 -5
- package/dist/acp.js +1 -1
- package/dist/claude-local.js +2 -0
- package/dist/close-diagnostics.d.ts +1 -6
- package/dist/close-diagnostics.js +4 -34
- package/dist/connect-contract.d.ts +5 -4
- package/dist/connect-need.d.ts +12 -0
- package/dist/connect-need.js +34 -0
- package/dist/conversation-details.js +1 -1
- package/dist/employee-location.js +2 -1
- package/dist/employee-provider.js +16 -7
- package/dist/failure-evidence.d.ts +12 -0
- package/dist/failure-evidence.js +83 -0
- package/dist/host-workspace.d.ts +13 -0
- package/dist/host-workspace.js +110 -0
- package/dist/index.js +38 -20
- package/dist/model-catalog.d.ts +11 -1
- package/dist/model-catalog.js +35 -7
- package/dist/preflight-diagnostic.js +4 -3
- package/dist/presentation.js +4 -0
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +124 -71
- package/dist/remote-details.js +55 -27
- package/dist/remote.d.ts +2 -0
- package/dist/remote.js +1 -0
- package/dist/workspace-source.js +4 -2
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { connectNeedMessage } from "./connect-need.js";
|
|
2
|
+
import { CloseDiagnostics, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, closeCode, closeErrorCodes, closeFailure } from "./close-diagnostics.js";
|
|
2
3
|
import { runCatalog } from "./catalog-ssh.js";
|
|
3
4
|
import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
|
|
4
5
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
@@ -254,10 +255,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
254
255
|
}
|
|
255
256
|
catch (error) {
|
|
256
257
|
if (error instanceof ModelConfigurationError)
|
|
257
|
-
throw new LlmError(error.message, error.code, { cause: error });
|
|
258
|
+
throw new LlmError(error.message + (failureEvidenceSummary(error) ? ';' + failureEvidenceSummary(error) : ''), error.code, { cause: error });
|
|
258
259
|
if (error instanceof NeedError) {
|
|
259
260
|
const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
|
|
260
|
-
const
|
|
261
|
+
const evidence = failureEvidenceSummary(error);
|
|
262
|
+
const message = (connectNeedMessage(error) ?? employeeFailure(error.code, this.agent.loginProvider)) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '') + (evidence ? ';' + evidence : '');
|
|
261
263
|
const login = originalLoginFailure(error);
|
|
262
264
|
let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
|
|
263
265
|
if (login?.technical && this.reportLoginFailure) {
|
|
@@ -271,6 +273,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
271
273
|
}
|
|
272
274
|
throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (diagnostic ? ';' + diagnostic : '')) });
|
|
273
275
|
}
|
|
276
|
+
if (error instanceof LlmError && failureEvidenceSummary(error))
|
|
277
|
+
throw new LlmError(error.failure.message + ';' + failureEvidenceSummary(error), error.code, { cause: error });
|
|
274
278
|
throw error;
|
|
275
279
|
}
|
|
276
280
|
}
|
|
@@ -303,6 +307,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
303
307
|
let catalog;
|
|
304
308
|
let selected;
|
|
305
309
|
let modelConfiguration;
|
|
310
|
+
let turnFailure;
|
|
306
311
|
let employeeNoticeShown = false;
|
|
307
312
|
const information = [];
|
|
308
313
|
const recordInformation = () => { if (runtime?.details)
|
|
@@ -401,8 +406,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
401
406
|
throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
|
|
402
407
|
if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
|
|
403
408
|
throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
|
|
409
|
+
catalog.bindSelection?.(selected.id, options.reasoningEffort);
|
|
404
410
|
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)); };
|
|
411
|
+
diagnostic.record({ layer: 'adapter', initiator, reason: catalog?.signal === source ? catalog.revocation?.()?.reason ?? reason : reason, action: 'abort', changedFields: catalog?.signal === source ? catalog.revocation?.()?.fields : undefined, changes: catalog?.signal === source ? catalog.revocation?.()?.changes ?? [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] : [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] }); }; source.addEventListener('abort', observed, { once: true }); abortOffs.push(() => source.removeEventListener('abort', observed)); };
|
|
406
412
|
observeSignal(catalog.signal, 'signal.model-catalog', 'MODEL_CATALOG_REVOKED');
|
|
407
413
|
observeSignal(signal, 'signal.turn-composite', 'ABORTED');
|
|
408
414
|
let modelSignal = AbortSignal.any([signal, catalog.signal]);
|
|
@@ -421,6 +427,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
421
427
|
request.signal = modelSignal;
|
|
422
428
|
modelSignal.throwIfAborted();
|
|
423
429
|
}
|
|
430
|
+
if (runtime.contextSignal) {
|
|
431
|
+
modelSignal = AbortSignal.any([modelSignal, runtime.contextSignal]);
|
|
432
|
+
options.signal = modelSignal;
|
|
433
|
+
request.signal = modelSignal;
|
|
434
|
+
modelSignal.throwIfAborted();
|
|
435
|
+
}
|
|
424
436
|
catalog.assertCurrent();
|
|
425
437
|
if (runtime.connection && (runtime.connection.instanceId !== catalog.target.instanceId || runtime.connection.epoch !== catalog.target.epoch) || runtime.identity !== `${catalog.target.target.username}@${catalog.target.target.host}:${catalog.target.target.port}`)
|
|
426
438
|
throw new ModelConfigurationError('模型目录与当前会话可信员工主机不一致');
|
|
@@ -568,7 +580,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
568
580
|
const resolved = mapping[0].resolvedModel;
|
|
569
581
|
if (typeof resolved !== 'string' || !resolved.trim())
|
|
570
582
|
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '主机目录没有提供所选模型的具体型号,本轮未发送', 'CATALOG_RESOLVED_MODEL_MISSING');
|
|
571
|
-
modelConfiguration = { selected: { ...
|
|
583
|
+
modelConfiguration = { selected: { ...mapping[0], ...(options.reasoningEffort === undefined ? {} : { effort: options.reasoningEffort }) }, configured: actual, resolvedModel: resolved, status: 'remote-configured', ...(this.agent.loginProvider === 'claude' ? { resolvedModelSource: 'host-user-catalog', sessionModelReported: false } : {}) };
|
|
572
584
|
details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
|
|
573
585
|
}
|
|
574
586
|
catch (error) {
|
|
@@ -652,41 +664,44 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
652
664
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
653
665
|
}
|
|
654
666
|
catch (error) {
|
|
667
|
+
// ACP cancellation must retain the source revocation that caused it.
|
|
668
|
+
// User cancellation and unrelated signals keep their existing semantics.
|
|
669
|
+
const sourceReason = runtime?.contextSignal?.aborted ? runtime.contextSignal.reason : undefined;
|
|
670
|
+
if (error instanceof NeedError && error.code === 'ABORTED' && sourceReason instanceof NeedError) {
|
|
671
|
+
error = Object.assign(new NeedError(sourceReason.code, sourceReason.message, sourceReason.requires), { ...(sourceReason.connectNeed ? { connectNeed: sourceReason.connectNeed } : {}), ...(sourceReason.officialReason ? { officialReason: sourceReason.officialReason } : {}), cause: new AggregateError([sourceReason, error], 'HOST_SOURCE_REVOKED') });
|
|
672
|
+
}
|
|
673
|
+
turnFailure = error;
|
|
655
674
|
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
656
675
|
recordInformation();
|
|
657
676
|
failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
|
|
658
677
|
if (live && session && session.promptDispatchCount > promptBaseline)
|
|
659
678
|
live.failure = { cause: error };
|
|
660
|
-
|
|
679
|
+
const reporting = [];
|
|
661
680
|
if (request?.recordEarlyFailure) {
|
|
662
681
|
const record = request.recordEarlyFailure;
|
|
663
682
|
request.recordEarlyFailure = undefined;
|
|
664
|
-
const code = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : signal.aborted ? 'ABORTED' : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
|
|
665
683
|
try {
|
|
666
|
-
await record(Object.assign(new NeedError(
|
|
684
|
+
await record(Object.assign(new NeedError(failureCode ?? 'TURN_PREPARATION_FAILED', '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
|
|
667
685
|
}
|
|
668
686
|
catch (failure) {
|
|
669
|
-
|
|
687
|
+
reporting.push(failure);
|
|
670
688
|
}
|
|
671
689
|
}
|
|
672
690
|
try {
|
|
673
691
|
await runtime?.details?.flush();
|
|
674
692
|
}
|
|
675
693
|
catch (failure) {
|
|
676
|
-
|
|
677
|
-
reportingFailure = { cause: failure };
|
|
678
|
-
else {
|
|
679
|
-
const first = reportingFailure.cause;
|
|
680
|
-
const originalCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
|
|
681
|
-
const flushCode = failure instanceof NeedError || failure instanceof ModelConfigurationError ? failure.code : failure instanceof LlmError ? failure.failure.code : 'CONVERSATION_DETAILS_FAILED';
|
|
682
|
-
reportingFailure = { cause: Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '本轮诊断保存及详情收尾失败,已停止。', '原错误代码 ' + originalCode + ';' + (first instanceof NeedError ? first.requires : '早期诊断保存失败') + ';详情收尾代码 ' + flushCode), { cause: new AggregateError([error, first, failure], 'ADMISSION_DIAGNOSTIC_AND_FLUSH_FAILED') }) };
|
|
683
|
-
}
|
|
694
|
+
reporting.push(failure);
|
|
684
695
|
}
|
|
685
696
|
if (activeMapper)
|
|
686
697
|
yield* activeMapper.flush();
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
698
|
+
const firstReason = request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason;
|
|
699
|
+
if (reporting.length) {
|
|
700
|
+
turnFailure = withFailureEvidence(error, reporting, 'diagnostic', firstReason, failureCode);
|
|
701
|
+
throw turnFailure;
|
|
702
|
+
}
|
|
703
|
+
turnFailure = firstReason && !(error instanceof NeedError || error instanceof ModelConfigurationError || error instanceof LlmError) ? withFailureEvidence(error, [], 'diagnostic', firstReason, failureCode) : withFirstFailureEvidence(error, firstReason, failureCode);
|
|
704
|
+
throw turnFailure;
|
|
690
705
|
}
|
|
691
706
|
finally {
|
|
692
707
|
for (const off of abortOffs)
|
|
@@ -716,6 +731,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
716
731
|
if (succeeded && key)
|
|
717
732
|
this.preparationFailures.delete(key);
|
|
718
733
|
}
|
|
734
|
+
catch (cleanup) {
|
|
735
|
+
throw withFailureEvidence(turnFailure ?? cleanup, [cleanup], 'cleanup', request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason, failureCode);
|
|
736
|
+
}
|
|
719
737
|
finally {
|
|
720
738
|
if (key && this.turns.get(key) === turn)
|
|
721
739
|
this.turns.delete(key);
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -20,7 +20,14 @@ export interface HostModel {
|
|
|
20
20
|
};
|
|
21
21
|
source: unknown;
|
|
22
22
|
}
|
|
23
|
+
export interface CatalogRevocation {
|
|
24
|
+
readonly fields: readonly CatalogChangeField[];
|
|
25
|
+
readonly changes: readonly CloseChange[];
|
|
26
|
+
readonly reason?: 'MODEL_CATALOG_REVOKED' | 'MODEL_CATALOG_SCOPE_CHANGED';
|
|
27
|
+
}
|
|
23
28
|
export interface CatalogLease {
|
|
29
|
+
revocation?(): CatalogRevocation | undefined;
|
|
30
|
+
bindSelection?(id: string, effort?: string): void;
|
|
24
31
|
readonly target: ModelCatalogTarget;
|
|
25
32
|
readonly models: readonly HostModel[];
|
|
26
33
|
readonly signal: AbortSignal;
|
|
@@ -45,7 +52,7 @@ export declare function catalogDiagnostic(issues: readonly CatalogIssue[]): stri
|
|
|
45
52
|
/** Defaults are provider declarations, never catalog order or display names. */
|
|
46
53
|
export declare function declaredDefault(agent: AgentDefinition, models: readonly HostModel[]): HostModel | undefined;
|
|
47
54
|
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';
|
|
55
|
+
export type CatalogChangeField = 'api-instance' | 'connection-instance' | 'connection-epoch' | 'connection-phase' | 'connection-need-code' | 'catalog-capability' | 'codex-target' | 'claude-target' | 'other-metadata' | 'selected-model';
|
|
49
56
|
/** Share only pending work; settled results are never cached. */
|
|
50
57
|
export declare class ModelCatalog implements ModelDirectory {
|
|
51
58
|
private get;
|
|
@@ -62,10 +69,12 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
62
69
|
private disposed;
|
|
63
70
|
private initialized;
|
|
64
71
|
private active;
|
|
72
|
+
private revocations;
|
|
65
73
|
private presentation?;
|
|
66
74
|
private presentationNeedCode?;
|
|
67
75
|
private information;
|
|
68
76
|
private flights;
|
|
77
|
+
private selections;
|
|
69
78
|
private serial;
|
|
70
79
|
private latest;
|
|
71
80
|
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);
|
|
@@ -73,6 +82,7 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
73
82
|
private trusted;
|
|
74
83
|
private assertFlight;
|
|
75
84
|
private failure;
|
|
85
|
+
private checkSelections;
|
|
76
86
|
private start;
|
|
77
87
|
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
78
88
|
dispose(): void;
|
package/dist/model-catalog.js
CHANGED
|
@@ -93,6 +93,7 @@ export function parseHostCatalog(input, provider, username) {
|
|
|
93
93
|
}
|
|
94
94
|
return freeze(structuredClone({ models, issues }));
|
|
95
95
|
}
|
|
96
|
+
function semanticJson(value) { const sort = (item) => Array.isArray(item) ? item.map(sort) : object(item) ? Object.fromEntries(Object.keys(item).sort().map(key => [key, sort(item[key])])) : item; return JSON.stringify(sort(value)); }
|
|
96
97
|
function changedCatalogFields(previous, next, switched) {
|
|
97
98
|
const before = previous ? JSON.parse(previous) : {}, after = JSON.parse(next), fields = [];
|
|
98
99
|
if (switched)
|
|
@@ -148,10 +149,12 @@ export class ModelCatalog {
|
|
|
148
149
|
disposed = false;
|
|
149
150
|
initialized = false;
|
|
150
151
|
active = new Set();
|
|
152
|
+
revocations = new WeakMap();
|
|
151
153
|
presentation;
|
|
152
154
|
presentationNeedCode;
|
|
153
155
|
information = new Set();
|
|
154
156
|
flights = new Map();
|
|
157
|
+
selections = new Set();
|
|
155
158
|
serial = 0;
|
|
156
159
|
latest = new Map();
|
|
157
160
|
constructor(get, maxBytes, run = runCatalog, changed = () => { }, diagnostic = () => { }, loaded = () => { }, revoking = () => { }) {
|
|
@@ -172,7 +175,7 @@ export class ModelCatalog {
|
|
|
172
175
|
// L's same-epoch recovery wording is presentation metadata, not an execution change.
|
|
173
176
|
const executionState = state && { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase };
|
|
174
177
|
const executionTargets = targets?.map(result => result.ok ? result : { ok: false, code: result.error.code });
|
|
175
|
-
const signature =
|
|
178
|
+
const signature = semanticJson({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets });
|
|
176
179
|
const switched = api !== this.api;
|
|
177
180
|
const changed = this.initialized && (switched || signature !== this.signature);
|
|
178
181
|
const presentation = createHash('sha256').update(JSON.stringify({ need: { code: state?.need?.code, message: state?.need?.message, requires: state?.need?.requires }, recovery: state?.recovery && { phase: state.recovery.phase, message: state.recovery.message, startedAt: state.recovery.startedAt, administratorAfterMs: state.recovery.administratorAfterMs } })).digest('hex');
|
|
@@ -202,8 +205,10 @@ export class ModelCatalog {
|
|
|
202
205
|
this.revoking(changedFields, changes);
|
|
203
206
|
this.revision++;
|
|
204
207
|
this.latest.clear();
|
|
205
|
-
for (const controller of this.active)
|
|
208
|
+
for (const controller of this.active) {
|
|
209
|
+
this.revocations.set(controller, freeze({ fields: changedFields, changes, reason: 'MODEL_CATALOG_SCOPE_CHANGED' }));
|
|
206
210
|
controller.abort();
|
|
211
|
+
}
|
|
207
212
|
this.active.clear();
|
|
208
213
|
this.diagnostic('claude', undefined);
|
|
209
214
|
this.diagnostic('codex', undefined);
|
|
@@ -242,6 +247,22 @@ export class ModelCatalog {
|
|
|
242
247
|
// Each consumer needs its own Error identity for its original input/signal proof.
|
|
243
248
|
return new CatalogError(error instanceof CatalogError || error instanceof ModelConfigurationError ? error.message.replace(/^无法获取模型列表:/, '') : '目录查询失败,请到设置 → 模型查看诊断。', error instanceof CatalogError ? error.diagnostic : error instanceof CatalogSshError ? error.diagnostic : error instanceof Error ? `${error instanceof LlmError ? error.code : 'MODEL_CATALOG_QUERY_FAILED'}:${safe(error.message)}` : 'MODEL_CATALOG_QUERY_FAILED:目录查询失败,根因未查明');
|
|
244
249
|
}
|
|
250
|
+
checkSelections(agent, api, target, models, serial) {
|
|
251
|
+
for (const lease of this.selections) {
|
|
252
|
+
const chosen = lease.selected, old = lease.flight.target;
|
|
253
|
+
if (lease.controller.signal.aborted || lease.flight.serial > serial || (lease.observed?.serial ?? 0) > serial || lease.flight.api !== api || old.provider !== agent.loginProvider || old.instanceId !== target.instanceId || old.epoch !== target.epoch || !sameTarget(old.target, target.target))
|
|
254
|
+
continue;
|
|
255
|
+
lease.observed = { serial, models };
|
|
256
|
+
if (!chosen)
|
|
257
|
+
continue;
|
|
258
|
+
const current = models.find(model => model.id === chosen.id && model.value === chosen.value);
|
|
259
|
+
if (current)
|
|
260
|
+
continue;
|
|
261
|
+
const changes = [{ field: 'catalog.' + agent.loginProvider + '.selected-model-valid', before: true, after: false }];
|
|
262
|
+
this.revocations.set(lease.controller, freeze({ fields: ['selected-model'], changes, reason: 'MODEL_CATALOG_REVOKED' }));
|
|
263
|
+
lease.controller.abort();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
245
266
|
start(agent, api, target, max, key) {
|
|
246
267
|
const deferred = Promise.withResolvers(), controller = new AbortController();
|
|
247
268
|
const flight = { key, api, target, revision: this.revision, serial: ++this.serial, controller, pending: true, users: 0, promise: deferred.promise };
|
|
@@ -255,6 +276,7 @@ export class ModelCatalog {
|
|
|
255
276
|
const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, controller.signal, max);
|
|
256
277
|
this.assertFlight(flight);
|
|
257
278
|
const parsed = parseHostCatalog(output, agent.loginProvider, target.target.username), diagnostic = catalogDiagnostic(parsed.issues);
|
|
279
|
+
this.checkSelections(agent, api, target, parsed.models, flight.serial);
|
|
258
280
|
if (!parsed.models.length)
|
|
259
281
|
fail('当前没有可选模型,请到设置 → 模型查看诊断。', diagnostic ?? '主机返回的完整模型目录为空(诊断代码:MODEL_CATALOG_EMPTY)');
|
|
260
282
|
if (current())
|
|
@@ -278,10 +300,12 @@ export class ModelCatalog {
|
|
|
278
300
|
return flight;
|
|
279
301
|
}
|
|
280
302
|
async read(agent, signal) {
|
|
281
|
-
let flight, released = false;
|
|
303
|
+
let flight, released = false, selection;
|
|
304
|
+
const selectedController = new AbortController();
|
|
282
305
|
const listeners = new Set();
|
|
283
306
|
const release = () => { if (released || !flight)
|
|
284
|
-
return; released = true;
|
|
307
|
+
return; released = true; if (selection)
|
|
308
|
+
this.selections.delete(selection); for (const listener of listeners)
|
|
285
309
|
this.information.delete(listener); listeners.clear(); flight.users--; if (!flight.users) {
|
|
286
310
|
if (flight.pending)
|
|
287
311
|
flight.controller.abort();
|
|
@@ -323,7 +347,7 @@ export class ModelCatalog {
|
|
|
323
347
|
}
|
|
324
348
|
flight ??= this.start(agent, api, target, max, key);
|
|
325
349
|
flight.users++;
|
|
326
|
-
const own = flight, combined =
|
|
350
|
+
const own = flight, combined = AbortSignal.any([own.controller.signal, selectedController.signal, ...(signal ? [signal] : [])]);
|
|
327
351
|
const assertCurrent = () => { if (combined.aborted)
|
|
328
352
|
fail('目录查询已取消'); this.assertFlight(own); };
|
|
329
353
|
const models = await new Promise((resolve, reject) => {
|
|
@@ -334,7 +358,11 @@ export class ModelCatalog {
|
|
|
334
358
|
abort();
|
|
335
359
|
});
|
|
336
360
|
assertCurrent();
|
|
337
|
-
|
|
361
|
+
selection = { flight: own, controller: selectedController };
|
|
362
|
+
this.selections.add(selection);
|
|
363
|
+
return { target: own.target, get models() { return selection?.observed?.models ?? models; }, signal: combined, assertCurrent, release, bindSelection: (id, effort) => { assertCurrent(); if (released)
|
|
364
|
+
fail('模型目录租约已经释放'); const observed = selection.observed?.models ?? models, model = observed.find(model => model.id === id); if (!model || effort !== undefined && !model.reasoning.options.some(option => option.value === effort))
|
|
365
|
+
fail('所选型号或推理等级已不在当前主机目录中,请重新选择'); selection.selected = { id: model.id, value: model.value }; }, revocation: () => this.revocations.get(selectedController) ?? this.revocations.get(own.controller), onInformation: listener => { if (released)
|
|
338
366
|
return () => { }; listeners.add(listener); this.information.add(listener); return () => { listeners.delete(listener); this.information.delete(listener); }; } };
|
|
339
367
|
}
|
|
340
368
|
catch (error) {
|
|
@@ -346,7 +374,7 @@ export class ModelCatalog {
|
|
|
346
374
|
}
|
|
347
375
|
}
|
|
348
376
|
dispose() { this.disposed = true; this.unsubscribe?.(); this.flights.clear(); for (const controller of this.active)
|
|
349
|
-
controller.abort(); this.active.clear(); this.information.clear(); }
|
|
377
|
+
controller.abort(); this.active.clear(); this.selections.clear(); this.information.clear(); }
|
|
350
378
|
}
|
|
351
379
|
export function modelInfo(provider, model) {
|
|
352
380
|
// Official rc.2 represents no reasoning levels by absence, and rejects an empty efforts array.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withFailureEvidence, failureEvidenceSummary, primaryFailureCode } from "./close-diagnostics.js";
|
|
1
2
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
3
|
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
3
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
@@ -91,12 +92,12 @@ export function createPreflightDiagnostics(options) {
|
|
|
91
92
|
if (request.preflightFailureRecorded)
|
|
92
93
|
return;
|
|
93
94
|
request.preflightFailureRecorded = true;
|
|
94
|
-
const scope = scopeFor(request), code = preflightCode(error);
|
|
95
|
+
const scope = scopeFor(request), code = primaryFailureCode(error, preflightCode(error));
|
|
95
96
|
try {
|
|
96
|
-
await record(request, provider, { kind: 'need', severity: 'action-required', code, summary: '本轮在连接前准备阶段停止;原始请求保留,未重发。', payload: { phase: 'workspace-source-admission', stage, ...scope.source, outcome: code === 'ABORTED' ? 'cancelled' : 'failed' } });
|
|
97
|
+
await record(request, provider, { kind: 'need', severity: 'action-required', code, summary: '本轮在连接前准备阶段停止;原始请求保留,未重发。' + failureEvidenceSummary(error), payload: { phase: 'workspace-source-admission', stage, ...scope.source, outcome: code === 'ABORTED' ? 'cancelled' : 'failed' } });
|
|
97
98
|
}
|
|
98
99
|
catch (diagnostic) {
|
|
99
|
-
throw
|
|
100
|
+
throw withFailureEvidence(error, [diagnostic], 'diagnostic', undefined, preflightCode(error));
|
|
100
101
|
}
|
|
101
102
|
},
|
|
102
103
|
forPreparation(sessionId, context) {
|
package/dist/presentation.js
CHANGED
|
@@ -9,11 +9,15 @@ export function employeeFailure(code, brand = 'codex') {
|
|
|
9
9
|
WORKSPACE_SOURCE_CONFIRMATION_UNAVAILABLE: '我需要打开工作区来源确认,目前无法显示确认动作;本轮未执行,你的原话仍保留。',
|
|
10
10
|
WORKSPACE_SOURCE_SELECTION_UNCONFIRMED: '无法核对工作区来源的确认结果,本轮已停止;你的原话仍保留。',
|
|
11
11
|
WORKSPACE_SOURCE_CONFIRMATION_FAILED: '工作区来源确认未完成,本轮已停止;你的原话仍保留。',
|
|
12
|
+
PROJECT_BINDING_INVALID: '文件来源已改,请新建会话继续;原会话历史仍可查看。',
|
|
12
13
|
WORKSPACE_LOCATION_REQUIRED: '我需要你确认这个工作区使用哪一个文件夹,本轮未执行;你的原话仍保留。',
|
|
13
14
|
WORKSPACE_LOCATION_CONFLICT: '工作区来源选择已更新,本次确认不能覆盖新的选择;本轮未执行,你的原话仍保留。',
|
|
14
15
|
LOCAL_USER_TURN_CHANGED: '本次输入已变化,本轮已停止,请确认对话后重新发送。',
|
|
15
16
|
LOCAL_USER_TURN_UNCONFIRMED: '无法核对本次员工输入,本轮已停止;请在当前会话重新发送。',
|
|
16
17
|
LOCAL_USER_TURN_ACCEPTANCE_INVALID: '本次输入的核验回执不一致,本轮已停止,请联系管理员。',
|
|
18
|
+
MODEL_CATALOG_REVOKED: '所选型号已不在当前主机目录中,本轮已停止;请在模型菜单核对选择。',
|
|
19
|
+
MODEL_CATALOG_SCOPE_CHANGED: '远程主机连接或可信目标已改变,本轮已停止;原输入没有自动重发。',
|
|
20
|
+
CONVERSATION_DETAILS_FAILED: '本轮已停止,技术详情保存未确认;下方分别列出运行与保存原因。',
|
|
17
21
|
TECHNICAL_DETAILS_UNAVAILABLE: '技术详情暂不可用,请更新远程主机连接组件。',
|
|
18
22
|
REMOTE_DETAIL_STORAGE_FAILED: '技术详情保存失败,本轮已停止,请联系管理员检查。',
|
|
19
23
|
REMOTE_DETAIL_DELIVERY_UNKNOWN: '技术详情的持久结果无法核对,本轮已停止。',
|
package/dist/provider.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface EmployeeExecution {
|
|
|
29
29
|
export interface Runtime {
|
|
30
30
|
identity: string;
|
|
31
31
|
cwd: string;
|
|
32
|
+
contextSignal?: AbortSignal;
|
|
32
33
|
execution?: EmployeeExecution;
|
|
33
34
|
acpTransport?: {
|
|
34
35
|
writable: WritableStream<Uint8Array>;
|
|
@@ -141,7 +142,7 @@ type Result<T> = {
|
|
|
141
142
|
code: string;
|
|
142
143
|
message: string;
|
|
143
144
|
requires: string;
|
|
144
|
-
} & Pick<ConnectNeed, 'officialReason'
|
|
145
|
+
} & Partial<Pick<ConnectNeed, 'officialReason' | 'layer' | 'actor' | 'action' | 'lastSeen'>>;
|
|
145
146
|
};
|
|
146
147
|
/** 消费正式connect-api 0.7公开子集;X不响应L登录票据、不直接调用登录。 */
|
|
147
148
|
export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'getWorkspaceBindingState' | 'getModelCatalogTarget' | 'onConnectionState' | 'onLoginPromptState' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'getWorkspaceLocationState' | 'getLocalWorkspaceContext' | 'onWorkspaceLocationState' | 'acceptLocalWorkspaceUserTurn' | 'selectWorkspaceLocation'>> {
|
package/dist/provider.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hostWorkspaceGuard } from "./host-workspace.js";
|
|
2
|
+
import { connectNeedError } from "./connect-need.js";
|
|
1
3
|
import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
|
|
2
4
|
import { NeedError } from "./remote.js";
|
|
3
5
|
import { CONNECTION_REQUIRED, TEST_HOST_NOTICE } from "./presentation.js";
|
|
@@ -38,8 +40,11 @@ function verifiedLogin(api, brand, expected) {
|
|
|
38
40
|
throw new NeedError('CONNECTION_LOST', 'connection binding changed', 'current employee connection');
|
|
39
41
|
requireLoginState(login);
|
|
40
42
|
const reason = loginReason(login, connection, brand);
|
|
41
|
-
if (reason)
|
|
42
|
-
|
|
43
|
+
if (reason) {
|
|
44
|
+
const original = (reason === 'LOGIN_PROVIDER_NEED' || reason === 'LOGIN_PHASE_BLOCKED') && login.instanceId === connection.instanceId ? login.providerNeeds[brand] : undefined;
|
|
45
|
+
const error = original ? connectNeedError(original) : new NeedError(reason === 'LOGIN_REQUIRED' ? 'LOGIN_REQUIRED' : 'AUTH_STATUS_INVALID', reason === 'LOGIN_REQUIRED' ? '需要登录,请到设置 → 远程主机连接 点登录' : '无法确认当前品牌的登录状态', 'L核验的本品牌登录状态');
|
|
46
|
+
throw Object.assign(error, { technical: loginDiagnostic('verified-login', reason, brand, connection, login), ...(typeof login.providerNeeds[brand]?.officialReason === 'string' ? { officialReason: login.providerNeeds[brand].officialReason } : {}) });
|
|
47
|
+
}
|
|
43
48
|
return { ...connection };
|
|
44
49
|
}
|
|
45
50
|
/** Wait only for L's explicit in-progress evidence. No timer, polling, refresh or authorization. */
|
|
@@ -102,12 +107,12 @@ async function waitForLogin(api, brand, expected, get, signal) {
|
|
|
102
107
|
async function ensureReady(api, brand, signal) {
|
|
103
108
|
const result = await awaitL(() => api.ensureReady(brand), signal);
|
|
104
109
|
if (!result.ok)
|
|
105
|
-
throw Object.assign(
|
|
110
|
+
throw Object.assign(connectNeedError(result.error), { technical: loginDiagnostic('ensure-ready', 'L_ENSURE_READY_FAILED', brand, api.getConnectionState(), api.getLoginPromptState(), result.error.officialReason), ...(typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {}) });
|
|
106
111
|
return result.value;
|
|
107
112
|
}
|
|
108
113
|
export function unwrap(result) {
|
|
109
114
|
if (!result.ok)
|
|
110
|
-
throw Object.assign(
|
|
115
|
+
throw Object.assign(connectNeedError(result.error), typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {});
|
|
111
116
|
return result.value;
|
|
112
117
|
}
|
|
113
118
|
export function validateTarget(t) {
|
|
@@ -155,6 +160,30 @@ export function sameTarget(a, b) {
|
|
|
155
160
|
}
|
|
156
161
|
export function connectProvider(get, options = {}) {
|
|
157
162
|
const agent = options.agent ?? CODEX_AGENT;
|
|
163
|
+
const hostScopes = new WeakMap();
|
|
164
|
+
const hostMethods = new WeakMap();
|
|
165
|
+
const retainHostScope = (runtime, scope) => {
|
|
166
|
+
hostScopes.get(runtime)?.dispose();
|
|
167
|
+
hostScopes.set(runtime, scope);
|
|
168
|
+
runtime.contextSignal = scope.revocation;
|
|
169
|
+
if (hostMethods.has(runtime))
|
|
170
|
+
return;
|
|
171
|
+
const original = { dispose: runtime.dispose, detach: runtime.detach };
|
|
172
|
+
hostMethods.set(runtime, original);
|
|
173
|
+
const release = () => { hostScopes.get(runtime)?.dispose(); hostScopes.delete(runtime); };
|
|
174
|
+
runtime.dispose = async (cause) => { try {
|
|
175
|
+
await original.dispose?.(cause);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
release();
|
|
179
|
+
} };
|
|
180
|
+
runtime.detach = async (cause) => { try {
|
|
181
|
+
await original.detach?.(cause);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
release();
|
|
185
|
+
} };
|
|
186
|
+
};
|
|
158
187
|
// Keep the legacy source SDK wrapper's strip-only import graph free of broker runtime syntax.
|
|
159
188
|
// Cache the initialization promise so concurrent sessions share this provider's binding lifetimes.
|
|
160
189
|
let employee;
|
|
@@ -203,6 +232,9 @@ export function connectProvider(get, options = {}) {
|
|
|
203
232
|
request.localAdmissionCheck?.();
|
|
204
233
|
admissions.set(request, { api, target, connection });
|
|
205
234
|
sourceStage = 'local-admission';
|
|
235
|
+
const locations = api.getContractInfo().workspaceLocations === 1 ? api.getWorkspaceLocationState?.() : undefined;
|
|
236
|
+
if (locations && (locations.phase === 'failed' || locations.phase === 'disposed') && locations.instanceId === connection.instanceId && locations.epoch === connection.epoch && locations.need)
|
|
237
|
+
throw connectNeedError(locations.need);
|
|
206
238
|
await options.local?.admit?.(request, api, connection, target);
|
|
207
239
|
confirmedSource?.assertCurrent();
|
|
208
240
|
}
|
|
@@ -228,88 +260,109 @@ export function connectProvider(get, options = {}) {
|
|
|
228
260
|
request.localAdmissionCheck?.();
|
|
229
261
|
if (!request.sessionId)
|
|
230
262
|
throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
local
|
|
236
|
-
|
|
263
|
+
const host = await hostWorkspaceGuard(get, api, connection, target, request, options.workspaceSource?.workspace, agent.loginProvider);
|
|
264
|
+
try {
|
|
265
|
+
if (!host && options.employee) {
|
|
266
|
+
employee ??= import("./employee-provider.js").then(module => module.createEmployeeProvider(get, options, agent));
|
|
267
|
+
const local = await (await employee)({ api, target, connection, request, stable: signal => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, signal)) });
|
|
268
|
+
if (local) {
|
|
269
|
+
local.loginRequired = () => offerLogin(api, request, get, agent);
|
|
270
|
+
return local;
|
|
271
|
+
}
|
|
237
272
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
273
|
+
if (!host && agent.loginProvider === 'claude' && options.local) {
|
|
274
|
+
const runtime = await options.local(request, api, connection, target);
|
|
275
|
+
if (runtime) {
|
|
276
|
+
try {
|
|
277
|
+
await stable();
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
await runtime.dispose?.();
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
return runtime;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!host) {
|
|
287
|
+
await options.notice?.reconcile(request, api);
|
|
288
|
+
await stable();
|
|
289
|
+
}
|
|
290
|
+
const resolved = await request.timing.run('workspace-resolve', async () => host ? host.resolve() : unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal)));
|
|
291
|
+
await stable();
|
|
292
|
+
host?.assertCurrent();
|
|
293
|
+
if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
|
|
294
|
+
throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
|
|
295
|
+
validateTarget(resolved.target);
|
|
296
|
+
const project = resolved.project;
|
|
297
|
+
const source = resolved.source;
|
|
298
|
+
if (resolved.sessionId !== request.sessionId || !resolved.workspaceId || !resolved.bindingId || !Number.isSafeInteger(resolved.bindingRevision) || resolved.bindingRevision < 1 || !sameTarget(target, resolved.target) || !project || (!host && (source !== undefined && source !== 'legacy-host' || typeof project.name !== 'string' || !project.name || project.name === '.' || project.name === '..' || /[\\/\x00-\x1f\x7f]/.test(project.name) || project.path !== resolved.target.projectRoot + '/' + project.name)))
|
|
299
|
+
throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
|
|
300
|
+
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
301
|
+
throw new NeedError(host ? 'PROJECT_BINDING_INVALID' : 'SESSION_CONTEXT_MISMATCH', host ? '文件来源已改,请新建会话继续;原会话历史仍可查看。' : '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
302
|
+
const details = await request.timing.run('details-open', async () => options.details?.(request, api, connection, target));
|
|
303
|
+
try {
|
|
304
|
+
if (details)
|
|
305
|
+
request.timing.bind(detail => details.record(detail));
|
|
306
|
+
await stable();
|
|
307
|
+
host?.assertCurrent();
|
|
308
|
+
let workspaceNotice = undefined;
|
|
309
|
+
const notice = !host && 'notice' in resolved ? resolved.notice : undefined;
|
|
310
|
+
if (notice) {
|
|
311
|
+
if (!options.notice)
|
|
312
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
|
|
313
|
+
workspaceNotice = await request.timing.run('workspace-notice', () => options.notice.deliver(notice, resolved, request, api));
|
|
314
|
+
await stable();
|
|
315
|
+
}
|
|
316
|
+
if (!options.open)
|
|
317
|
+
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
318
|
+
const observedBinding = host ? undefined : api.getWorkspaceBindingState?.();
|
|
319
|
+
const validateContext = async (ignoreCancellation = false) => {
|
|
320
|
+
const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
|
|
321
|
+
await check();
|
|
322
|
+
if (host && !ignoreCancellation)
|
|
323
|
+
await host.resolve();
|
|
324
|
+
if (observedBinding) {
|
|
325
|
+
const current = api.getWorkspaceBindingState();
|
|
326
|
+
if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
|
|
327
|
+
throw new NeedError('SESSION_CONTEXT_MISMATCH', '当前工作区绑定已改变,原会话操作未执行', '原生会话原绑定');
|
|
328
|
+
}
|
|
329
|
+
await check();
|
|
330
|
+
if (!ignoreCancellation)
|
|
331
|
+
host?.assertCurrent();
|
|
332
|
+
};
|
|
333
|
+
host?.assertCurrent();
|
|
334
|
+
const runtime = await options.open(resolved.target, project, { ...request, ...(host ? { signal: host.signal } : {}), agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details, validateContext });
|
|
242
335
|
try {
|
|
243
336
|
await stable();
|
|
337
|
+
host?.observeRuntime(runtime);
|
|
244
338
|
}
|
|
245
339
|
catch (error) {
|
|
246
340
|
await runtime.dispose?.();
|
|
247
341
|
throw error;
|
|
248
342
|
}
|
|
343
|
+
if (host)
|
|
344
|
+
retainHostScope(runtime, host);
|
|
345
|
+
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
346
|
+
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
347
|
+
runtime.details = details ?? runtime.details;
|
|
348
|
+
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
349
|
+
runtime.connection = { ...connection };
|
|
350
|
+
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
249
351
|
return runtime;
|
|
250
352
|
}
|
|
251
|
-
}
|
|
252
|
-
await options.notice?.reconcile(request, api);
|
|
253
|
-
await stable();
|
|
254
|
-
const resolved = await request.timing.run('workspace-resolve', async () => unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal)));
|
|
255
|
-
await stable();
|
|
256
|
-
if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
|
|
257
|
-
throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
|
|
258
|
-
validateTarget(resolved.target);
|
|
259
|
-
const project = resolved.project;
|
|
260
|
-
if (resolved.sessionId !== request.sessionId || !resolved.workspaceId || !resolved.bindingId || !Number.isSafeInteger(resolved.bindingRevision) || resolved.bindingRevision < 1 || !sameTarget(target, resolved.target) || !project || typeof project.name !== 'string' || !project.name || project.name === '.' || project.name === '..' || /[\\/\x00-\x1f\x7f]/.test(project.name) || project.path !== resolved.target.projectRoot + '/' + project.name)
|
|
261
|
-
throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
|
|
262
|
-
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
263
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
264
|
-
const details = await request.timing.run('details-open', async () => options.details?.(request, api, connection, target));
|
|
265
|
-
try {
|
|
266
|
-
if (details)
|
|
267
|
-
request.timing.bind(detail => details.record(detail));
|
|
268
|
-
await stable();
|
|
269
|
-
let workspaceNotice = undefined;
|
|
270
|
-
if (resolved.notice) {
|
|
271
|
-
if (!options.notice)
|
|
272
|
-
throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
|
|
273
|
-
workspaceNotice = await request.timing.run('workspace-notice', () => options.notice.deliver(resolved.notice, resolved, request, api));
|
|
274
|
-
await stable();
|
|
275
|
-
}
|
|
276
|
-
if (!options.open)
|
|
277
|
-
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
278
|
-
const observedBinding = api.getWorkspaceBindingState?.();
|
|
279
|
-
const validateContext = async (ignoreCancellation = false) => {
|
|
280
|
-
const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
|
|
281
|
-
await check();
|
|
282
|
-
if (observedBinding) {
|
|
283
|
-
const current = api.getWorkspaceBindingState();
|
|
284
|
-
if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
|
|
285
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '当前工作区绑定已改变,原会话操作未执行', '原生会话原绑定');
|
|
286
|
-
}
|
|
287
|
-
await check();
|
|
288
|
-
};
|
|
289
|
-
const runtime = await options.open(resolved.target, project, { ...request, agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details, validateContext });
|
|
290
|
-
try {
|
|
291
|
-
await stable();
|
|
292
|
-
}
|
|
293
353
|
catch (error) {
|
|
294
|
-
|
|
354
|
+
try {
|
|
355
|
+
await details?.flush();
|
|
356
|
+
}
|
|
357
|
+
catch (diagnostic) {
|
|
358
|
+
const code = (value) => value instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'UNKNOWN_FAILURE';
|
|
359
|
+
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能保存;本轮已停止', `原错误代码 ${code(error)};诊断错误代码 ${code(diagnostic)}`);
|
|
360
|
+
}
|
|
295
361
|
throw error;
|
|
296
362
|
}
|
|
297
|
-
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
298
|
-
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
299
|
-
runtime.details = details ?? runtime.details;
|
|
300
|
-
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
301
|
-
runtime.connection = { ...connection };
|
|
302
|
-
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
303
|
-
return runtime;
|
|
304
363
|
}
|
|
305
364
|
catch (error) {
|
|
306
|
-
|
|
307
|
-
await details?.flush();
|
|
308
|
-
}
|
|
309
|
-
catch (diagnostic) {
|
|
310
|
-
const code = (value) => value instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'UNKNOWN_FAILURE';
|
|
311
|
-
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能保存;本轮已停止', `原错误代码 ${code(error)};诊断错误代码 ${code(diagnostic)}`);
|
|
312
|
-
}
|
|
365
|
+
host?.dispose();
|
|
313
366
|
throw error;
|
|
314
367
|
}
|
|
315
368
|
};
|