@ai-workbench/remote-agent 0.8.14 → 0.8.16

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 CHANGED
@@ -1,10 +1,10 @@
1
- **`@ai-workbench/remote-agent@0.8.14` 发布形态包;消费审定的 B1–B9 校验分类及 X2 首次关闭原因观测,供 leo 真机复验。Claude 型号按本轮 setter 精确回执与同域目录映射确认,并如实注明来源。Codex 上次 `EMPLOYEE_EXEC_CLOSED` 的真机根因仍未查明,本版补充首因留痕,不宣称其已在真机修复。由编排发布后,通过官方插件 → 添加插件安装;X 不执行 publish。**
1
+ **`@ai-workbench/remote-agent@0.8.16` 发布形态包。失败时在官方错误消息保留首次观察关闭、运行原因、诊断保存及清理结果的安全代码;未确认的详情保留 pending,不重发或伪造保存成功。目标对象键顺序与目录展示元数据不撤销当前选择;最新可信目录移除所选型号时明确停止。真实身份/代际/文件夹失配仍停止。0.8.15 leo 原始撤销字段与61秒慢查询根因未查明,本版真实 leo/员工链路未复验。通过官方插件 → 添加插件安装;固定包由编排发布,X 不 publish。**
2
2
 
3
3
  # 远程ACP插件
4
4
 
5
5
  一个插件提供 **Claude(远程主机)** 和 **Codex(远程主机)**,通过员工主机上的官方CLI运行。Codex保留单次模式;Claude固定会话模式。只用官方模型列表、插件管理和配置入口,不提供自制面板、问答或登录按钮。登录和原生工作区绑定唯一归连接插件L。
6
6
 
7
- 当前版本号为0.8.14;包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;本轮按编排放行准备发布形态包,供 leo 真机自验;X 不执行实际发布,由编排统一发布。开发fixture逐字节保留L0.7.19固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的不完整实现会明确报缺项。依赖范围为`~0.7.19`,L由官方插件管理单独安装,不打进X。组件验证不代表真实开通、模型或leo E通过;HanaMesh另验。
7
+ 当前版本号为0.8.16;包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;本轮按编排放行准备发布形态包,供 leo 真机自验;X 不执行实际发布,由编排统一发布。开发fixture逐字节保留L0.7.19固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的不完整实现会明确报缺项。依赖范围为`~0.7.19`,L由官方插件管理单独安装,不打进X。组件验证不代表真实开通、模型或leo E通过;HanaMesh另验。
8
8
 
9
9
  已包含L0.7.18消费增量:来源确认、输入接纳、登录、本机接纳、输入准备、模型目录及运行时解析的早期失败,用正式`preflightDiagnostics:1` / `preflight-check`进入L「连接前技术详情(含登录与工作区确认)」。阶段、真实来源/状态和错误代码可展开;未知来源为null,旧login-check的完整安全原因保留。记录不选择来源、不接纳输入、不自动重发;无原生会话的菜单查询仍在模型设置报告。当前工程和设置组件验证不等于leo GUI验收。
10
10
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  ## 员工路径
14
14
 
15
- 从官方插件管理安装,照常使用默认工作区/会话/模型入口。L0.7.17沿唯一“远程主机连接”设置节记录文件来源;主机来源沿用L既有解析;本地来源接入N0.4.4/S0.4.11员工端点。旧映射可在本轮官方一次确认中选择改用本机文件夹;拒绝、取消或输入/来源变化时不切换、不执行。也可在L设置逐工作区确认沿用原主机关联,新主机位置仍按L现有合同停止为WORKSPACE_LOCATION_PROVIDER_REQUIRED。本地六类工具经Claude公开toolAliases路由到员工电脑:cwd是员工选定目录,Git使用本机仓库,不回退主机同名目录。本地真实员工输入先取得官方当前轮次CAS预期,再调用L0.7.17 acceptLocalWorkspaceUserTurn;核回包后才打开D19和本轮主机目录/执行。每轮localAcceptanceId冻结进原详情sink,迟到结果不借新轮ID;LOCAL_USER_TURN_CHANGED立即停止本轮,不重接纳或退legacy。已用固定真包完成本地D19组件联测;真实N/S员工端仍NOT_RUN,产品验收以 leo 实际执行结果为准。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
15
+ 从官方插件管理安装,照常使用默认工作区/会话/模型入口。L0.7.17沿唯一“远程主机连接”设置节记录文件来源;主机来源沿用L既有解析;本地来源接入N0.4.4/S0.4.11员工端点。旧映射可在本轮官方一次确认中选择改用本机文件夹;拒绝、取消或输入/来源变化时不切换、不执行。也可在L设置逐工作区确认沿用原主机关联,新主机位置仍按L现有合同停止为WORKSPACE_LOCATION_PROVIDER_REQUIRED。本地六类工具经Claude公开toolAliases路由到员工电脑:cwd是员工选定目录,Git使用本机仓库,不回退主机同名目录。本地真实员工输入先取得官方当前轮次CAS预期,再调用L0.7.17 acceptLocalWorkspaceUserTurn;核回包后才打开D19和本轮主机目录/执行。每轮localAcceptanceId冻结进原详情sink,迟到结果不借新轮ID;LOCAL_USER_TURN_CHANGED立即停止本轮,不重接纳或退legacy。已用固定真包完成本地D19组件联测;编排已回证0.8.14 Claude/Opus5/Default在leo本地目录首轮写文件成功;该回证不覆盖本0.8.16或Codex,产品验收以各版本leo实际结果为准。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
16
16
 
17
17
  未登录时两个模型均回复:**需要登录,请到设置 → 远程主机连接 点登录**。无L或缺正式能力时直接说明缺项,不让员工填写主机、用户、路径或密钥。
18
18
 
@@ -74,9 +74,9 @@ D22阶段耗时进入技术详情;D23文件审批用中文动作,原工具
74
74
 
75
75
  D21(0.8.5):两个品牌的官方模型菜单从 L 的 `modelCatalogTarget:1` / `getModelCatalogTarget` 取得当前可信员工目标,再只读调用 S 的 `aw-model-catalog --provider codex|claude`。目录按 host-user 作用域展示完整模型与逐模型推理等级,保留远端标识原值;失败显示“无法获取模型列表:原因”,不登录、不创建项目、不返回替代模型。L 必须宣告该正式能力,S 必须安装保护入口;这两项未就绪时不能据组件测试声称菜单可用。
76
76
 
77
- 每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名每轮须有精确的模型配置成功回执,并由同一员工主机、同一连接代际的目录唯一映射到具体型号。具体型号注明来自主机目录,会话未单独回报;旧恢复记录不作本轮证据。目录缺项、映射不唯一、回执不符或身份/代际变化时不发送输入。真实回复追加所选值和远端确认值,并在恢复标识与技术详情保留,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
77
+ 每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名每轮须有精确的模型配置成功回执,并由同一员工主机、同一连接代际的目录唯一映射到具体型号。具体型号注明来自主机目录,会话未单独回报;旧恢复记录不作本轮证据。目录缺项、映射不唯一、回执不符或身份/代际变化时不发送输入。所选值、远端确认值及型号来源仅在恢复标识与技术详情保留,插件不向真实回复追加技术参数,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
78
78
 
79
- 0.8.5统一包原依赖L `~0.7.13`;当前0.8.14对齐L `~0.7.19`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理,X新增设置0。
79
+ 0.8.5统一包原依赖L `~0.7.13`;当前0.8.16对齐L `~0.7.19`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理,X新增设置0。
80
80
 
81
81
  D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建会话上下文政策,官方 CLI 仍为未改的0.160.1。系统/开发者要求仅在新建时生效;恢复时与初建摘要相同则正常恢复且省略新建专用字段,不声称恢复接口会更新指令。要求不同或清空时,在同一真实回复说明“会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。”,并新建远端承接公开对话,保留旧标识和查询记录;旧/非法初建摘要明确拒绝,不能猜测。
82
82
 
@@ -88,7 +88,7 @@ D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建
88
88
 
89
89
  Read/Write/Edit/Glob/Grep由员工DSH同UID后端执行;Bash经L严格SSH到S同代桥再经N员工SSH端点执行,命令正文只走stdin。每次调用核官方会话唯一工作区、L实例/epoch/绑定/目录身份、N真实prepare及S实时target。取消、SSH255或退出证明缺失记录UNKNOWN,不重放。只关闭自己的绑定执行器,不撤其他工作区或设备服务。cwd不是权限根,绝对路径和上级访问遵循真实OS权限;不增加目录白名单。
