@ai-workbench/remote-agent 0.8.10 → 0.8.12

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/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { runCatalog } from "./catalog-ssh.js";
2
+ import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
1
3
  import { SessionId } from '@deepseek-ai/dsh-session';
2
4
  import Schema from '@deepseek-ai/schemastery';
3
5
  import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
@@ -24,11 +26,15 @@ import { verifyRecoveryCheckpoint, currentInputIds } from "./recovery-checkpoint
24
26
  import { ModelCatalog, modelInfo } from "./model-catalog.js";
25
27
  import { ModelConfigurationError } from "./model-configuration.js";
26
28
  import { officialEmployeeWorkspace } from "./employee-workspace.js";
29
+ import { nativeWorkspaceSourceConfirmation } from "./workspace-source-confirmation.js";
27
30
  import { resolveEmployeeNative } from "./employee-native.js";
28
31
  import { createLocalUserTurnObserver } from "./local-user-turn.js";
29
32
  import { createClaudeLocalProvider } from "./claude-local.js";
30
33
  import { readLocalUserTurn } from "./local-user-turn.js";
31
34
  import { readEmployeeWorkspace } from "./employee-workspace.js";
35
+ import { createModelDefaults, createDefaultSelectionWriter } from "./model-defaults.js";
36
+ import { createPreflightDiagnostics } from "./preflight-diagnostic.js";
37
+ import { installModelInputRetention, markModelPreparationFailure } from "./model-input-retention.js";
32
38
  export const name = 'ai-workbench-remote-agent';
33
39
  export const settingsNamespace = 'ai-workbench-codex';
34
40
  // L can be installed/enabled after X; keeping X mounted lets it explain the prerequisite.
@@ -65,6 +71,23 @@ function readPointer(options, agent) {
65
71
  throw new NeedError('SESSION_CONTEXT_DIGEST_INVALID', '原Codex会话的初建上下文摘要未知或无效,不能猜测是否相同,本轮未发送', '原会话合法的64位十六进制初建上下文摘要');
66
72
  return pointer;
67
73
  }
74
+ // A reporting failure can wrap the original admission error. Preserve its
75
+ // already-safe login snapshot even when a newer preflight delivery is unknown.
76
+ function originalLoginFailure(error) {
77
+ const pending = [error], seen = new Set();
78
+ while (pending.length) {
79
+ const current = pending.pop();
80
+ if (!current || typeof current !== 'object' || seen.has(current))
81
+ continue;
82
+ seen.add(current);
83
+ if (current instanceof NeedError && current.technical)
84
+ return current;
85
+ if (current instanceof AggregateError)
86
+ pending.push(...current.errors);
87
+ if (current instanceof Error && current.cause !== undefined)
88
+ pending.push(current.cause);
89
+ }
90
+ }
68
91
  export class CodexRemoteAdapter extends LlmAdapter {
69
92
  resolveRuntime;
70
93
  approve;
@@ -73,6 +96,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
73
96
  readImage;
74
97
  reportLoginFailure;
75
98
  directory;
99
+ preflightDiagnostics;
76
100
  config;
77
101
  sessions = new Map();
78
102
  turns = new Map();
@@ -80,7 +104,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
80
104
  shuttingDown = false;
81
105
  detached = new Set();
82
106
  cleanups = new WeakMap();
83
- constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory) {
107
+ constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics) {
84
108
  super();
85
109
  this.resolveRuntime = resolveRuntime;
86
110
  this.approve = approve;
@@ -89,6 +113,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
89
113
  this.readImage = readImage;
90
114
  this.reportLoginFailure = reportLoginFailure;
91
115
  this.directory = directory;
116
+ this.preflightDiagnostics = preflightDiagnostics;
92
117
  this.config = typeof config === 'function' ? config : () => config;
93
118
  }
94
119
  providerInfo(provider) { return { id: provider, name: this.providerName() }; }
@@ -100,14 +125,21 @@ export class CodexRemoteAdapter extends LlmAdapter {
100
125
  finally {
101
126
  catalog.release();
102
127
  } }
