@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 +8 -6
- package/dist/acp.js +1 -1
- package/dist/claude-local.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-presentation.d.ts +6 -0
- package/dist/employee-presentation.js +16 -0
- package/dist/employee-provider.js +14 -5
- package/dist/failure-evidence.d.ts +12 -0
- package/dist/failure-evidence.js +83 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.js +76 -32
- package/dist/model-catalog.d.ts +11 -1
- package/dist/model-catalog.js +35 -7
- package/dist/model-presentation.d.ts +13 -0
- package/dist/model-presentation.js +18 -0
- 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
|
|
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
|
|
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
|
|
|
@@ -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
|
-
|
|
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.
|
|
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);
|
package/dist/claude-local.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: { ...
|
|
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
|
|
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
|
-
|
|
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(
|
|
671
|
+
await record(Object.assign(new NeedError(failureCode ?? 'TURN_PREPARATION_FAILED', '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
|
|
629
672
|
}
|
|
630
673
|
catch (failure) {
|
|
631
|
-
|
|
674
|
+
reporting.push(failure);
|
|
632
675
|
}
|
|
633
676
|
}
|
|
634
677
|
try {
|
|
635
678
|
await runtime?.details?.flush();
|
|
636
679
|
}
|
|
637
680
|
catch (failure) {
|
|
638
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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())); }
|
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.
|
|
@@ -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
|
|
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 };
|