90
90
 
91
- 每轮同一官方回复明确:子代理和harness内部直接工具调用尚未证明可在员工端执行,可能仍在主机;已识别的原生未改道工具/Agent审批拒绝。PDF等二进制读取及后台命令明确不支持,不静默主机代跑。六类组件/MCP/配置转交证明不等于真实Claude原生alias执行或真实员工链路通过。实际Git依赖员工环境中已有Git,缺失会返回实际命令失败,不在主机替代,也不自动安装。
91
+ 本机执行在每个DSH会话首次用一句“文件和命令在你的电脑上执行”说明;既有0.8.14说明也计为已显示。完整限制每轮记录在技术详情,遇到不支持的操作再明确提示:子代理和harness内部直接工具调用尚未证明可在员工端执行,可能仍在主机;已识别的原生未改道工具/Agent审批拒绝。PDF等二进制读取及后台命令明确不支持,不静默主机代跑。六类组件/MCP/配置转交证明不等于真实Claude原生alias执行或真实员工链路通过。实际Git依赖员工环境中已有Git,缺失会返回实际命令失败,不在主机替代,也不自动安装。
92
92
 
93
93
  本地目前仅支持macOS(arm64/x64);Grep固定携带微软@vscode/ripgrep-universal1.18.0的两架构原字节并核SHA,无员工PATH搜索或安装下载。主机需S0.4.11,N可选peer~0.4.4(缺失明确报能力缺项),L当前~0.7.18,本地接纳与D19工程路径已接,真实员工端门仍开放。无DSH/CLI修改、额外设置节或npm发布。
94
94
 
@@ -124,3 +124,5 @@ D20(0.8.8开发记录):本地来源的Codex原生文件与命令通过固
124
124
  每次目录SSH保独立事件ID、品牌、UTC起止、耗时、PID、L实例/epoch/phase、严格L argv/目标/known_hosts/ProxyCommand、退出码/信号、完整脱敏stderr与原字节数/SHA256、stdout字节数/SHA及主机typed错误;不记录凭据或整份模型目录。日志在官方profile的 `ai-workbench-remote-agent/catalog-diagnostics/<eventId>-started.json` 与 `-closed.json`,700目录/600文件、fsync;无自动过期,最新两品牌在界面可展开,旧调用保留不可变历史。容量沿现有`acpMaxMessageBytes`,超限停止并标记stderrComplete=false,UTF-8替换另显式标记,未返回部分模型;保存失败明确显示仅内存,不冒称耐久。旧0811丢失的stderr无法还原。同品牌、instanceId、epoch只合并仍在进行的查询;完成后不缓存,身份/代际/可信目标改变即作废。调用方各持独立lease/取消与错误身份,一个取消不会取消其他等待者;全员取消才撤销该次查询,取消中的旧进程结束前同作用域新调用等待清理。登录检查的显示变化不会取消无需登录的host-user目录。此次没有增加超时、重试、SSH连接复用或产品设置;不等于255、Claude主机错误及N层根因已修复,也未进行leo GUI验收。
125
125
 
126
126
  0.8.14 不把空菜单、非穷举 picker、可选 UI category/label 或同值配置通知当作额外发送门槛;每轮仍要求可靠模型入口与 fresh exact setter ACK。本轮完成后的配置变化只记录 warning,保留真实回复与 finish,并明确未获单独的实际执行型号回报。同一身份、代际与可信目标的 needCode/诊断变化只记 info;真实身份、代际、目标或能力失效仍撤回。每次关闭/取消保留 caller、字段前后值和首次观测原因(不等于根因)。新增配置、阈值、超时、重试为 0。
127
+
128
+ 0.8.16 不新增 Config、默认值、阈值、超时或重试。首因可见、旧代不重投、实际选择与绑定一致属于不可关闭的正确性不变量。目录完成结果不用于后续请求缓存;活跃租约仅保留更新观测的选择有效性证据,释放即撤登记。断连/错代仍保留原详情信封及未确认状态,仅按旧合同读 receipt 核验。`unconfirmed-<eventId>.json` 私有记录追加是否尝试及失败阶段/固定码,不能当作 L 的 ACK;员工可见原因沿官方错误消息,无新增面板。
package/dist/acp.js CHANGED
@@ -390,7 +390,7 @@ export class AcpSession {
390
390
  signal?.addEventListener('abort', abort, { once: true });
391
391
  try {
392
392
  const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
393
- protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.14' },
393
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.16' },
394
394
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1 } } } : {}) },
395
395
  }, { cancellationSignal: signal }));
396
396
  this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
@@ -6,6 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
+ import { CLAUDE_EMPLOYEE_SCOPE_NOTICE as NOTICE } from "./employee-presentation.js";
9
10
  import { CloseDiagnostics, closeCode } from "./close-diagnostics.js";
10
11
  import { isDeepStrictEqual } from 'node:util';
11
12
  import { openEmployeeLease } from "./employee-endpoint.js";
@@ -16,7 +17,6 @@ import { sshIo } from "./broker.js";
16
17
  import { NeedError, shellQuote } from "./remote.js";
17
18
  import { awaitL } from "./provider.js";
18
19
  const failure = (code) => new NeedError(code, '当前本机执行上下文无法确认,本轮已停止;没有改在主机执行。', 'L当前本地绑定、N员工端点与S同代桥');
19
- const NOTICE = '本轮六类文件与命令工具在你的电脑执行,使用你选择的文件夹;Git也使用本机仓库。子代理和内部直接调用尚不支持本机,可能仍在主机;已识别的此类审批不会执行。PDF等二进制读取、后台命令暂不支持。';
20
20
  /** Local source only. Host and legacy-host remain with the original provider. */