103
- async resolveModel(provider, id, signal) { const catalog = await this.catalog(signal); try {
104
- const model = catalog.models.find(model => model.id === id);
105
- if (!model)
106
- throw new LlmError('无法获取模型列表:所选模型已不在当前主机目录中', 'MODEL_CATALOG_UNAVAILABLE');
107
- return modelInfo(provider, model);
128
+ async resolveModel(provider, id, signal) { try {
129
+ const catalog = await this.catalog(signal);
130
+ try {
131
+ const model = catalog.models.find(model => model.id === id);
132
+ if (!model)
133
+ throw new LlmError(id === 'remote' ? '无法获取模型列表:这个会话保存的是旧版模型选择,请打开本会话的模型菜单,重新选择模型后再发送。' : '所选模型已不在当前主机目录中,请打开本会话的模型菜单重新选择模型。', 'MODEL_CATALOG_UNAVAILABLE');
134
+ return modelInfo(provider, model);
135
+ }
136
+ finally {
137
+ catalog.release();
138
+ }
108
139
  }
109
- finally {
110
- catalog.release();
140
+ catch (error) {
141
+ markModelPreparationFailure(provider, error, signal);
142
+ throw error;
111
143
  } }
112
144
  async invalidateModelState() { for (const turn of this.turns.values())
113
145
  turn.abort(); await Promise.all([...this.sessions.keys()].map(id => this.disposeSession(id))); }
@@ -177,10 +209,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
177
209
  if (error instanceof NeedError) {
178
210
  const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
179
211
  const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
180
- let diagnostic = error.technical ? JSON.stringify(error.technical) : '';
181
- if (error.technical && this.reportLoginFailure) {
212
+ const login = originalLoginFailure(error);
213
+ let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
214
+ if (login?.technical && this.reportLoginFailure) {
182
215
  try {
183
- const receipt = await this.reportLoginFailure(options.sessionId, error.code, error.technical);
216
+ const receipt = await this.reportLoginFailure(options.sessionId, login.code, login.technical);
184
217
  diagnostic += ';诊断 eventId=' + receipt.eventId;
185
218
  }
186
219
  catch (failure) {
@@ -206,6 +239,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
206
239
  const signal = AbortSignal.any([this.lifetime.signal, turn.signal, ...(original.signal ? [original.signal] : [])]);
207
240
  const options = { ...original, signal };
208
241
  const timing = new StageTiming();
242
+ let request;
243
+ let earlyStage = 'input-prepare';
209
244
  let runtime;
210
245
  let session;
211
246
  let live;
@@ -218,6 +253,10 @@ export class CodexRemoteAdapter extends LlmAdapter {
218
253
  let modelConfiguration;
219
254
  const config = this.config();
220
255
  const mode = this.agent.oneShot ? config.mode : 'session';
256
+ request = { sessionId: options.sessionId, mode: mode ?? 'session', timing, signal };
257
+ this.preflightDiagnostics?.begin(request, this.agent.loginProvider);
258
+ if (this.preflightDiagnostics)
259
+ request.recordEarlyFailure = (error, stage) => this.preflightDiagnostics.failure(request, this.agent.loginProvider, error, stage);
221
260
  try {
222
261
  if (!mode || (mode === 'session' && (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)))
223
262
  throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的mode/acpMaxMessageBytes');
@@ -236,11 +275,13 @@ export class CodexRemoteAdapter extends LlmAdapter {
236
275
  const configuration = JSON.stringify([mode, config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
237
276
  if (live && live.configuration !== configuration)
238
277
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话运行设置已改变,请新建会话', '原会话运行设置');
239
- const 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 }) };
278
+ 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 }) });
240
279
  const after = options.messages.findLastIndex(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
241
280
  request.userInputIds = Object.freeze(options.messages.slice(after + 1).flatMap(message => message.role === 'user' && message.source?.kind === 'user' && typeof message.id === 'string' ? [message.id] : []));
242
281
  request.userMessageIds = request.userInputIds;
282
+ earlyStage = 'source-context';
243
283
  await this.resolveRuntime.preflight?.(request);
284
+ earlyStage = 'input-prepare';
244
285
  let input = await timing.run('input-prepare', () => prepareInput(options, this.readImage));
245
286
  const contextSha256 = createHash('sha256').update(input.systemPrompt ?? '').digest('hex');
246
287
  request.contextSha256 = contextSha256;
@@ -270,6 +311,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
270
311
  if (mode === 'one-shot')
271
312
  throw new NeedError('MODEL_CONFIGURATION_UNSUPPORTED', '单次模式不支持确认当前模型与推理等级,请改用会话模式', '支持模型配置确认的会话模式');
272
313
  request.localAdmissionCheck?.();
314
+ earlyStage = 'model-catalog';
273
315
  catalog = await this.catalog(signal);
274
316
  selected = catalog.models.find(model => model.id === options.model);
275
317
  if (!selected)
@@ -279,6 +321,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
279
321
  let modelSignal = AbortSignal.any([signal, catalog.signal]);
280
322
  options.signal = modelSignal;
281
323
  request.signal = modelSignal;
324
+ earlyStage = 'runtime-resolve';
282
325
  runtime = await this.resolveRuntime(request);
283
326
  signal.throwIfAborted();
284
327
  if (runtime.execution?.signal) {
@@ -315,6 +358,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
315
358
  if (!details)
316
359
  throw new NeedError('TECHNICAL_DETAILS_UNAVAILABLE', '技术详情暂不可用,请更新远程主机连接组件。', 'L技术详情持久接收能力');
317
360
  timing.bind(detail => details.record(detail));
361
+ request.recordEarlyFailure = undefined;
318
362
  const mapper = new StreamMapper(mode === 'session' && !contextChanged ? (runtime.restartNotice ? undefined : live ? live.usageSnapshot : previous ? (previous.usageSnapshot ?? null) : undefined) : undefined, observation => details.record(observation));
319
363
  activeMapper = mapper;
320
364
  let id;
@@ -505,11 +549,29 @@ export class CodexRemoteAdapter extends LlmAdapter {
505
549
  if (live && (remoteAttempted || runtime === live.runtime))
506
550
  live.failure = { cause: error };
507
551
  let reportingFailure;
552
+ if (request?.recordEarlyFailure) {
553
+ const record = request.recordEarlyFailure;
554
+ request.recordEarlyFailure = undefined;
555
+ const code = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : signal.aborted ? 'ABORTED' : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
556
+ try {
557
+ await record(Object.assign(new NeedError(code, '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
558
+ }
559
+ catch (failure) {
560
+ reportingFailure = { cause: failure };
561
+ }
562
+ }
508
563
  try {
509
564
  await runtime?.details?.flush();
510
565
  }
511
566
  catch (failure) {
512
- reportingFailure = { cause: failure };
567
+ if (reportingFailure === undefined)
568
+ reportingFailure = { cause: failure };
569
+ else {
570
+ const first = reportingFailure.cause;
571
+ const originalCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
572
+ const flushCode = failure instanceof NeedError || failure instanceof ModelConfigurationError ? failure.code : failure instanceof LlmError ? failure.failure.code : 'CONVERSATION_DETAILS_FAILED';
573
+ 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') }) };
574
+ }
513
575
  }
514
576
  if (activeMapper)
515
577
  yield* activeMapper.flush();
@@ -543,8 +605,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
543
605
  /** One official registration owns both routes, preserving opaque replay ownership. */
544
606
  export class RemoteAgentsAdapter extends LlmAdapter {
545
607
  adapters = new Map();
546
- constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory) { super(); for (const agent of AGENTS)
547
- this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory)); }
608
+ constructor(config, makeRuntime, approve, nameFor, readImage, reportLoginFailure, directory, preflightDiagnostics) { super(); for (const agent of AGENTS)
609
+ this.adapters.set(agent.id, new CodexRemoteAdapter(config, makeRuntime(agent), approve, () => nameFor(agent), agent, readImage, reportLoginFailure, directory, preflightDiagnostics)); }
548
610
  adapter(provider) { const adapter = this.adapters.get(provider); if (!adapter)
549
611
  throw new LlmError('远程模型未注册', 'PROVIDER_NOT_FOUND'); return adapter; }
550
612
  providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
@@ -582,8 +644,30 @@ export function apply(ctx, live) {
582
644
  }
583
645
  return manager;
584
646
  };
647
+ const preflightDiagnostics = createPreflightDiagnostics({ ctx, getApi: get, manager: managerForProfile });
585
648
  let changed = () => { };
586
- const modelDirectory = new ModelCatalog(get, () => config().acpMaxMessageBytes, undefined, () => changed());
649
+ const diagnostics = new Map();
650
+ const diagnostic = (scope, provider, message) => {
651
+ let entry = diagnostics.get(provider);
652
+ if (!entry) {
653
+ entry = new Map();
654
+ diagnostics.set(provider, entry);
655
+ }
656
+ if (entry.get(scope) === message)
657
+ return;
658
+ if (message === undefined)
659
+ entry.delete(scope);
660
+ else
661
+ entry.set(scope, message);
662
+ };
663
+ let defaults;
664
+ const catalogDiagnostics = createCatalogDiagnostics(() => { const profile = ctx.get('profileContext'); return profile?.dir; });
665
+ // Public Loader Entry.options.id; optional when mounted without Loader.
666
+ const actualSettingsNamespace = ctx.fiber.entry?.options.id;
667
+ 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
+ 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
+ diagnostic('catalog', AGENTS.find(agent => agent.loginProvider === brand).id, message);
670
+ }, (agent, models, assertCurrent) => defaults?.observe(agent, models, assertCurrent));
587
671
  const adapter = new RemoteAgentsAdapter(config, agent => {
588
672
  const details = async (request, api, connection, target) => {
589
673
  return managerForProfile().open({ sessionId: request.sessionId, provider: agent.loginProvider, identity: `${target.username}@${target.host}:${target.port}`, connection, localAcceptanceId: request.localAcceptanceId ?? request.localAcceptance?.acceptanceId });
@@ -617,16 +701,20 @@ export function apply(ctx, live) {
617
701
  };
618
702
  return runtime;
619
703
  };
620
- const local = agent.loginProvider === 'claude' ? createClaudeLocalProvider({ getConnect: get, getNetwork: () => ctx.get('aiWorkbenchNetwork'), readWorkspace: id => readEmployeeWorkspace(ctx, id), readUserTurn: request => readLocalUserTurn(ctx, { sessionId: request.sessionId, route: agent.id, userMessageIds: request.userMessageIds ?? [] }), reportAdmissionFailure: (request, code) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code }), recordUnscoped: (request, observation) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code: observation.code, observation }), configuration: config, details, open }) : undefined;
621
- return connectProvider(get, { agent, ...(agent.loginProvider === 'codex' ? { employee: {
704
+ const local = agent.loginProvider === 'claude' ? createClaudeLocalProvider({ getConnect: get, getNetwork: () => ctx.get('aiWorkbenchNetwork'), readWorkspace: id => readEmployeeWorkspace(ctx, id), readUserTurn: request => readLocalUserTurn(ctx, { sessionId: request.sessionId, route: agent.id, userMessageIds: request.userMessageIds ?? [] }), reportAdmissionFailure: (request, code) => preflightDiagnostics.failure(request, 'claude', code, 'local-admission'), recordUnscoped: (request, observation) => managerForProfile().recordLocalFailure({ sessionId: request.sessionId, provider: 'claude', code: observation.code, observation }), configuration: config, details, open }) : undefined;
705
+ const workspace = async (request) => {
706
+ const services = ctx;
707
+ const registry = services.get('workspaceRegistry');
708
+ const sessions = services.get('sessions');
709
+ return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
710
+ };
711
+ return connectProvider(get, { agent, workspaceSource: {
712
+ workspace, currentUserTurn: request => localTurns.current(request), confirm: nativeWorkspaceSourceConfirmation(ctx),
713
+ record: (request, observation) => preflightDiagnostics.record(request, agent.loginProvider, observation),
714
+ }, ...(agent.loginProvider === 'codex' ? { employee: {
622
715
  currentUserTurn: request => localTurns.current(request),
623
716
  getNetwork: () => ctx.get('aiWorkbenchNetwork'),
624
- workspace: async (request) => {
625
- const services = ctx;
626
- const registry = services.get('workspaceRegistry');
627
- const sessions = services.get('sessions');
628
- return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
629
- }, native: () => resolveEmployeeNative(),
717
+ workspace, native: () => resolveEmployeeNative(),
630
718
  } } : {}), notice: createWorkspaceNoticeConsumer(ctx, agent), details, open, local });
631
719
  }, dshApproval(ctx), agent => agent.name[language], readImage, (sessionId, code, diagnostic) => {
632
720
  const events = sessionId ? ctx.get('agents')?.get(SessionId(sessionId))?.session.snapshotEvents() : undefined;
@@ -646,13 +734,31 @@ export function apply(ctx, live) {
646
734
  step = null;
647
735
  }
648
736
  return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
649
- }, modelDirectory);
737
+ }, modelDirectory, preflightDiagnostics);
650
738
  const ids = AGENTS.map(agent => agent.id);
651
- const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: settingsNamespace, settingsPath: [] }));
739
+ const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: actualSettingsNamespace ?? settingsNamespace, settingsPath: [] }));
652
740
  const directory = ctx.llm.registerConfigurableProviders(entries());
