@ai-workbench/remote-agent 0.8.15 → 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 +6 -4
- package/dist/acp.js +1 -1
- package/dist/close-diagnostics.d.ts +1 -6
- package/dist/close-diagnostics.js +4 -34
- package/dist/conversation-details.js +1 -1
- package/dist/employee-provider.js +12 -4
- package/dist/failure-evidence.d.ts +12 -0
- package/dist/failure-evidence.js +83 -0
- package/dist/index.js +25 -20
- package/dist/model-catalog.d.ts +11 -1
- package/dist/model-catalog.js +35 -7
- package/dist/preflight-diagnostic.js +4 -3
- package/dist/presentation.js +3 -0
- package/dist/remote-details.js +55 -27
- package/dist/workspace-source.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
**`@ai-workbench/remote-agent@0.8.
|
|
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.
|
|
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组件联测;编排已回证0.8.14 Claude/Opus5/Default在leo本地目录首轮写文件成功;该回证不覆盖本0.8.
|
|
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
|
|
|
@@ -76,7 +76,7 @@ D21(0.8.5):两个品牌的官方模型菜单从 L 的 `modelCatalogTarget:
|
|
|
76
76
|
|
|
77
77
|
每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名每轮须有精确的模型配置成功回执,并由同一员工主机、同一连接代际的目录唯一映射到具体型号。具体型号注明来自主机目录,会话未单独回报;旧恢复记录不作本轮证据。目录缺项、映射不唯一、回执不符或身份/代际变化时不发送输入。所选值、远端确认值及型号来源仅在恢复标识与技术详情保留,插件不向真实回复追加技术参数,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
|
|
78
78
|
|
|
79
|
-
0.8.5统一包原依赖L `~0.7.13`;当前0.8.
|
|
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
|
|
|
@@ -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.
|
|
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);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
4
|
-
export
|
|
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
|
|
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);
|
|
@@ -7,7 +7,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
9
|
import { CODEX_EMPLOYEE_SCOPE_NOTICE } from "./employee-presentation.js";
|
|
10
|
-
import { CloseDiagnostics, closeCode, closeErrorCodes } from "./close-diagnostics.js";
|
|
10
|
+
import { CloseDiagnostics, withFailureEvidence, closeCode, closeErrorCodes } from "./close-diagnostics.js";
|
|
11
11
|
import { createHash } from 'node:crypto';
|
|
12
12
|
import { admitEmployeeLocation } from "./employee-location.js";
|
|
13
13
|
import { EmployeeExecution } from "./employee-exec.js";
|
|
@@ -336,13 +336,21 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
336
336
|
return runtime;
|
|
337
337
|
}
|
|
338
338
|
catch (error) {
|
|
339
|
-
|
|
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
|
+
}
|
|
340
346
|
try {
|
|
341
347
|
await details?.flush();
|
|
342
348
|
}
|
|
343
|
-
catch {
|
|
344
|
-
|
|
349
|
+
catch (failure) {
|
|
350
|
+
reporting.push(failure);
|
|
345
351
|
}
|
|
352
|
+
if (reporting.length)
|
|
353
|
+
throw withFailureEvidence(error, reporting, 'diagnostic', closeDiagnostics?.first?.reason);
|
|
346
354
|
throw error;
|
|
347
355
|
}
|
|
348
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.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';
|
|
@@ -254,10 +254,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
254
254
|
}
|
|
255
255
|
catch (error) {
|
|
256
256
|
if (error instanceof ModelConfigurationError)
|
|
257
|
-
throw new LlmError(error.message, error.code, { cause: error });
|
|
257
|
+
throw new LlmError(error.message + (failureEvidenceSummary(error) ? ';' + failureEvidenceSummary(error) : ''), error.code, { cause: error });
|
|
258
258
|
if (error instanceof NeedError) {
|
|
259
259
|
const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
|
|
260
|
-
const
|
|
260
|
+
const evidence = failureEvidenceSummary(error);
|
|
261
|
+
const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '') + (evidence ? ';' + evidence : '');
|
|
261
262
|
const login = originalLoginFailure(error);
|
|
262
263
|
let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
|
|
263
264
|
if (login?.technical && this.reportLoginFailure) {
|
|
@@ -271,6 +272,8 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
271
272
|
}
|
|
272
273
|
throw new LlmError(message, error.code, { cause: new Error(`诊断代码:${error.code};${error.requires}` + (diagnostic ? ';' + diagnostic : '')) });
|
|
273
274
|
}
|
|
275
|
+
if (error instanceof LlmError && failureEvidenceSummary(error))
|
|
276
|
+
throw new LlmError(error.failure.message + ';' + failureEvidenceSummary(error), error.code, { cause: error });
|
|
274
277
|
throw error;
|
|
275
278
|
}
|
|
276
279
|
}
|
|
@@ -303,6 +306,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
303
306
|
let catalog;
|
|
304
307
|
let selected;
|
|
305
308
|
let modelConfiguration;
|
|
309
|
+
let turnFailure;
|
|
306
310
|
let employeeNoticeShown = false;
|
|
307
311
|
const information = [];
|
|
308
312
|
const recordInformation = () => { if (runtime?.details)
|
|
@@ -401,8 +405,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
401
405
|
throw new ModelConfigurationError('所选模型不在本次可信主机目录中,请重新选择模型');
|
|
402
406
|
if (options.reasoningEffort !== undefined && !selected.reasoning.options.some(option => option.value === options.reasoningEffort))
|
|
403
407
|
throw new ModelConfigurationError('所选推理等级不在本次模型目录中');
|
|
408
|
+
catalog.bindSelection?.(selected.id, options.reasoningEffort);
|
|
404
409
|
const observeSignal = (source, initiator, reason) => { const observed = () => { const diagnostic = request?.closeDiagnostics ?? runtime?.closeDiagnostics; if (diagnostic)
|
|
405
|
-
diagnostic.record({ layer: 'adapter', initiator, reason, action: 'abort', changes: [{ field: 'adapter.turn-aborted', before: false, after: source.aborted }] }); }; source.addEventListener('abort', observed, { once: true }); abortOffs.push(() => source.removeEventListener('abort', observed)); };
|
|
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)); };
|
|
406
411
|
observeSignal(catalog.signal, 'signal.model-catalog', 'MODEL_CATALOG_REVOKED');
|
|
407
412
|
observeSignal(signal, 'signal.turn-composite', 'ABORTED');
|
|
408
413
|
let modelSignal = AbortSignal.any([signal, catalog.signal]);
|
|
@@ -568,7 +573,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
568
573
|
const resolved = mapping[0].resolvedModel;
|
|
569
574
|
if (typeof resolved !== 'string' || !resolved.trim())
|
|
570
575
|
throw new NeedError('MODEL_RESOLUTION_UNCONFIRMED', '主机目录没有提供所选模型的具体型号,本轮未发送', 'CATALOG_RESOLVED_MODEL_MISSING');
|
|
571
|
-
modelConfiguration = { selected: { ...
|
|
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 } : {}) };
|
|
572
577
|
details.record({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_CONFIRMED', summary: '本轮选择已由远端配置回执确认;' + modelResolutionLabel(modelConfiguration) + ';不代表推理效果已验证', payload: modelConfiguration });
|
|
573
578
|
}
|
|
574
579
|
catch (error) {
|
|
@@ -652,41 +657,38 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
652
657
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
653
658
|
}
|
|
654
659
|
catch (error) {
|
|
660
|
+
turnFailure = error;
|
|
655
661
|
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
656
662
|
recordInformation();
|
|
657
663
|
failureCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.code : 'TURN_PREPARATION_FAILED';
|
|
658
664
|
if (live && session && session.promptDispatchCount > promptBaseline)
|
|
659
665
|
live.failure = { cause: error };
|
|
660
|
-
|
|
666
|
+
const reporting = [];
|
|
661
667
|
if (request?.recordEarlyFailure) {
|
|
662
668
|
const record = request.recordEarlyFailure;
|
|
663
669
|
request.recordEarlyFailure = undefined;
|
|
664
|
-
const code = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : signal.aborted ? 'ABORTED' : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
|
|
665
670
|
try {
|
|
666
|
-
await record(Object.assign(new NeedError(
|
|
671
|
+
await record(Object.assign(new NeedError(failureCode ?? 'TURN_PREPARATION_FAILED', '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
|
|
667
672
|
}
|
|
668
673
|
catch (failure) {
|
|
669
|
-
|
|
674
|
+
reporting.push(failure);
|
|
670
675
|
}
|
|
671
676
|
}
|
|
672
677
|
try {
|
|
673
678
|
await runtime?.details?.flush();
|
|
674
679
|
}
|
|
675
680
|
catch (failure) {
|
|
676
|
-
|
|
677
|
-
reportingFailure = { cause: failure };
|
|
678
|
-
else {
|
|
679
|
-
const first = reportingFailure.cause;
|
|
680
|
-
const originalCode = error instanceof NeedError || error instanceof ModelConfigurationError ? error.code : error instanceof LlmError ? error.failure.code : 'WORKSPACE_SOURCE_CONFIRMATION_FAILED';
|
|
681
|
-
const flushCode = failure instanceof NeedError || failure instanceof ModelConfigurationError ? failure.code : failure instanceof LlmError ? failure.failure.code : 'CONVERSATION_DETAILS_FAILED';
|
|
682
|
-
reportingFailure = { cause: Object.assign(new NeedError('CONVERSATION_DETAILS_FAILED', '本轮诊断保存及详情收尾失败,已停止。', '原错误代码 ' + originalCode + ';' + (first instanceof NeedError ? first.requires : '早期诊断保存失败') + ';详情收尾代码 ' + flushCode), { cause: new AggregateError([error, first, failure], 'ADMISSION_DIAGNOSTIC_AND_FLUSH_FAILED') }) };
|
|
683
|
-
}
|
|
681
|
+
reporting.push(failure);
|
|
684
682
|
}
|
|
685
683
|
if (activeMapper)
|
|
686
684
|
yield* activeMapper.flush();
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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;
|
|
690
692
|
}
|
|
691
693
|
finally {
|
|
692
694
|
for (const off of abortOffs)
|
|
@@ -716,6 +718,9 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
716
718
|
if (succeeded && key)
|
|
717
719
|
this.preparationFailures.delete(key);
|
|
718
720
|
}
|
|
721
|
+
catch (cleanup) {
|
|
722
|
+
throw withFailureEvidence(turnFailure ?? cleanup, [cleanup], 'cleanup', request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason, failureCode);
|
|
723
|
+
}
|
|
719
724
|
finally {
|
|
720
725
|
if (key && this.turns.get(key) === turn)
|
|
721
726
|
this.turns.delete(key);
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -20,7 +20,14 @@ export interface HostModel {
|
|
|
20
20
|
};
|
|
21
21
|
source: unknown;
|
|
22
22
|
}
|
|
23
|
+
export interface CatalogRevocation {
|
|
24
|
+
readonly fields: readonly CatalogChangeField[];
|
|
25
|
+
readonly changes: readonly CloseChange[];
|
|
26
|
+
readonly reason?: 'MODEL_CATALOG_REVOKED' | 'MODEL_CATALOG_SCOPE_CHANGED';
|
|
27
|
+
}
|
|
23
28
|
export interface CatalogLease {
|
|
29
|
+
revocation?(): CatalogRevocation | undefined;
|
|
30
|
+
bindSelection?(id: string, effort?: string): void;
|
|
24
31
|
readonly target: ModelCatalogTarget;
|
|
25
32
|
readonly models: readonly HostModel[];
|
|
26
33
|
readonly signal: AbortSignal;
|
|
@@ -45,7 +52,7 @@ export declare function catalogDiagnostic(issues: readonly CatalogIssue[]): stri
|
|
|
45
52
|
/** Defaults are provider declarations, never catalog order or display names. */
|
|
46
53
|
export declare function declaredDefault(agent: AgentDefinition, models: readonly HostModel[]): HostModel | undefined;
|
|
47
54
|
export declare function parseHostCatalog(input: string, provider: string, username: string): ParsedCatalog;
|
|
48
|
-
export type CatalogChangeField = 'api-instance' | 'connection-instance' | 'connection-epoch' | 'connection-phase' | 'connection-need-code' | 'catalog-capability' | 'codex-target' | 'claude-target' | 'other-metadata';
|
|
55
|
+
export type CatalogChangeField = 'api-instance' | 'connection-instance' | 'connection-epoch' | 'connection-phase' | 'connection-need-code' | 'catalog-capability' | 'codex-target' | 'claude-target' | 'other-metadata' | 'selected-model';
|
|
49
56
|
/** Share only pending work; settled results are never cached. */
|
|
50
57
|
export declare class ModelCatalog implements ModelDirectory {
|
|
51
58
|
private get;
|
|
@@ -62,10 +69,12 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
62
69
|
private disposed;
|
|
63
70
|
private initialized;
|
|
64
71
|
private active;
|
|
72
|
+
private revocations;
|
|
65
73
|
private presentation?;
|
|
66
74
|
private presentationNeedCode?;
|
|
67
75
|
private information;
|
|
68
76
|
private flights;
|
|
77
|
+
private selections;
|
|
69
78
|
private serial;
|
|
70
79
|
private latest;
|
|
71
80
|
constructor(get: () => ConnectApi | undefined, maxBytes: () => number | undefined, run?: CatalogRunner, changed?: () => void, diagnostic?: (provider: 'claude' | 'codex', message: string | undefined) => void, loaded?: (agent: AgentDefinition, models: readonly HostModel[], assertCurrent: () => void) => void | Promise<void>, revoking?: (fields: readonly CatalogChangeField[], changes: readonly CloseChange[]) => void);
|
|
@@ -73,6 +82,7 @@ export declare class ModelCatalog implements ModelDirectory {
|
|
|
73
82
|
private trusted;
|
|
74
83
|
private assertFlight;
|
|
75
84
|
private failure;
|
|
85
|
+
private checkSelections;
|
|
76
86
|
private start;
|
|
77
87
|
read(agent: AgentDefinition, signal?: AbortSignal): Promise<CatalogLease>;
|
|
78
88
|
dispose(): void;
|
package/dist/model-catalog.js
CHANGED
|
@@ -93,6 +93,7 @@ export function parseHostCatalog(input, provider, username) {
|
|
|
93
93
|
}
|
|
94
94
|
return freeze(structuredClone({ models, issues }));
|
|
95
95
|
}
|
|
96
|
+
function semanticJson(value) { const sort = (item) => Array.isArray(item) ? item.map(sort) : object(item) ? Object.fromEntries(Object.keys(item).sort().map(key => [key, sort(item[key])])) : item; return JSON.stringify(sort(value)); }
|
|
96
97
|
function changedCatalogFields(previous, next, switched) {
|
|
97
98
|
const before = previous ? JSON.parse(previous) : {}, after = JSON.parse(next), fields = [];
|
|
98
99
|
if (switched)
|
|
@@ -148,10 +149,12 @@ export class ModelCatalog {
|
|
|
148
149
|
disposed = false;
|
|
149
150
|
initialized = false;
|
|
150
151
|
active = new Set();
|
|
152
|
+
revocations = new WeakMap();
|
|
151
153
|
presentation;
|
|
152
154
|
presentationNeedCode;
|
|
153
155
|
information = new Set();
|
|
154
156
|
flights = new Map();
|
|
157
|
+
selections = new Set();
|
|
155
158
|
serial = 0;
|
|
156
159
|
latest = new Map();
|
|
157
160
|
constructor(get, maxBytes, run = runCatalog, changed = () => { }, diagnostic = () => { }, loaded = () => { }, revoking = () => { }) {
|
|
@@ -172,7 +175,7 @@ export class ModelCatalog {
|
|
|
172
175
|
// L's same-epoch recovery wording is presentation metadata, not an execution change.
|
|
173
176
|
const executionState = state && { instanceId: state.instanceId, epoch: state.epoch, phase: state.phase };
|
|
174
177
|
const executionTargets = targets?.map(result => result.ok ? result : { ok: false, code: result.error.code });
|
|
175
|
-
const signature =
|
|
178
|
+
const signature = semanticJson({ state: executionState, capability: info?.modelCatalogTarget, targets: executionTargets });
|
|
176
179
|
const switched = api !== this.api;
|
|
177
180
|
const changed = this.initialized && (switched || signature !== this.signature);
|
|
178
181
|
const presentation = createHash('sha256').update(JSON.stringify({ need: { code: state?.need?.code, message: state?.need?.message, requires: state?.need?.requires }, recovery: state?.recovery && { phase: state.recovery.phase, message: state.recovery.message, startedAt: state.recovery.startedAt, administratorAfterMs: state.recovery.administratorAfterMs } })).digest('hex');
|
|
@@ -202,8 +205,10 @@ export class ModelCatalog {
|
|
|
202
205
|
this.revoking(changedFields, changes);
|
|
203
206
|
this.revision++;
|
|
204
207
|
this.latest.clear();
|
|
205
|
-
for (const controller of this.active)
|
|
208
|
+
for (const controller of this.active) {
|
|
209
|
+
this.revocations.set(controller, freeze({ fields: changedFields, changes, reason: 'MODEL_CATALOG_SCOPE_CHANGED' }));
|
|
206
210
|
controller.abort();
|
|
211
|
+
}
|
|
207
212
|
this.active.clear();
|
|
208
213
|
this.diagnostic('claude', undefined);
|
|
209
214
|
this.diagnostic('codex', undefined);
|
|
@@ -242,6 +247,22 @@ export class ModelCatalog {
|
|
|
242
247
|
// Each consumer needs its own Error identity for its original input/signal proof.
|
|
243
248
|
return new CatalogError(error instanceof CatalogError || error instanceof ModelConfigurationError ? error.message.replace(/^无法获取模型列表:/, '') : '目录查询失败,请到设置 → 模型查看诊断。', error instanceof CatalogError ? error.diagnostic : error instanceof CatalogSshError ? error.diagnostic : error instanceof Error ? `${error instanceof LlmError ? error.code : 'MODEL_CATALOG_QUERY_FAILED'}:${safe(error.message)}` : 'MODEL_CATALOG_QUERY_FAILED:目录查询失败,根因未查明');
|
|
244
249
|
}
|
|
250
|
+
checkSelections(agent, api, target, models, serial) {
|
|
251
|
+
for (const lease of this.selections) {
|
|
252
|
+
const chosen = lease.selected, old = lease.flight.target;
|
|
253
|
+
if (lease.controller.signal.aborted || lease.flight.serial > serial || (lease.observed?.serial ?? 0) > serial || lease.flight.api !== api || old.provider !== agent.loginProvider || old.instanceId !== target.instanceId || old.epoch !== target.epoch || !sameTarget(old.target, target.target))
|
|
254
|
+
continue;
|
|
255
|
+
lease.observed = { serial, models };
|
|
256
|
+
if (!chosen)
|
|
257
|
+
continue;
|
|
258
|
+
const current = models.find(model => model.id === chosen.id && model.value === chosen.value);
|
|
259
|
+
if (current)
|
|
260
|
+
continue;
|
|
261
|
+
const changes = [{ field: 'catalog.' + agent.loginProvider + '.selected-model-valid', before: true, after: false }];
|
|
262
|
+
this.revocations.set(lease.controller, freeze({ fields: ['selected-model'], changes, reason: 'MODEL_CATALOG_REVOKED' }));
|
|
263
|
+
lease.controller.abort();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
245
266
|
start(agent, api, target, max, key) {
|
|
246
267
|
const deferred = Promise.withResolvers(), controller = new AbortController();
|
|
247
268
|
const flight = { key, api, target, revision: this.revision, serial: ++this.serial, controller, pending: true, users: 0, promise: deferred.promise };
|
|
@@ -255,6 +276,7 @@ export class ModelCatalog {
|
|
|
255
276
|
const output = await this.run(target.target, '/usr/local/aw-host/bin/aw-model-catalog --provider ' + agent.loginProvider, controller.signal, max);
|
|
256
277
|
this.assertFlight(flight);
|
|
257
278
|
const parsed = parseHostCatalog(output, agent.loginProvider, target.target.username), diagnostic = catalogDiagnostic(parsed.issues);
|
|
279
|
+
this.checkSelections(agent, api, target, parsed.models, flight.serial);
|
|
258
280
|
if (!parsed.models.length)
|
|
259
281
|
fail('当前没有可选模型,请到设置 → 模型查看诊断。', diagnostic ?? '主机返回的完整模型目录为空(诊断代码:MODEL_CATALOG_EMPTY)');
|
|
260
282
|
if (current())
|
|
@@ -278,10 +300,12 @@ export class ModelCatalog {
|
|
|
278
300
|
return flight;
|
|
279
301
|
}
|
|
280
302
|
async read(agent, signal) {
|
|
281
|
-
let flight, released = false;
|
|
303
|
+
let flight, released = false, selection;
|
|
304
|
+
const selectedController = new AbortController();
|
|
282
305
|
const listeners = new Set();
|
|
283
306
|
const release = () => { if (released || !flight)
|
|
284
|
-
return; released = true;
|
|
307
|
+
return; released = true; if (selection)
|
|
308
|
+
this.selections.delete(selection); for (const listener of listeners)
|
|
285
309
|
this.information.delete(listener); listeners.clear(); flight.users--; if (!flight.users) {
|
|
286
310
|
if (flight.pending)
|
|
287
311
|
flight.controller.abort();
|
|
@@ -323,7 +347,7 @@ export class ModelCatalog {
|
|
|
323
347
|
}
|
|
324
348
|
flight ??= this.start(agent, api, target, max, key);
|
|
325
349
|
flight.users++;
|
|
326
|
-
const own = flight, combined =
|
|
350
|
+
const own = flight, combined = AbortSignal.any([own.controller.signal, selectedController.signal, ...(signal ? [signal] : [])]);
|
|
327
351
|
const assertCurrent = () => { if (combined.aborted)
|
|
328
352
|
fail('目录查询已取消'); this.assertFlight(own); };
|
|
329
353
|
const models = await new Promise((resolve, reject) => {
|
|
@@ -334,7 +358,11 @@ export class ModelCatalog {
|
|
|
334
358
|
abort();
|
|
335
359
|
});
|
|
336
360
|
assertCurrent();
|
|
337
|
-
|
|
361
|
+
selection = { flight: own, controller: selectedController };
|
|
362
|
+
this.selections.add(selection);
|
|
363
|
+
return { target: own.target, get models() { return selection?.observed?.models ?? models; }, signal: combined, assertCurrent, release, bindSelection: (id, effort) => { assertCurrent(); if (released)
|
|
364
|
+
fail('模型目录租约已经释放'); const observed = selection.observed?.models ?? models, model = observed.find(model => model.id === id); if (!model || effort !== undefined && !model.reasoning.options.some(option => option.value === effort))
|
|
365
|
+
fail('所选型号或推理等级已不在当前主机目录中,请重新选择'); selection.selected = { id: model.id, value: model.value }; }, revocation: () => this.revocations.get(selectedController) ?? this.revocations.get(own.controller), onInformation: listener => { if (released)
|
|
338
366
|
return () => { }; listeners.add(listener); this.information.add(listener); return () => { listeners.delete(listener); this.information.delete(listener); }; } };
|
|
339
367
|
}
|
|
340
368
|
catch (error) {
|
|
@@ -346,7 +374,7 @@ export class ModelCatalog {
|
|
|
346
374
|
}
|
|
347
375
|
}
|
|
348
376
|
dispose() { this.disposed = true; this.unsubscribe?.(); this.flights.clear(); for (const controller of this.active)
|
|
349
|
-
controller.abort(); this.active.clear(); this.information.clear(); }
|
|
377
|
+
controller.abort(); this.active.clear(); this.selections.clear(); this.information.clear(); }
|
|
350
378
|
}
|
|
351
379
|
export function modelInfo(provider, model) {
|
|
352
380
|
// Official rc.2 represents no reasoning levels by absence, and rejects an empty efforts array.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withFailureEvidence, failureEvidenceSummary, primaryFailureCode } from "./close-diagnostics.js";
|
|
1
2
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
2
3
|
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
3
4
|
import { isDeepStrictEqual } from 'node:util';
|
|
@@ -91,12 +92,12 @@ export function createPreflightDiagnostics(options) {
|
|
|
91
92
|
if (request.preflightFailureRecorded)
|
|
92
93
|
return;
|
|
93
94
|
request.preflightFailureRecorded = true;
|
|
94
|
-
const scope = scopeFor(request), code = preflightCode(error);
|
|
95
|
+
const scope = scopeFor(request), code = primaryFailureCode(error, preflightCode(error));
|
|
95
96
|
try {
|
|
96
|
-
await record(request, provider, { kind: 'need', severity: 'action-required', code, summary: '本轮在连接前准备阶段停止;原始请求保留,未重发。', payload: { phase: 'workspace-source-admission', stage, ...scope.source, outcome: code === 'ABORTED' ? 'cancelled' : 'failed' } });
|
|
97
|
+
await record(request, provider, { kind: 'need', severity: 'action-required', code, summary: '本轮在连接前准备阶段停止;原始请求保留,未重发。' + failureEvidenceSummary(error), payload: { phase: 'workspace-source-admission', stage, ...scope.source, outcome: code === 'ABORTED' ? 'cancelled' : 'failed' } });
|
|
97
98
|
}
|
|
98
99
|
catch (diagnostic) {
|
|
99
|
-
throw
|
|
100
|
+
throw withFailureEvidence(error, [diagnostic], 'diagnostic', undefined, preflightCode(error));
|
|
100
101
|
}
|
|
101
102
|
},
|
|
102
103
|
forPreparation(sessionId, context) {
|
package/dist/presentation.js
CHANGED
|
@@ -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: '技术详情的持久结果无法核对,本轮已停止。',
|
package/dist/remote-details.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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 (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
414
|
+
outcomeKnown = true;
|
|
390
415
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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)
|
package/dist/workspace-source.js
CHANGED
|
@@ -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) =>
|
|
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 };
|