21
21
  export function createClaudeLocalProvider(options) {
22
22
  const entries = new Map();
@@ -1,5 +1,4 @@
1
1
  import type { DetailObservation } from './conversation-details.ts';
2
- import { NeedError } from './remote.ts';
3
2
  export interface CloseChange {
4
3
  field: string;
5
4
  before: string | number | boolean | null;
@@ -18,11 +17,7 @@ export interface CloseCause {
18
17
  changes?: readonly CloseChange[];
19
18
  }
20
19
  export type CloseObserver = (cause: CloseCause) => void;
21
- /** Only typed codes, never exception messages, causes, stderr or RPC data. */
22
- export declare function closeCode(error: unknown, fallback?: string): string;
23
- /** Retain trusted typed codes through wrappers; never traverse error text or invoke getters. */
24
- export declare function closeErrorCodes(error: unknown, fallback?: string): string[];
25
- export declare function closeFailure(code: string, message: string, requires: string, codes: readonly string[]): NeedError;
20
+ export { closeCode, closeErrorCodes, closeFailure, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, primaryFailureCode } from './failure-evidence.ts';
26
21
  /** Assign order before abort/kill; persist through the existing asynchronous D19 outbox. */
27
22
  export declare class CloseDiagnostics {
28
23
  private sink;
@@ -1,37 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { NeedError } from "./remote.js";
3
- /** Only typed codes, never exception messages, causes, stderr or RPC data. */
4
- export function closeCode(error, fallback = 'CLOSE_REASON_UNKNOWN') {
5
- return error instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : fallback;
6
- }
7
- /** Retain trusted typed codes through wrappers; never traverse error text or invoke getters. */
8
- export function closeErrorCodes(error, fallback = 'CLOSE_REASON_UNKNOWN') {
9
- const queue = [error], seen = new Set(), codes = new Set();
10
- while (queue.length) {
11
- const next = queue.pop();
12
- if (!(next instanceof NeedError || next instanceof AggregateError) || seen.has(next))
13
- continue;
14
- seen.add(next);
15
- if (next instanceof NeedError) {
16
- const code = Object.getOwnPropertyDescriptor(next, 'code');
17
- if (code && 'value' in code && typeof code.value === 'string' && /^[A-Z][A-Z0-9_]*$/.test(code.value))
18
- codes.add(code.value);
19
- }
20
- const cause = Object.getOwnPropertyDescriptor(next, 'cause');
21
- if (cause && 'value' in cause)
22
- queue.push(cause.value);
23
- if (next instanceof AggregateError) {
24
- const errors = Object.getOwnPropertyDescriptor(next, 'errors');
25
- if (errors && 'value' in errors && Array.isArray(errors.value))
26
- for (const child of errors.value)
27
- queue.push(child);
28
- }
29
- }
30
- return codes.size ? [...codes] : [fallback];
31
- }
32
- export function closeFailure(code, message, requires, codes) {
33
- return Object.assign(new NeedError(code, message, requires), { cause: new AggregateError([...new Set(codes)].map(value => new NeedError(value, '关闭收尾代码 ' + value, '按安全代码核对')), 'CLOSE_AND_DIAGNOSTIC_FAILURES') });
34
- }
3
+ import { closeCode, closeErrorCodes, withFailureEvidence } from "./failure-evidence.js";
4
+ export { closeCode, closeErrorCodes, closeFailure, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, primaryFailureCode } from "./failure-evidence.js";
35
5
  /** Assign order before abort/kill; persist through the existing asynchronous D19 outbox. */
36
6
  export class CloseDiagnostics {
37
7
  sink;
@@ -71,7 +41,7 @@ export class CloseDiagnostics {
71
41
  const payload = Object.freeze({ ...metadata, pendingOperations: Object.freeze([...this.pending.keys()]), closeId: this.id, ...this.parentId ? { parentCloseId: this.parentId } : {}, closeSequence: ++this.sequence, first, stage: this.stage, clock: 'client-monotonic', elapsedMs: performance.now() - this.started });
72
42
  this.firstValue ??= payload;
73
43
  try {
74
- sink({ kind: 'session', severity: 'warning', code: first ? 'REMOTE_CLOSE_FIRST' : 'REMOTE_CLOSE_NEXT', summary: first ? '首次观察到关闭(不等同根因)' : '后续关闭或清理', payload });
44
+ sink({ kind: 'session', severity: 'warning', code: first ? 'REMOTE_CLOSE_FIRST' : 'REMOTE_CLOSE_NEXT', summary: cause.changedFields?.includes('selected-model') ? '最新可信目录已不再提供本轮所选型号,本轮已停止;请在模型菜单核对选择。' : first ? '首次观察到关闭(不等同根因)' : '后续关闭或清理', payload });
75
45
  }
76
46
  catch (error) {
77
47
  this.state.failure ??= { error };
@@ -97,7 +67,7 @@ export async function flushCloseDiagnostics(diagnostics, flush, primary) {
97
67
  }
98
68
  if (reporting.length) {
99
69
  const primaryCodes = primary ? closeErrorCodes(primary.error, 'REMOTE_CLEANUP_UNCONFIRMED') : [];
100
- throw closeFailure('CONVERSATION_DETAILS_FAILED', '关闭诊断保存未完成;原关闭与清理结果需核对。', '首次关闭 ' + String(diagnostics.first?.reason ?? 'CLOSE_REASON_UNKNOWN') + ';清理 ' + (primaryCodes.join(',') || 'closed') + ';保存 ' + reporting.join(','), [...primaryCodes, ...reporting]);
70
+ throw withFailureEvidence(primary?.error ?? new NeedError(primaryCodes[0] ?? 'REMOTE_CLEANUP_UNCONFIRMED', '关闭收尾未确认', '核对实际结果'), reporting.map(code => new NeedError(code, '诊断保存未确认', '只读核对')), 'diagnostic', diagnostics.first?.reason);
101
71
  }
102
72
  if (primary)
103
73
  throw primary.error;
@@ -345,7 +345,7 @@ function closeStructure(value) {
345
345
  if (!change || typeof change !== 'object' || Array.isArray(change))
346
346
  return opaqueStructure(change);
347
347
  const field = change.field;
348
- const known = typeof field === 'string' && (/^(?:observer.close-sequence|executor.ended|transport.closed|transport.termination-requested|provider.ended|broker.ended|broker.attached|broker.lease-aborted|lease.aborted|lease.closed|mcp.ended|mcp.closing|mcp.poisoned|shell.settled|shell.stop-code|tools.aborted|tools.stop-code|acp.close-requested|adapter.turn-aborted|adapter.detached|adapter.shutting-down|workspace.binding-id|workspace.source|workspace.status|workspace.match-count|network.phase|catalog.api-replaced|catalog.capability|catalog.signature-sha256|connection.instance-id|connection.epoch|connection.phase|connection.need-code)$/.test(field) || /^catalog\.(?:codex|claude)\.(?:ok|error-code|instance-id|epoch|provider|target-sha256|metadata-sha256)$/.test(field));
348
+ const known = typeof field === 'string' && (/^(?:observer.close-sequence|executor.ended|transport.closed|transport.termination-requested|provider.ended|broker.ended|broker.attached|broker.lease-aborted|lease.aborted|lease.closed|mcp.ended|mcp.closing|mcp.poisoned|shell.settled|shell.stop-code|tools.aborted|tools.stop-code|acp.close-requested|adapter.turn-aborted|adapter.detached|adapter.shutting-down|workspace.binding-id|workspace.source|workspace.status|workspace.match-count|network.phase|catalog.api-replaced|catalog.capability|catalog.signature-sha256|connection.instance-id|connection.epoch|connection.phase|connection.need-code)$/.test(field) || /^catalog\.(?:codex|claude)\.(?:ok|error-code|instance-id|epoch|provider|target-sha256|metadata-sha256|selected-model-valid)$/.test(field));
349
349
  if (!known)
350
350
  return opaqueStructure(change);
351
351
  const scalar = (value) => value === null || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value) || typeof value === 'string' && /^[a-zA-Z0-9_.:@-]+$/.test(value) ? value : opaqueStructure(value);
@@ -0,0 +1,6 @@
1
+ import type { GenerateOptions } from '@deepseek-ai/dsh-llm';
2
+ export declare const CLAUDE_EMPLOYEE_SCOPE_NOTICE = "\u672C\u8F6E\u516D\u7C7B\u6587\u4EF6\u4E0E\u547D\u4EE4\u5DE5\u5177\u5728\u4F60\u7684\u7535\u8111\u6267\u884C\uFF0C\u4F7F\u7528\u4F60\u9009\u62E9\u7684\u6587\u4EF6\u5939\uFF1BGit\u4E5F\u4F7F\u7528\u672C\u673A\u4ED3\u5E93\u3002\u5B50\u4EE3\u7406\u548C\u5185\u90E8\u76F4\u63A5\u8C03\u7528\u5C1A\u4E0D\u652F\u6301\u672C\u673A\uFF0C\u53EF\u80FD\u4ECD\u5728\u4E3B\u673A\uFF1B\u5DF2\u8BC6\u522B\u7684\u6B64\u7C7B\u5BA1\u6279\u4E0D\u4F1A\u6267\u884C\u3002PDF\u7B49\u4E8C\u8FDB\u5236\u8BFB\u53D6\u3001\u540E\u53F0\u547D\u4EE4\u6682\u4E0D\u652F\u6301\u3002";
3
+ export declare const CODEX_EMPLOYEE_SCOPE_NOTICE = "\u6587\u4EF6\u4E0E\u547D\u4EE4\u5728\u672C\u673A\u9009\u5B9A\u76EE\u5F55\u8FD0\u884C\uFF0CGit \u4F7F\u7528\u672C\u673A\u4ED3\u5E93\uFF1B\u6A21\u578B\u3001\u767B\u5F55\u548C\u5168\u5C40\u52A9\u624B\u8981\u6C42\u4ECD\u5728\u8FDC\u7A0B\u4E3B\u673A\u3002\u672C\u673A\u81EA\u5B9A\u4E49\u7684\u9644\u52A0\u5DE5\u5177\u3001\u81EA\u52A8\u52A8\u4F5C\u3001\u63D2\u4EF6\u53CA\u65AD\u5F00\u540E\u7684\u4F1A\u8BDD\u6062\u590D\u6682\u4E0D\u652F\u6301\u3002\u5B50\u4EFB\u52A1\u88AB\u7CFB\u7EDF\u56DE\u6536\u540E\u80FD\u5426\u7EE7\u7EED\uFF0C\u5C1A\u672A\u9A8C\u8BC1\u3002";
4
+ export declare const EMPLOYEE_EXECUTION_NOTICE = "\u6587\u4EF6\u548C\u547D\u4EE4\u5728\u4F60\u7684\u7535\u8111\u4E0A\u6267\u884C";
5
+ /** Presentation receipt only; it grants no process, workspace or replay authority. */
6
+ export declare function employeeNoticePreviouslyShown(options: Pick<GenerateOptions, 'messages' | 'sessionId'>): boolean;
@@ -0,0 +1,16 @@
1
+ import { AGENTS } from "./agents.js";
2
+ export const CLAUDE_EMPLOYEE_SCOPE_NOTICE = '本轮六类文件与命令工具在你的电脑执行,使用你选择的文件夹;Git也使用本机仓库。子代理和内部直接调用尚不支持本机,可能仍在主机;已识别的此类审批不会执行。PDF等二进制读取、后台命令暂不支持。';
3
+ export const CODEX_EMPLOYEE_SCOPE_NOTICE = '文件与命令在本机选定目录运行,Git 使用本机仓库;模型、登录和全局助手要求仍在远程主机。本机自定义的附加工具、自动动作、插件及断开后的会话恢复暂不支持。子任务被系统回收后能否继续,尚未验证。';
4
+ export const EMPLOYEE_EXECUTION_NOTICE = '文件和命令在你的电脑上执行';
5
+ /** Presentation receipt only; it grants no process, workspace or replay authority. */
6
+ export function employeeNoticePreviouslyShown(options) {
7
+ if (!options.sessionId)
8
+ return false;
9
+ return options.messages.some(message => {
10
+ if (message.role !== 'assistant' || !AGENTS.some(agent => agent.id === message.source.provider))
11
+ return false;
12
+ const pointer = message.source.replayState?.response;
13
+ const sameSession = !!pointer && pointer.version === 2 && pointer.agent === message.source.provider && pointer.dshSessionId === options.sessionId && [pointer.identity, pointer.cwd, pointer.sessionId].every(value => typeof value === 'string' && value.length > 0);
14
+ return sameSession && (pointer.presentation?.employeeExecutionNotice === 'local-v1' || message.content.some(block => block.type === 'text' && [CLAUDE_EMPLOYEE_SCOPE_NOTICE, CODEX_EMPLOYEE_SCOPE_NOTICE].some(notice => block.text.startsWith(notice + '\n\n'))));
15
+ });
16
+ }
@@ -6,7 +6,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- import { CloseDiagnostics, closeCode, closeErrorCodes } from "./close-diagnostics.js";
9
+ import { CODEX_EMPLOYEE_SCOPE_NOTICE } from "./employee-presentation.js";
10
+ import { CloseDiagnostics, withFailureEvidence, closeCode, closeErrorCodes } from "./close-diagnostics.js";
10
11
  import { createHash } from 'node:crypto';
11
12
  import { admitEmployeeLocation } from "./employee-location.js";
12
13
  import { EmployeeExecution } from "./employee-exec.js";
@@ -245,7 +246,7 @@ export function createEmployeeProvider(get, options, agent) {
245
246
  }
246
247
  const disposers = [];
247
248
  const execution = new EmployeeExecution({ closeDiagnostics, bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
248
- const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes }, employeePresentation: '文件与命令在本机选定目录运行,Git 使用本机仓库;模型、登录和全局助手要求仍在远程主机。本机自定义的附加工具、自动动作、插件及断开后的会话恢复暂不支持。子任务被系统回收后能否继续,尚未验证。' };
249
+ const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes }, employeePresentation: CODEX_EMPLOYEE_SCOPE_NOTICE };
249
250
  entry = { scope, runtime, current: input, details, execution, network, ended: false, dispose: (reason, cause = { layer: 'runtime', initiator: 'provider.dispose', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', action: 'dispose' }) => {
250
251
  closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'provider.ended', before: entry.ended, after: true }] });
251
252
  if (entry.closing)
@@ -335,13 +336,21 @@ export function createEmployeeProvider(get, options, agent) {
335
336
  return runtime;
336
337
  }
337
338
  catch (error) {
338
- details?.record({ kind: 'need', severity: 'action-required', code: errorCode(error), summary: asNeed(error).message, payload: { result: 'UNKNOWN' } });
339
+ const reporting = [];
340
+ try {
341
+ details?.record({ kind: 'need', severity: 'action-required', code: errorCode(error), summary: asNeed(error).message, payload: { result: 'UNKNOWN' } });
342
+ }
343
+ catch (failure) {
344
+ reporting.push(failure);
345
+ }
339
346
  try {
340
347
  await details?.flush();
341
348
  }
342
- catch {
343
- throw fail('CONVERSATION_DETAILS_FAILED');
349
+ catch (failure) {
350
+ reporting.push(failure);
344
351
  }
352
+ if (reporting.length)
353
+ throw withFailureEvidence(error, reporting, 'diagnostic', closeDiagnostics?.first?.reason);
345
354
  throw error;
346
355
  }
347
356
  };
@@ -0,0 +1,12 @@
1
+ import { NeedError } from './remote.ts';
2
+ /** Only typed codes, never exception messages, causes, stderr or RPC data. */
3
+ export declare function closeCode(error: unknown, fallback?: string): string;
4
+ /** Retain trusted typed codes through wrappers; never traverse error text or invoke getters. */
5
+ export declare function closeErrorCodes(error: unknown, fallback?: string): string[];
6
+ export declare function closeFailure(code: string, message: string, requires: string, codes: readonly string[]): NeedError;
7
+ /** Saving succeeded: retain the original trusted error type, code and presentation. */
8
+ export declare function withFirstFailureEvidence<T>(primary: T, firstReason: unknown, primaryCode: string): T;
9
+ /** Preserve causal roles without copying arbitrary exception text into employee output. */
10
+ export declare function withFailureEvidence(primary: unknown, secondary: readonly unknown[], kind: 'diagnostic' | 'cleanup', firstReason?: unknown, primaryCode?: string): NeedError;
11
+ export declare function primaryFailureCode(error: unknown, fallback: string): string;
12
+ export declare function failureEvidenceSummary(error: unknown): string;
@@ -0,0 +1,83 @@
1
+ import { NeedError } from "./remote.js";
2
+ /** Only typed codes, never exception messages, causes, stderr or RPC data. */
3
+ export function closeCode(error, fallback = 'CLOSE_REASON_UNKNOWN') {
4
+ return error instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : fallback;
5
+ }
6
+ /** Retain trusted typed codes through wrappers; never traverse error text or invoke getters. */
7
+ export function closeErrorCodes(error, fallback = 'CLOSE_REASON_UNKNOWN') {
8
+ const queue = [error], seen = new Set(), codes = new Set();
9
+ while (queue.length) {
10
+ const next = queue.pop();
11
+ if (!(next instanceof NeedError || next instanceof AggregateError) || seen.has(next))
12
+ continue;
13
+ seen.add(next);
14
+ if (next instanceof NeedError) {
15
+ const code = Object.getOwnPropertyDescriptor(next, 'code');
16
+ if (code && 'value' in code && typeof code.value === 'string' && /^[A-Z][A-Z0-9_]*$/.test(code.value))
17
+ codes.add(code.value);
18
+ }
19
+ const cause = Object.getOwnPropertyDescriptor(next, 'cause');
20
+ if (cause && 'value' in cause)
21
+ queue.push(cause.value);
22
+ if (next instanceof AggregateError) {
23
+ const errors = Object.getOwnPropertyDescriptor(next, 'errors');
24
+ if (errors && 'value' in errors && Array.isArray(errors.value))
25
+ for (const child of errors.value)
26
+ queue.push(child);
27
+ }
28
+ }
29
+ return codes.size ? [...codes] : [fallback];
30
+ }
31
+ export function closeFailure(code, message, requires, codes) {
32
+ return Object.assign(new NeedError(code, message, requires), { cause: new AggregateError([...new Set(codes)].map(value => new NeedError(value, '关闭收尾代码 ' + value, '按安全代码核对')), 'CLOSE_AND_DIAGNOSTIC_FAILURES') });
33
+ }
34
+ const evidence = new WeakMap();
35
+ const safeCode = (code) => typeof code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(code);
36
+ function retainedEvidence(error) {
37
+ const queue = [error], seen = new Set();
38
+ while (queue.length) {
39
+ const current = queue.shift();
40
+ if (current && typeof current === 'object') {
41
+ const known = evidence.get(current);
42
+ if (known)
43
+ return known;
44
+ }
45
+ if (!(current instanceof NeedError || current instanceof AggregateError) || seen.has(current))
46
+ continue;
47
+ seen.add(current);
48
+ const cause = Object.getOwnPropertyDescriptor(current, 'cause');
49
+ if (cause && 'value' in cause)
50
+ queue.push(cause.value);
51
+ const children = Object.getOwnPropertyDescriptor(current, 'errors');
52
+ if (children && 'value' in children && Array.isArray(children.value))
53
+ queue.push(...children.value);
54
+ }
55
+ return undefined;
56
+ }
57
+ /** Saving succeeded: retain the original trusted error type, code and presentation. */
58
+ export function withFirstFailureEvidence(primary, firstReason, primaryCode) {
59
+ if (primary && typeof primary === 'object' && safeCode(firstReason) && safeCode(primaryCode)) {
60
+ const previous = retainedEvidence(primary);
61
+ evidence.set(primary, Object.freeze({ first: previous?.first ?? firstReason, original: previous?.original ?? Object.freeze([primaryCode]), diagnostic: previous?.diagnostic ?? Object.freeze([]), cleanup: previous?.cleanup ?? Object.freeze([]) }));
62
+ }
63
+ return primary;
64
+ }
65
+ /** Preserve causal roles without copying arbitrary exception text into employee output. */
66
+ export function withFailureEvidence(primary, secondary, kind, firstReason, primaryCode) {
67
+ const previous = retainedEvidence(primary), first = previous?.first ?? (safeCode(firstReason) ? firstReason : undefined);
68
+ const original = previous?.original ?? (safeCode(primaryCode) ? [primaryCode] : closeErrorCodes(primary, 'TURN_FAILED'));
69
+ const secondaryFacts = secondary.map(retainedEvidence), added = [...new Set(secondary.flatMap((error, index) => secondaryFacts[index] ? secondaryFacts[index].original.filter(code => !original.includes(code)) : closeErrorCodes(error, kind === 'diagnostic' ? 'DIAGNOSTIC_WRITE_FAILED' : 'REMOTE_CLEANUP_UNCONFIRMED')))];
70
+ const facts = Object.freeze({ ...(first ? { first } : {}), original: Object.freeze(original), diagnostic: Object.freeze([...new Set([...(previous?.diagnostic ?? []), ...secondaryFacts.flatMap(f => f?.diagnostic ?? []), ...(kind === 'diagnostic' ? added : [])])]), cleanup: Object.freeze([...new Set([...(previous?.cleanup ?? []), ...secondaryFacts.flatMap(f => f?.cleanup ?? []), ...(kind === 'cleanup' ? added : [])])]) });
71
+ const code = kind === 'cleanup' ? 'REMOTE_CLEANUP_UNCONFIRMED' : !secondary.length ? primary instanceof NeedError ? primary.code : safeCode(primaryCode) ? primaryCode : 'TURN_FAILED' : primary instanceof NeedError && facts.diagnostic.length > 0 && facts.diagnostic.every(code => code === primary.code) && !facts.cleanup.length && !previous ? primary.code : 'CONVERSATION_DETAILS_FAILED';
72
+ const failure = Object.assign(new NeedError(code, '本轮已停止;运行原因与诊断或清理结果分别保留。', '核对原始原因及未确认的保存/清理结果'), { cause: new AggregateError([primary, ...(safeCode(primaryCode) && !(primary instanceof NeedError) ? [new NeedError(primaryCode, '运行错误代码', '可信调用点提供的固定代码')] : []), ...secondary, ...(first ? [new NeedError(first, '首次观察关闭代码', '不是已证明根因')] : [])], 'TURN_FAILURE_EVIDENCE') });
73
+ evidence.set(failure, facts);
74
+ failure.requires = '原错误代码 ' + facts.original.join('、') + (facts.diagnostic.length ? ';诊断错误代码 ' + facts.diagnostic.join('、') : '') + (facts.first ? ';首次观察关闭 ' + facts.first : '') + (facts.cleanup.length ? ';清理错误代码 ' + facts.cleanup.join('、') : '');
75
+ return failure;
76
+ }
77
+ export function primaryFailureCode(error, fallback) { return retainedEvidence(error)?.original[0] ?? fallback; }
78
+ export function failureEvidenceSummary(error) {
79
+ const facts = retainedEvidence(error);
80
+ if (!facts)
81
+ return '';
82
+ return '技术详情:' + [...(facts.first ? ['首次观察关闭 ' + facts.first] : []), '运行原因 ' + facts.original.join('、'), ...(facts.diagnostic.length ? ['详情保存未确认 ' + facts.diagnostic.join('、')] : []), ...(facts.cleanup.length ? ['清理未确认 ' + facts.cleanup.join('、')] : [])].join(';');
83
+ }
package/dist/index.d.ts CHANGED
@@ -76,6 +76,8 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
76
76
  private detached;
77
77
  private cleanups;
78
78
  private preparationFailures;
79
+ private modelPresentation;
80
+ private employeeNoticeSessions;
79
81
  constructor(config: PluginConfig | (() => PluginConfig), resolveRuntime: RuntimeProvider, approve?: InvocationApproval | undefined, providerName?: () => string, agent?: AgentDefinition, readImage?: ImageReader | undefined, reportLoginFailure?: LoginFailureReporter | undefined, directory?: ModelDirectory | undefined, preflightDiagnostics?: PreflightDiagnostics | undefined);
80
82
  providerInfo(provider: string): {
81
83
  id: string;
@@ -85,8 +87,12 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
85
87
  listModels(provider: string): Promise<(import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
86
88
  catalog: HostModel;
87
89
  })[]>;
88
- resolveModel(provider: string, id: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
89
- catalog: HostModel;
90
+ resolveModel(provider: string, id: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo>;
91
+ prepareCall(provider: string, id: string, signal?: AbortSignal): Promise<{
92
+ model: import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
93
+ catalog: HostModel;
94
+ };
95
+ stream: (options: GenerateOptions) => AsyncGenerator<StreamChunk, any, any>;
90
96
  }>;
91
97
  observeModelInvalidation(fields: readonly string[], changes?: readonly CloseChange[]): void;
92
98
  invalidateModelState(): Promise<void>;
@@ -118,8 +124,12 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
118
124
  listModels(provider: string): Promise<(import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
119
125
  catalog: HostModel;
120
126
  })[]>;
121
- resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
122
- catalog: HostModel;
127
+ resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo>;
128
+ prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<{
129
+ model: import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
130
+ catalog: HostModel;
131
+ };
132
+ stream: (options: GenerateOptions) => AsyncGenerator<StreamChunk, any, any>;
123
133
  }>;
124
134
  observeModelInvalidation(fields: readonly string[], changes?: readonly CloseChange[]): void;
125
135
  invalidateModelState(): Promise<void>;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CloseDiagnostics, closeCode, closeErrorCodes, closeFailure } from "./close-diagnostics.js";
1
+ import { CloseDiagnostics, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, closeCode, closeErrorCodes, closeFailure } from "./close-diagnostics.js";
2
2
  import { runCatalog } from "./catalog-ssh.js";
3
3
  import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
4
4
  import { SessionId } from '@deepseek-ai/dsh-session';
@@ -25,6 +25,8 @@ import { approvalLabel } from "./approval-label.js";
25
25
  import { StageTiming } from "./stage-timing.js";
26
26
  import { verifyRecoveryCheckpoint, currentInputIds } from "./recovery-checkpoint.js";
27
27
  import { ModelCatalog, modelInfo } from "./model-catalog.js";
28
+ import { ModelPresentation, unavailableModelInfo } from "./model-presentation.js";
29
+ import { EMPLOYEE_EXECUTION_NOTICE, employeeNoticePreviouslyShown } from "./employee-presentation.js";
28
30
  import { ModelConfigurationError } from "./model-configuration.js";
29
31
  import { officialEmployeeWorkspace } from "./employee-workspace.js";
30
32
  import { nativeWorkspaceSourceConfirmation } from "./workspace-source-confirmation.js";
@@ -113,6 +115,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
113
115
  detached = new Set();
114
116
  cleanups = new WeakMap();
115
117
  preparationFailures = new Map();
118
+ modelPresentation = new ModelPresentation();
119
+ employeeNoticeSessions = new Set();
116
120
  constructor(config, resolveRuntime, approve, providerName = () => PROVIDER_NAMES.en, agent = CODEX_AGENT, readImage, reportLoginFailure, directory, preflightDiagnostics) {
117
121
  super();
118
122
  this.resolveRuntime = resolveRuntime;
@@ -129,18 +133,21 @@ export class CodexRemoteAdapter extends LlmAdapter {
129
133
  catalog(signal) { if (!this.directory)
130
134
  throw new LlmError('无法获取模型列表:缺少可信主机目录能力', 'MODEL_CATALOG_UNAVAILABLE'); return this.directory.read(this.agent, signal); }
131
135
  async listModels(provider) { const catalog = await this.catalog(); try {
136
+ this.modelPresentation.remember(catalog);
132
137
  return catalog.models.map(model => modelInfo(provider, model));
133
138
  }
134
139
  finally {
135
140
  catalog.release();
136
141
  } }
137
142
  async resolveModel(provider, id, signal) { try {
143
+ signal?.throwIfAborted();
144
+ const displayed = this.modelPresentation.resolve(provider, id);
145
+ if (displayed)
146
+ return displayed;
138
147
  const catalog = await this.catalog(signal);
139
148
  try {
140
149
  const model = catalog.models.find(model => model.id === id);
141
- if (!model)
142
- throw new LlmError(id === 'remote' ? '无法获取模型列表:这个会话保存的是旧版模型选择,请打开本会话的模型菜单,重新选择模型后再发送。' : '所选模型已不在当前主机目录中,请打开本会话的模型菜单重新选择模型。', 'MODEL_CATALOG_UNAVAILABLE');
143
- return modelInfo(provider, model);
150
+ return model ? modelInfo(provider, model) : unavailableModelInfo(provider, id);
144
151
  }
145
152
  finally {
146
153
  catalog.release();
@@ -150,6 +157,25 @@ export class CodexRemoteAdapter extends LlmAdapter {
150
157
  markModelPreparationFailure(provider, error, signal);
151
158
  throw error;
152
159
  } }
160
+ async prepareCall(provider, id, signal) {
161
+ try {
162
+ const catalog = await this.catalog(signal);
163
+ try {
164
+ catalog.assertCurrent();
165
+ const selected = catalog.models.find(model => model.id === id);
166
+ if (!selected)
167
+ throw new LlmError('所选模型已不在当前主机目录中,请在模型菜单重新选择后再发送;本次输入尚未发送。', 'MODEL_CATALOG_UNAVAILABLE');
168
+ return { model: modelInfo(provider, selected), stream: (options) => this.stream(options) };
169
+ }
170
+ finally {
171
+ catalog.release();
172
+ }
173
+ }
174
+ catch (error) {
175
+ markModelPreparationFailure(provider, error, signal);
176
+ throw error;
177
+ }
178
+ }
153
179
  observeModelInvalidation(fields, changes = []) { for (const live of this.sessions.values())
154
180
  live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'catalog.signature-change', reason: 'MODEL_CATALOG_SIGNATURE_CHANGED', action: 'invalidate', changedFields: fields, changes }); }
155
181
  async invalidateModelState() { for (const [id, turn] of this.turns) {
@@ -214,6 +240,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
214
240
  for (const live of this.sessions.values())
215
241
  live.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.dispose', reason: 'PLUGIN_DISPOSED', action: 'dispose', changes: [{ field: 'adapter.shutting-down', before: this.shuttingDown, after: true }] });
216
242
  this.shuttingDown = true;
243
+ this.modelPresentation.clear();
244
+ this.employeeNoticeSessions.clear();
217
245
  const results = await Promise.allSettled([...new Set([...this.sessions.keys(), ...this.turns.keys()])].map(id => this.detachSession(id)));
218
246
  this.lifetime.abort();
219
247
  const failure = results.find(result => result.status === 'rejected');
@@ -226,10 +254,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
226
254
  }
227
255
  catch (error) {
228
256
  if (error instanceof ModelConfigurationError)
229
- throw new LlmError(error.message, error.code, { cause: error });
257
+ throw new LlmError(error.message + (failureEvidenceSummary(error) ? ';' + failureEvidenceSummary(error) : ''), error.code, { cause: error });
230
258
  if (error instanceof NeedError) {
231
259
  const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
232
- const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '');
260
+ const evidence = failureEvidenceSummary(error);
261
+ const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '') + (evidence ? ';' + evidence : '');
233
262
  const login = originalLoginFailure(error);
234
263
  let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
235
264
  if (login?.technical && this.reportLoginFailure) {
@@ -243,6 +272,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
243
272
  }
244
273
  throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (diagnostic ? ';' + diagnostic : '')) });
245
274
  }