653
741
  const registration = ctx.llm.registerAdapter(ids, adapter);
654
- changed = () => { registration.replace(ids); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
742
+ installModelInputRetention(ctx, { isOurProvider: provider => ids.includes(provider), diagnosticForAttempt: position => preflightDiagnostics.forPreparation(position.sessionId, position), preflightDiagnostic: (provider, message) => diagnostic('preflight', provider, message), diagnostic: (provider, message) => diagnostic('input', provider, message) });
655
743
  const serviceValue = (name) => ctx.get(name);
744
+ const refreshDefault = () => {
745
+ const service = serviceValue('agentDefaultModel');
746
+ const selected = service?.currentSelection();
747
+ const agent = AGENTS.find(agent => agent.id === selected?.provider);
748
+ if (!defaults || !agent)
749
+ return;
750
+ // Read through the same trusted, fresh catalog. Failures are already recorded there.
751
+ void modelDirectory.read(agent).then(lease => lease.release(), () => { });
752
+ };
753
+ ctx.inject(['agentDefaultModel'], child => {
754
+ const service = child.get('agentDefaultModel');
755
+ const current = createModelDefaults(service, (provider, message) => diagnostic('default', provider, message), createDefaultSelectionWriter(service, () => serviceValue('configEditor')));
756
+ defaults = current;
757
+ refreshDefault();
758
+ child.effect(() => () => { current.dispose(); if (defaults === current)
759
+ defaults = undefined; });
760
+ });
761
+ changed = () => { registration.replace(ids); refreshDefault(); void adapter.invalidateModelState().catch(() => { ctx.logger('remote-agent').warn('原会话清理结果未确认,已阻断继续发送;请联系管理员核对。'); }); };
656
762
  let employeeConnect = serviceValue('aiWorkbenchConnect'), employeeNetwork = serviceValue('aiWorkbenchNetwork');
657
763
  ctx.on('internal/service', service => {
658
764
  if (service === 'aiWorkbenchConnect')
@@ -2,6 +2,7 @@ import { type LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
2
2
  import type { ModelCatalogTarget } from '@ai-workbench/connect/api';
3
3
  import type { AgentDefinition } from './agents.ts';
4
4
  import { type ConnectApi, type ConnectTarget } from './provider.ts';
5
+ export { runCatalog } from './catalog-ssh.ts';
5
6
  export interface HostModel {
6
7
  id: string;
7
8
  value: string;
@@ -28,15 +29,27 @@ export interface ModelDirectory {
28
29
  read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
29
30
  }
30
31
  export type CatalogRunner = (target: ConnectTarget, command: string, signal: AbortSignal, maxBytes: number) => Promise<string>;
31
- export declare function parseHostCatalog(input: string, provider: string, username: string): HostModel[];
32
- /** Only the public L sshArgs function chooses SSH trust and transport arguments. */
33
- export declare const runCatalog: CatalogRunner;
34
- /** No cache: every query binds to L's current object, host-user and generation. */
32
+ export interface CatalogIssue {
33
+ readonly modelName: string;
34
+ readonly reason: string;
35
+ readonly code: string;
36
+ }
37
+ export interface ParsedCatalog {
38
+ readonly models: readonly HostModel[];
39
+ readonly issues: readonly CatalogIssue[];
40
+ }
41
+ export declare function catalogDiagnostic(issues: readonly CatalogIssue[]): string | undefined;
42
+ /** Defaults are provider declarations, never catalog order or display names. */
43
+ export declare function declaredDefault(agent: AgentDefinition, models: readonly HostModel[]): HostModel | undefined;
44
+ export declare function parseHostCatalog(input: string, provider: string, username: string): ParsedCatalog;
45
+ /** Share only pending work; settled results are never cached. */
35
46
  export declare class ModelCatalog implements ModelDirectory {
36
47
  private get;
37
48
  private maxBytes;
38
49
  private run;
39
50
  private changed;
51
+ private diagnostic;
52
+ private loaded;
40
53
  private api?;
41
54
  private unsubscribe?;
42
55
  private signature?;
@@ -44,9 +57,15 @@ export declare class ModelCatalog implements ModelDirectory {
44
57
  private disposed;
45
58
  private initialized;
46
59
  private active;
47
- constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner, changed?: () => void);
60
+ private flights;
61
+ private serial;
62
+ 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>);
48
64
  refresh(): void;
49
65
  private trusted;
66
+ private assertFlight;
67
+ private failure;
68
+ private start;
50
69
  read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
51
70
  dispose(): void;
52
71
  }