275
+ if (error instanceof LlmError && failureEvidenceSummary(error))
276
+ throw new LlmError(error.failure.message + ';' + failureEvidenceSummary(error), error.code, { cause: error });
246
277
  throw error;
247
278
  }
248
279
  }
@@ -275,6 +306,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
275
306
  let catalog;
276
307
  let selected;
277
308
  let modelConfiguration;
309
+ let turnFailure;
310
+ let employeeNoticeShown = false;
278
311
  const information = [];
279
312
  const recordInformation = () => { if (runtime?.details)
280
313
  for (const entry of information.splice(0))
@@ -294,6 +327,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
294
327
  if (!mode || (mode === 'session' && (!Number.isSafeInteger(config.acpMaxMessageBytes) || config.acpMaxMessageBytes < 1)))
295
328
  throw new NeedError('SETTINGS_REQUIRED', '远程模型连接设置无效,请联系管理员检查', '有效的mode/acpMaxMessageBytes');
296
329
  const previous = readPointer(options, this.agent);
330
+ employeeNoticeShown = !!key && this.employeeNoticeSessions.has(key) || employeeNoticePreviouslyShown(options);
331
+ if (employeeNoticeShown && key)
332
+ this.employeeNoticeSessions.add(key);
297
333
  const last = options.messages.findLast(message => message.role === 'assistant' && !isWorkspaceNoticeMessage(message));
298
334
  const missingPointer = !!last && !previous;
299
335
  let requireFreshSession = missingPointer || !!key && this.preparationFailures.has(key);
@@ -369,8 +405,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
369
405
  throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
370
406
  if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
371
407
  throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
408
+ catalog.bindSelection?.(selected.id, options.reasoningEffort);
372
409
  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)); };
410
+ 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)); };
374
411
  observeSignal(catalog.signal, 'signal.model-catalog', 'MODEL_CATALOG_REVOKED');
375
412
  observeSignal(signal, 'signal.turn-composite', 'ABORTED');
376
413
  let modelSignal = AbortSignal.any([signal, catalog.signal]);
@@ -423,8 +460,19 @@ export class CodexRemoteAdapter extends LlmAdapter {
423
460
  activeMapper = mapper;
424
461
  let id;
425
462
  let stop;
426
- if (runtime.employeePresentation) {
427
- yield* mapper.text(runtime.employeePresentation + '\n\n');
463
+ if (runtime.employeePresentation)
464
+ details.record({ kind: 'session', severity: 'warning', code: 'EMPLOYEE_EXECUTION_SCOPE', summary: runtime.employeePresentation, payload: { source: 'local' } });
465
+ if (runtime.execution)
466
+ details.record({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_SCOPE', summary: runtime.execution.notice, payload: runtime.execution.scope });
467
+ if ((runtime.employeePresentation || runtime.execution) && !employeeNoticeShown) {
468
+ for (const chunk of mapper.text(EMPLOYEE_EXECUTION_NOTICE + '\n\n')) {
469
+ if (chunk.type === 'text-delta') {
470
+ employeeNoticeShown = true;
471
+ if (key)
472
+ this.employeeNoticeSessions.add(key);
473
+ }
474
+ yield chunk;
475
+ }
428
476
  yield* mapper.flush();
429
477
  }
430
478
  if (runtime.workspaceNotice) {
@@ -435,11 +483,6 @@ export class CodexRemoteAdapter extends LlmAdapter {
435
483
  yield* mapper.text(runtime.restartNotice + '\n\n');
436
484
  yield* mapper.flush();
437
485
  }
438
- if (runtime.execution) {
439
- details.record({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_SCOPE', summary: '本机工具路由及未覆盖操作范围', payload: runtime.execution.scope });
440
- yield* mapper.text(runtime.execution.notice + '\n\n');
441
- yield* mapper.flush();
442
- }
443
486
  if (mode === 'session') {
444
487
  if (live && live.runtime !== runtime)
445
488
  throw new NeedError('SESSION_TRANSPORT_MISMATCH', '原会话的主机通道已更换,不能自动切换进程', '原主机会话通道');
@@ -530,7 +573,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
530
573
  const resolved = mapping[0].resolvedModel;
531
574
  if (typeof resolved !== 'string' || !resolved.trim())
532
575
  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 } : {}) };
576
+ 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 } : {}) };
534
577
  details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
535
578
  }
536
579
  catch (error) {
@@ -599,7 +642,6 @@ export class CodexRemoteAdapter extends LlmAdapter {
599
642
  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
643
  await details.flush();
601
644
  }
602
- yield* mapper.text(`\n\n本轮所选模型:${modelConfiguration.selected.name}(${modelConfiguration.selected.value});推理等级:${modelConfiguration.selected.effort ?? '沿用远端配置'}。发送前远端配置回执:${modelConfiguration.configured.model};${modelResolutionLabel(modelConfiguration)};推理等级:${modelConfiguration.configured.effort ?? '远端未提供'}(${changed ? '本轮期间配置有变化,实际执行型号未单独回报' : '发送前配置已确认,推理效果未验证'})。`);
603
645
  }
604
646
  yield* mapper.flush();
605
647
  let recoveryCheckpointId;
@@ -609,46 +651,44 @@ export class CodexRemoteAdapter extends LlmAdapter {
609
651
  recoveryCheckpointId = id;
610
652
  }
611
653
  const previousRemoteSessions = [...(previous?.previousRemoteSessions ?? []), ...(contextChanged ? [{ sessionId: previous.sessionId, contextSha256: previous.contextSha256 }] : [])];
612
- const pointer = { ...(previousRemoteSessions.length ? { previousRemoteSessions } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), ...(recoveryCheckpointId ? { recoveryCheckpointId } : {}), ...(runtime.workspaceNotice ? { presentation: { workspaceNotice: runtime.workspaceNotice } } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
654
+ const presentation = { ...(runtime.workspaceNotice ? { workspaceNotice: runtime.workspaceNotice } : {}), ...(employeeNoticeShown ? { employeeExecutionNotice: 'local-v1' } : {}) };
655
+ const pointer = { ...(previousRemoteSessions.length ? { previousRemoteSessions } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), ...(recoveryCheckpointId ? { recoveryCheckpointId } : {}), ...(Object.keys(presentation).length ? { presentation } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), mode, identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
613
656
  succeeded = true;
614
657
  yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
615
658
  }
616
659
  catch (error) {
660
+ turnFailure = error;
617
661
  runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
618
662
  recordInformation();
619
663
  failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
620
664
  if (live && session && session.promptDispatchCount > promptBaseline)
621
665
  live.failure = { cause: error };
622
- let reportingFailure;
666
+ const reporting = [];
623
667
  if (request?.recordEarlyFailure) {
624
668
  const record = request.recordEarlyFailure;
625
669
  request.recordEarlyFailure = undefined;
626
- const code = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : signal.aborted ? 'ABORTED' : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
627
670
  try {
628
- await record(Object.assign(new NeedError(code, '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
671
+ await record(Object.assign(new NeedError(failureCode ?? 'TURN_PREPARATION_FAILED', '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
629
672
  }
630
673
  catch (failure) {
631
- reportingFailure = { cause: failure };
674
+ reporting.push(failure);
632
675
  }
633
676
  }
634
677
  try {
635
678
  await runtime?.details?.flush();
636
679
  }
637
680
  catch (failure) {
638
- if (reportingFailure === undefined)
639
- reportingFailure = { cause: failure };
640
- else {
641
- const first = reportingFailure.cause;
642
- const originalCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
643
- const flushCode = failure instanceof NeedError || failure instanceof ModelConfigurationError ? failure.code : failure instanceof LlmError ? failure.failure.code : 'CONVERSATION_DETAILS_FAILED';
644
- 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') }) };
645
- }
681
+ reporting.push(failure);
646
682
  }
647
683
  if (activeMapper)
648
684
  yield* activeMapper.flush();
649
- if (reportingFailure !== undefined)
650
- throw reportingFailure.cause;
651
- throw error;
685
+ const firstReason = request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason;
686
+ if (reporting.length) {
687
+ turnFailure = withFailureEvidence(error, reporting, 'diagnostic', firstReason, failureCode);
688
+ throw turnFailure;
689
+ }
690
+ turnFailure = firstReason && !(error instanceof NeedError || error instanceof ModelConfigurationError || error instanceof LlmError) ? withFailureEvidence(error, [], 'diagnostic', firstReason, failureCode) : withFirstFailureEvidence(error, firstReason, failureCode);
691
+ throw turnFailure;
652
692
  }
653
693
  finally {
654
694
  for (const off of abortOffs)
@@ -678,6 +718,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
678
718
  if (succeeded && key)
679
719
  this.preparationFailures.delete(key);
680
720
  }
721
+ catch (cleanup) {
722
+ throw withFailureEvidence(turnFailure ?? cleanup, [cleanup], 'cleanup', request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason, failureCode);
723
+ }
681
724
  finally {
682
725
  if (key && this.turns.get(key) === turn)
683
726
  this.turns.delete(key);
@@ -697,6 +740,7 @@ export class RemoteAgentsAdapter extends LlmAdapter {
697
740
  providerInfo(provider) { return this.adapter(provider).providerInfo(provider); }
698
741
  listModels(provider) { return this.adapter(provider).listModels(provider); }
699
742
  resolveModel(provider, model, signal) { return this.adapter(provider).resolveModel(provider, model, signal); }
743
+ prepareCall(provider, model, signal) { return this.adapter(provider).prepareCall(provider, model, signal); }
700
744
  observeModelInvalidation(fields, changes = []) { for (const adapter of this.adapters.values())
701
745
  adapter.observeModelInvalidation(fields, changes); }
702
746
  async invalidateModelState() { await Promise.all([...this.adapters.values()].map(adapter => adapter.invalidateModelState())); }
@@ -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;
@@ -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 = JSON.stringify({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets });
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; for (const listener of listeners)
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 = signal ? AbortSignal.any([signal, own.controller.signal]) : own.controller.signal;
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
- return { target: own.target, models, signal: combined, assertCurrent, release, onInformation: listener => { if (released)
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.
@@ -0,0 +1,13 @@
1
+ import type { LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
2
+ import { type CatalogLease, type HostModel } from './model-catalog.ts';
3
+ /** One displayed directory, never an execution/admission cache. No clock or TTL. */
4
+ export declare class ModelPresentation {
5
+ private snapshot?;
6
+ remember(catalog: CatalogLease): void;
7
+ resolve(provider: string, id: string): (LlmResolvedModelInfo & {
8
+ catalog: HostModel;
9
+ }) | undefined;
10
+ clear(): void;
11
+ }
12
+ /** Exact saved identity for display only: no invented catalog item or capabilities. */
13
+ export declare function unavailableModelInfo(provider: string, id: string): LlmResolvedModelInfo;
@@ -0,0 +1,18 @@
1
+ import { modelInfo } from "./model-catalog.js";
2
+ /** One displayed directory, never an execution/admission cache. No clock or TTL. */
3
+ export class ModelPresentation {
4
+ snapshot;
5
+ remember(catalog) { catalog.assertCurrent(); this.snapshot = { models: structuredClone(catalog.models), assertCurrent: catalog.assertCurrent }; }
6
+ resolve(provider, id) {
7
+ const snapshot = this.snapshot, model = snapshot?.models.find(row => row.id === id);
8
+ if (!model)
9
+ return;
10
+ snapshot.assertCurrent();
11
+ return modelInfo(provider, structuredClone(model));
12
+ }
13
+ clear() { this.snapshot = undefined; }
14
+ }
15
+ /** Exact saved identity for display only: no invented catalog item or capabilities. */
16
+ export function unavailableModelInfo(provider, id) {
17
+ return { provider, id, name: id === 'remote' ? '旧版选择已不可用,请重新选择模型' : '所选模型已不可用,请重新选择模型', description: '当前主机目录中没有这个选择;请在模型菜单重新选择后再发送。' };
18
+ }
@@ -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 Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '本轮连接前诊断未能保存,已停止。', '原错误代码 ' + code + ';诊断错误代码 ' + preflightCode(diagnostic)), { cause: new AggregateError([error, diagnostic], 'PREFLIGHT_AND_DIAGNOSTIC_FAILED') });
100
+ throw withFailureEvidence(error, [diagnostic], 'diagnostic', undefined, preflightCode(error));
100
101
  }
101
102
  },
102
103
  forPreparation(sessionId, context) {
@@ -14,6 +14,9 @@ export function employeeFailure(code, brand = 'codex') {
14
14
  LOCAL_USER_TURN_CHANGED: '本次输入已变化,本轮已停止,请确认对话后重新发送。',
15
15
  LOCAL_USER_TURN_UNCONFIRMED: '无法核对本次员工输入,本轮已停止;请在当前会话重新发送。',
16
16
  LOCAL_USER_TURN_ACCEPTANCE_INVALID: '本次输入的核验回执不一致,本轮已停止,请联系管理员。',
17
+ MODEL_CATALOG_REVOKED: '所选型号已不在当前主机目录中,本轮已停止;请在模型菜单核对选择。',
18
+ MODEL_CATALOG_SCOPE_CHANGED: '远程主机连接或可信目标已改变,本轮已停止;原输入没有自动重发。',
19
+ CONVERSATION_DETAILS_FAILED: '本轮已停止,技术详情保存未确认;下方分别列出运行与保存原因。',
17
20
  TECHNICAL_DETAILS_UNAVAILABLE: '技术详情暂不可用,请更新远程主机连接组件。',
18
21
  REMOTE_DETAIL_STORAGE_FAILED: '技术详情保存失败,本轮已停止,请联系管理员检查。',
19
22
  REMOTE_DETAIL_DELIVERY_UNKNOWN: '技术详情的持久结果无法核对,本轮已停止。',
@@ -1,3 +1,4 @@
1
+ import { closeCode, withFailureEvidence } from "./close-diagnostics.js";
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
3
  import { constants } from 'node:fs';
3
4
  import { mkdir, open, lstat, realpath, readdir } from 'node:fs/promises';
@@ -362,40 +363,67 @@ export function createRemoteDetails(options) {
362
363
  scope.maximum = sequence;
363
364
  const detail = Object.freeze({ ...safe, payload: wire(safe.payload), version: 1, eventId: randomUUID(), instanceId: request.connection.instanceId, epoch: request.connection.epoch, sessionId: request.sessionId, provider: request.provider, turn, step, sequence, kind: kind(observation) });
364
365
  enqueue(async () => {
365
- // A private X journal is retained even if L is disconnected or rejects it.
366
+ // Pending is not a delivery verdict. Keep the exact failed stage separately.
366
367
  const file = join(dir, sequence + '-' + detail.eventId + '.json');
368
+ let phase = 'journal', appendAttempted = false, outcomeKnown = false;
367
369
  try {
368
- await durableWrite(file, { state: 'pending', detail });
369
- }
370
- catch {
371
- throw failure('REMOTE_DETAIL_STORAGE_FAILED');
372
- }
373
- stable();
374
- let response;
375
- try {
376
- response = await (localScope === undefined ? api.appendRemoteConversationDetail(structuredClone(detail)) : api.appendRemoteConversationDetail(structuredClone(detail), localScope));
377
- }
378
- catch {
370
+ try {
371
+ await durableWrite(file, { state: 'pending', detail });
372
+ }
373
+ catch {
374
+ throw failure('REMOTE_DETAIL_STORAGE_FAILED');
375
+ }
376
+ phase = 'append-scope';
379
377
  stable();
380
- const receipt = unwrap(await api.getRemoteConversationDetailReceipt({ eventId: detail.eventId, sessionId: detail.sessionId, provider: detail.provider }));
378
+ let response;
379
+ try {
380
+ phase = 'append';
381
+ appendAttempted = true;
382
+ response = await (localScope === undefined ? api.appendRemoteConversationDetail(structuredClone(detail)) : api.appendRemoteConversationDetail(structuredClone(detail), localScope));
383
+ }
384
+ catch {
385
+ phase = 'receipt-scope';
386
+ stable();
387
+ phase = 'receipt';
388
+ const receipt = unwrap(await api.getRemoteConversationDetailReceipt({ eventId: detail.eventId, sessionId: detail.sessionId, provider: detail.provider }));
389
+ phase = 'receipt-scope';
390
+ stable();
391
+ if (receipt.eventId !== detail.eventId)
392
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
393
+ if (!receipt.recorded)
394
+ throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
395
+ if (!isDeepStrictEqual(receipt.detail, detail))
396
+ throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
397
+ phase = 'settlement';
398
+ await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
399
+ outcomeKnown = true;
400
+ return;
401
+ }
402
+ phase = 'ack-scope';
381
403
  stable();
382
- if (receipt.eventId !== detail.eventId)
383
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
384
- if (!receipt.recorded)
385
- throw failure('REMOTE_DETAIL_DELIVERY_UNKNOWN');
386
- if (!isDeepStrictEqual(receipt.detail, detail))
387
- throw failure('REMOTE_DETAIL_RECEIPT_INVALID');
404
+ if (response?.ok === false) {
405
+ phase = 'settlement';
406
+ await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'rejected', code: response.error.code });
407
+ outcomeKnown = true;
408
+ }
409
+ const ack = unwrap(response);
410
+ if (ack.eventId !== detail.eventId || ack.recorded !== true)
411
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
412
+ phase = 'settlement';
388
413
  await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
389
- return;
414
+ outcomeKnown = true;
390
415
  }
391
- stable();
392
- if (response?.ok === false) {
393
- await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'rejected', code: response.error.code });
416
+ catch (error) {
417
+ if (!outcomeKnown) {
418
+ try {
419
+ await durableWrite(join(dir, 'unconfirmed-' + detail.eventId + '.json'), { version: 1, eventId: detail.eventId, state: 'unconfirmed', phase, appendAttempted, code: closeCode(error, 'REMOTE_DETAIL_STORAGE_FAILED') });
420
+ }
421
+ catch {
422
+ throw withFailureEvidence(error, [failure('REMOTE_DETAIL_STORAGE_FAILED')], 'diagnostic');
423
+ }
424
+ }
425
+ throw error;
394
426
  }
395
- const ack = unwrap(response);
396
- if (ack.eventId !== detail.eventId || ack.recorded !== true)
397
- throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
398
- await durableWrite(join(dir, 'settled-' + detail.eventId + '.json'), { eventId: detail.eventId, state: 'recorded' });
399
427
  });
400
428
  },
401
429
  async flush() { await tail; if (firstFailure)
@@ -1,9 +1,10 @@
1
+ import { withFailureEvidence } from "./failure-evidence.js";
1
2
  import { isDeepStrictEqual } from 'node:util';
2
3
  import { sanitizeDisplayLabel } from "./conversation-details.js";
3
4
  import { NeedError } from "./remote.js";
4
5
  const fail = (code) => new NeedError(code, '工作区来源尚未确认,本轮未执行;你的原话仍保留。', '当前工作区来源及原始员工输入');
5
6
  const codeOf = (value) => value instanceof NeedError ? value.code : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
6
- const diagnosticFailure = (error, diagnostic) => Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '工作区来源诊断未能保存,本轮已停止;原话保留。', '原错误代码 ' + codeOf(error) + ';诊断错误代码 ' + codeOf(diagnostic)), { cause: new AggregateError([error, diagnostic], 'WORKSPACE_SOURCE_AND_DIAGNOSTIC_FAILED') });
7
+ const diagnosticFailure = (error, diagnostic) => withFailureEvidence(error, [diagnostic], 'diagnostic', undefined, codeOf(error));
7
8
  /** Explicit human source transition only. No source selection is inferred from a folder path. */
8
9
  export async function confirmLegacyWorkspaceSource(get, api, request, hooks) {
9
10
  let initialContext = { workspaceId: null, bindingId: null, source: null, status: null };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-workbench/remote-agent",
3
- "version": "0.8.14",
3
+ "version": "0.8.16",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",