@ai-workbench/remote-agent 0.8.16 → 0.8.18
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 +15 -5
- package/dist/acp.js +15 -4
- package/dist/claude-local.js +2 -0
- package/dist/connect-contract.d.ts +5 -4
- package/dist/connect-need.d.ts +12 -0
- package/dist/connect-need.js +34 -0
- package/dist/employee-location.js +2 -1
- package/dist/employee-provider.js +22 -13
- package/dist/employee-ssh.d.ts +1 -1
- package/dist/employee-ssh.js +1 -1
- package/dist/failure-evidence.d.ts +2 -0
- package/dist/failure-evidence.js +9 -1
- package/dist/host-workspace.d.ts +13 -0
- package/dist/host-workspace.js +110 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +76 -19
- package/dist/presentation.js +1 -0
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +124 -71
- package/dist/remote.d.ts +2 -0
- package/dist/remote.js +1 -0
- package/dist/workspace-source.js +2 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
**`@ai-workbench/remote-agent@0.8.
|
|
1
|
+
**`@ai-workbench/remote-agent@0.8.18` 发布形态包。继承0.8.17主机来源执行与L具体Need透传;本版修复Codex员工执行通道时序(需受保护aw.3)、去除逐RPC重复SSH,失败先交还官方界面并显示“正在确认主机清理”。通过官方插件 → 添加插件安装;固定包由编排发布,X不publish。工程/包证明与leo产品验收分别记录,未运行的门保持NOT_RUN。**
|
|
2
2
|
|
|
3
3
|
# 远程ACP插件
|
|
4
4
|
|
|
5
5
|
一个插件提供 **Claude(远程主机)** 和 **Codex(远程主机)**,通过员工主机上的官方CLI运行。Codex保留单次模式;Claude固定会话模式。只用官方模型列表、插件管理和配置入口,不提供自制面板、问答或登录按钮。登录和原生工作区绑定唯一归连接插件L。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
当前版本0.8.18,由X按编排裁定在X2的0.8.17上交付aw.3/去重复SSH/失败先交还;0.8.17的主机来源执行和L具体Need透传完整保留;包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;本轮按编排放行准备发布形态包,供 leo 真机自验;X 不执行实际发布,由编排统一发布。开发fixture逐字节保留L0.7.20固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的不完整实现会明确报缺项。依赖范围为`~0.7.20`,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,11 @@
|
|
|
12
12
|
|
|
13
13
|
## 员工路径
|
|
14
14
|
|
|
15
|
-
从官方插件管理安装,照常使用默认工作区/会话/模型入口。L0.7.
|
|
15
|
+
从官方插件管理安装,照常使用默认工作区/会话/模型入口。L沿唯一“远程主机连接”设置节记录文件来源;L0.7.20新选主机位置由两品牌按已核验的规范目录执行,旧legacy主机绑定仍沿原规则;本地来源接入N0.4.4/S0.4.11员工端点。旧映射可在本轮官方一次确认中选择改用本机文件夹;拒绝、取消或输入/来源变化时不切换、不执行。也可在L设置逐工作区确认沿用原主机关联。缺正式host解析能力时明确说明所缺组件、保留原话,本轮不派发。本地六类工具经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.18或Codex,产品验收以各版本leo实际结果为准。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
|
|
16
|
+
|
|
17
|
+
新主机位置允许当前SSH员工账号可访问的已选绝对目录、文件系统根目录、非Git或只读目录;不改写原projectRoot、不创建Git、不转本机同名文件夹。read/execute必须可核实,write=false不额外一刀切拒绝,实际写入服从主机系统权限。运行前必核来源UUID、连接代次、目录及链接入口dev/ino和权限;revision与内容增删导致的mtime/ctime变化不单独失效。此核对属于正确性条件,不可关闭。最后inspect到主机spawn之间仍有OS时序边界,不表示原子目录句柄租约。
|
|
18
|
+
|
|
19
|
+
文件来源或位置改变后,原会话不能改绑或重放,员工看到“文件来源已改,请新建会话继续”;原会话历史仍可查。L的具体Need保留message/actor/action和代码,员工正文显示原原因与实际文字动作;明确选择文件来源的动作给“设置 → 远程主机连接 → 工作区文件来源”,新建会话等动作保留原标签,不按错误码猜导航;不可用动作明确标记。技术原因officialReason不进正文,未提供按钮API时不编造可点击入口。
|
|
16
20
|
|
|
17
21
|
未登录时两个模型均回复:**需要登录,请到设置 → 远程主机连接 点登录**。无L或缺正式能力时直接说明缺项,不让员工填写主机、用户、路径或密钥。
|
|
18
22
|
|
|
@@ -64,7 +68,7 @@ D19(0.8.1):相邻正文增量合成连续文本块,工作区首次说明
|
|
|
64
68
|
技术详情由 L 0.7.11 的唯一“远程主机连接”设置节展示,正文只放模型文本。命令目录保留完整描述;未知事件和工具输入输出保留字段、JSON类型及处理结果,未获准原文明确脱敏,数字用量可核对。没有 `remoteConversationDetails:1` 时明确提示“技术详情暂不可用”,本轮不发送模型请求。X 在官方当前 profile 的 `ai-workbench-remote-agent/diagnostics/` 保存私有诊断日志(目录700、文件600、落盘同步);写入/归属/顺序/容量失败中止本轮,不自动重发、换代重投、过期或淘汰。结果未知时只读核对原事件完整回执,未找到仍保持未知。X本轮无新增设置/阈值;L的单事件容量和分页设置在L设置节可见。
|
|
65
69
|
|
|
66
70
|
|
|
67
|
-
0.8.2 登录检查原交付需要 L `~0.7.11`(本统一版peer为`~0.7.
|
|
71
|
+
0.8.2 登录检查原交付需要 L `~0.7.11`(本统一版peer为`~0.7.20`),失败来源及固定分支保留到 L“登录前技术详情”(`loginDiagnostics:1`),员工正文保持原文。按 L 明确的本品牌 queued/checking/authorizing 状态订阅等待;不以全局checking猜测在途,不新增刷新、轮询、超时或重试。每轮ensureReady仍是合同规定的显式检查,L的新可信结果可以替代旧检查,较早返回不得覆盖它。
|
|
68
72
|
|
|
69
73
|
L安全规范的officialReason原串完整保留;旧/不支持格式明确记OFFICIAL_REASON_UNSUPPORTED与字节数/SHA,秘密原文不保存。L0.7.11的LOGIN_INSTANCE_MISMATCH记录通过observedLoginInstanceId与原始login.instanceId保存实际观测ID,事件及connection仍绑定当前可信ID/代次;L技术详情和X私有日志均保留真实双ID,不归一、不清空快照。复合未知官方原因以固定summary完整保存OFFICIAL_REASON_UNSUPPORTED及bytes/SHA,不交原文。X私有原快照先同步落盘,再追加L;ACK未知只读核实原事件,跨代不重写/重发。此版本的工程验证不替代leo员工路径复验,真实故障根因尚未证实。
|
|
70
74
|
|
|
@@ -76,7 +80,7 @@ D21(0.8.5):两个品牌的官方模型菜单从 L 的 `modelCatalogTarget:
|
|
|
76
80
|
|
|
77
81
|
每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名每轮须有精确的模型配置成功回执,并由同一员工主机、同一连接代际的目录唯一映射到具体型号。具体型号注明来自主机目录,会话未单独回报;旧恢复记录不作本轮证据。目录缺项、映射不唯一、回执不符或身份/代际变化时不发送输入。所选值、远端确认值及型号来源仅在恢复标识与技术详情保留,插件不向真实回复追加技术参数,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
|
|
78
82
|
|
|
79
|
-
0.8.5统一包原依赖L `~0.7.13`;当前0.8.
|
|
83
|
+
0.8.5统一包原依赖L `~0.7.13`;0.8.16原对齐L `~0.7.19`;当前0.8.18对齐L `~0.7.20`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理,X新增设置0。
|
|
80
84
|
|
|
81
85
|
D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建会话上下文政策,官方 CLI 仍为未改的0.160.1。系统/开发者要求仅在新建时生效;恢复时与初建摘要相同则正常恢复且省略新建专用字段,不声称恢复接口会更新指令。要求不同或清空时,在同一真实回复说明“会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。”,并新建远端承接公开对话,保留旧标识和查询记录;旧/非法初建摘要明确拒绝,不能猜测。
|
|
82
86
|
|
|
@@ -126,3 +130,9 @@ D20(0.8.8开发记录):本地来源的Codex原生文件与命令通过固
|
|
|
126
130
|
0.8.14 不把空菜单、非穷举 picker、可选 UI category/label 或同值配置通知当作额外发送门槛;每轮仍要求可靠模型入口与 fresh exact setter ACK。本轮完成后的配置变化只记录 warning,保留真实回复与 finish,并明确未获单独的实际执行型号回报。同一身份、代际与可信目标的 needCode/诊断变化只记 info;真实身份、代际、目标或能力失效仍撤回。每次关闭/取消保留 caller、字段前后值和首次观测原因(不等于根因)。新增配置、阈值、超时、重试为 0。
|
|
127
131
|
|
|
128
132
|
0.8.16 不新增 Config、默认值、阈值、超时或重试。首因可见、旧代不重投、实际选择与绑定一致属于不可关闭的正确性不变量。目录完成结果不用于后续请求缓存;活跃租约仅保留更新观测的选择有效性证据,释放即撤登记。断连/错代仍保留原详情信封及未确认状态,仅按旧合同读 receipt 核验。`unconfirmed-<eventId>.json` 私有记录追加是否尝试及失败阶段/固定码,不能当作 L 的 ACK;员工可见原因沿官方错误消息,无新增面板。
|
|
133
|
+
|
|
134
|
+
### 本机执行的核验位置(不可关闭)
|
|
135
|
+
|
|
136
|
+
L接纳每轮真实输入时核对实际目录和N端点/代次。X只在该代执行通道open时向S核对一次target,并继续验证员工执行器固定SHA和官方版本;原生注册发生在open完成之后。之后broker输入及每个native RPC只核对既有内存连接、工作区绑定和接纳事实,撤销来自已订阅的L连接/位置、N网络事件和执行SSH关闭。N实例/代次改变会重新接纳并建立新通道,旧流不会跨代迁移。
|
|
137
|
+
|
|
138
|
+
这属于执行位置与已接纳输入的一致性不变量,没有关闭设置。失败先交还官方会话并显示“正在确认主机清理”,详情落盘及条件kill/list随后在原清理promise进行;同一会话仍由原cleanupFailure/未知送达门阻止不安全重发。没有新增超时、重试、模型限制或中止检查。技术详情事件EMPLOYEE_EXECUTION_VALIDATION_POLICY列出同一核验位置。
|
package/dist/acp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CloseDiagnostics } from "./close-diagnostics.js";
|
|
1
|
+
import { CloseDiagnostics, withFailureEvidence } from "./close-diagnostics.js";
|
|
2
2
|
import * as acp from '@agentclientprotocol/sdk/unstable';
|
|
3
3
|
import { NeedError } from "./remote.js";
|
|
4
4
|
import { employeeFailure } from "./presentation.js";
|
|
@@ -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.18' },
|
|
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);
|
|
@@ -609,8 +609,18 @@ export function acpNeed(error) {
|
|
|
609
609
|
if (typeof value !== 'string')
|
|
610
610
|
continue;
|
|
611
611
|
const code = value.split(':', 1)[0];
|
|
612
|
-
if (employeeAdapterCodes.has(code))
|
|
613
|
-
|
|
612
|
+
if (employeeAdapterCodes.has(code)) {
|
|
613
|
+
const failure = new NeedError(code, employeeFailure(code), `员工电脑执行适配器明确拒绝:${code};RPC 错误码 ${error.code}`);
|
|
614
|
+
if (code === 'AW_EMPLOYEE_ENVIRONMENT_REGISTRATION_FAILED' && value.startsWith(code + ': ')) {
|
|
615
|
+
const reason = value.slice(code.length + 2);
|
|
616
|
+
if (employeeAdapterCodes.has(reason) || employeeOpenCauseCodes.has(reason)) {
|
|
617
|
+
const retained = withFailureEvidence(Object.assign(failure, { cause: new NeedError(reason, '员工执行通道开通失败', '核对固定开通原因') }), [], 'diagnostic');
|
|
618
|
+
retained.requires += `;RPC 错误码 ${error.code}`;
|
|
619
|
+
return retained;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return failure;
|
|
623
|
+
}
|
|
614
624
|
}
|
|
615
625
|
}
|
|
616
626
|
if (error instanceof acp.RequestError && error.code === -32000)
|
|
@@ -619,6 +629,7 @@ export function acpNeed(error) {
|
|
|
619
629
|
return new NeedError('ACP_FAILED', '远端 ACP 请求失败,根因未查明;未切换到新会话', `有效的远端 ACP 连接;RPC 错误码 ${error.code}`);
|
|
620
630
|
return new NeedError('ACP_FAILED', '远端 ACP 请求失败或连接中断,根因未查明;未切换到新会话', '有效的远端 ACP 连接');
|
|
621
631
|
}
|
|
632
|
+
const employeeOpenCauseCodes = new Set(['AW_EMPLOYEE_BINARY_UNAVAILABLE', 'AW_EMPLOYEE_BINARY_HASH_FAILED', 'AW_EMPLOYEE_BINARY_HASH_MISMATCH', 'AW_EMPLOYEE_BINARY_VERSION_FAILED', 'AW_EMPLOYEE_BINARY_VERSION_MISMATCH', 'EMPLOYEE_CONTEXT_CHANGED', 'EMPLOYEE_ENDPOINT_INVALID', 'EMPLOYEE_BINARY_REQUIRED', 'EMPLOYEE_EXECUTION_UNSUPPORTED', 'EMPLOYEE_EXEC_PROTOCOL_INVALID', 'EMPLOYEE_EXEC_IDENTITY_MISMATCH', 'EMPLOYEE_EXEC_CLOSED', 'EMPLOYEE_EXEC_UNKNOWN', 'EMPLOYEE_EXEC_ALREADY_OPEN', 'EMPLOYEE_EXEC_READ_BUSY', 'ACP_MESSAGE_TOO_LARGE', 'ABORTED', 'SETTINGS_REQUIRED']);
|
|
622
633
|
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; }
|
|
623
634
|
const knownUpdates = new Set(['user_message_chunk', 'agent_message_chunk', 'agent_thought_chunk', 'tool_call', 'tool_call_update', 'plan', 'plan_update', 'plan_removed', 'available_commands_update', 'current_mode_update', 'config_option_update', 'session_info_update', 'usage_update', 'notice', 'compaction_update', 'compaction_summary_chunk', 'subagent_update', 'session_message', 'session_message_chunk']);
|
|
624
635
|
function safeLabel(value) { return value.replaceAll(/[\r\n\x00-\x1f\x7f]/g, ' '); }
|
package/dist/claude-local.js
CHANGED
|
@@ -31,6 +31,8 @@ export function createClaudeLocalProvider(options) {
|
|
|
31
31
|
if (typeof local.getWorkspaceLocationState !== 'function')
|
|
32
32
|
throw failure('EMPLOYEE_CONTEXT_UNAVAILABLE');
|
|
33
33
|
const workspace = await options.readWorkspace(request.sessionId), state = local.getWorkspaceLocationState(), matches = state.items.filter(row => row.workspaceId === workspace.workspaceId);
|
|
34
|
+
if (matches.length === 1 && matches[0].source === 'host')
|
|
35
|
+
return; // Host state/Need belongs to the host resolver, before local admission.
|
|
34
36
|
if (matches.length !== 1 || state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch)
|
|
35
37
|
throw failure('EMPLOYEE_CONTEXT_CHANGED');
|
|
36
38
|
const item = matches[0];
|
|
@@ -2,8 +2,8 @@ import type { ConnectNeed } from '@ai-workbench/connect/api';
|
|
|
2
2
|
import type { ConnectTarget } from './provider.ts';
|
|
3
3
|
import type { ContractInfo, Provider, WorkspaceUse, WorkspaceNoticeReceipt as LWorkspaceNoticeReceipt } from '@ai-workbench/connect/api';
|
|
4
4
|
/** 正式L0.7公开类型;消费子集不表示已安装或已通过真实产品门。 */
|
|
5
|
-
export type { WorkspaceNotice, SessionProjectTarget } from '@ai-workbench/connect/api';
|
|
6
|
-
import type { SessionProjectTarget } from '@ai-workbench/connect/api';
|
|
5
|
+
export type { WorkspaceNotice, SessionProjectTarget, HostWorkspaceResolveContext, HostSessionProjectTarget } from '@ai-workbench/connect/api';
|
|
6
|
+
import type { SessionProjectTarget, HostWorkspaceResolveContext, HostSessionProjectTarget } from '@ai-workbench/connect/api';
|
|
7
7
|
export type LoginProvider = Provider;
|
|
8
8
|
export type WorkspaceNoticeReceipt = Extract<LWorkspaceNoticeReceipt, {
|
|
9
9
|
surface: 'provider';
|
|
@@ -17,11 +17,12 @@ export type ConnectResult<T> = {
|
|
|
17
17
|
code: string;
|
|
18
18
|
message: string;
|
|
19
19
|
requires: string;
|
|
20
|
-
} & Pick<ConnectNeed, 'officialReason'
|
|
20
|
+
} & Partial<Pick<ConnectNeed, 'officialReason' | 'layer' | 'actor' | 'action' | 'lastSeen'>>;
|
|
21
21
|
};
|
|
22
|
+
export type SessionProjectResolution = SessionProjectTarget | HostSessionProjectTarget;
|
|
22
23
|
export interface NativeWorkspaceApi {
|
|
23
24
|
getContractInfo(): ContractInfo;
|
|
24
25
|
ensureReady(provider: LoginProvider): Promise<ConnectResult<ConnectTarget>>;
|
|
25
|
-
resolveSessionProject(sessionId: string, use: WorkspaceUse): Promise<ConnectResult<
|
|
26
|
+
resolveSessionProject(sessionId: string, use: WorkspaceUse, location?: HostWorkspaceResolveContext): Promise<ConnectResult<SessionProjectResolution>>;
|
|
26
27
|
acknowledgeWorkspaceNotice(receipt: WorkspaceNoticeReceipt): Promise<ConnectResult<void>>;
|
|
27
28
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ConnectNeed } from '@ai-workbench/connect/api';
|
|
2
|
+
import { NeedError } from './remote.ts';
|
|
3
|
+
type NeedInput = {
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
requires: string;
|
|
7
|
+
} & Partial<Omit<ConnectNeed, 'code' | 'message' | 'requires'>>;
|
|
8
|
+
/** Only the public L Need shape is employee wording; arbitrary errors stay diagnostic. */
|
|
9
|
+
export declare function connectNeedError(value: NeedInput): NeedError;
|
|
10
|
+
export declare function originalConnectNeed(error: unknown): Readonly<ConnectNeed> | undefined;
|
|
11
|
+
export declare function connectNeedMessage(error: unknown): string | undefined;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NeedError } from "./remote.js";
|
|
2
|
+
/** Only the public L Need shape is employee wording; arbitrary errors stay diagnostic. */
|
|
3
|
+
export function connectNeedError(value) {
|
|
4
|
+
const error = new NeedError(value.code, value.message, value.requires);
|
|
5
|
+
if (typeof value.officialReason === 'string')
|
|
6
|
+
error.officialReason = value.officialReason;
|
|
7
|
+
if (typeof value.layer === 'string' && ['enrollment', 'network', 'ssh', 'auth', 'integration', 'settings', 'projects'].includes(value.layer) && typeof value.actor === 'string' && ['employee', 'administrator', 'ai-administrator', 'plugin'].includes(value.actor) && value.action && typeof value.action.id === 'string' && typeof value.action.label === 'string' && typeof value.action.available === 'boolean') {
|
|
8
|
+
error.connectNeed = Object.freeze({ code: value.code, layer: value.layer, message: value.message, requires: value.requires, actor: value.actor, action: Object.freeze({ id: value.action.id, label: value.action.label, available: value.action.available }), ...(typeof value.officialReason === 'string' ? { officialReason: value.officialReason } : {}), ...(typeof value.lastSeen === 'string' ? { lastSeen: value.lastSeen } : {}) });
|
|
9
|
+
}
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
export function originalConnectNeed(error) {
|
|
13
|
+
const queue = [error], seen = new Set();
|
|
14
|
+
while (queue.length) {
|
|
15
|
+
const item = queue.shift();
|
|
16
|
+
if (!item || typeof item !== 'object' || seen.has(item))
|
|
17
|
+
continue;
|
|
18
|
+
seen.add(item);
|
|
19
|
+
if (item instanceof NeedError && item.connectNeed)
|
|
20
|
+
return item.connectNeed;
|
|
21
|
+
if (item instanceof AggregateError)
|
|
22
|
+
queue.push(...item.errors);
|
|
23
|
+
if (item instanceof Error && item.cause !== undefined)
|
|
24
|
+
queue.push(item.cause);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function connectNeedMessage(error) {
|
|
28
|
+
const need = originalConnectNeed(error);
|
|
29
|
+
if (!need)
|
|
30
|
+
return;
|
|
31
|
+
const location = need.action.id === 'select-workspace-location';
|
|
32
|
+
const action = location ? '设置 → 远程主机连接 → 工作区文件来源(' + need.action.label + ')' : need.action.label;
|
|
33
|
+
return need.message + (action ? '\n下一步' + (need.action.available ? '' : '(当前不可用)') + ':' + action + '。' : '');
|
|
34
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectNeedError } from "./connect-need.js";
|
|
1
2
|
import { NeedError } from "./remote.js";
|
|
2
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
const fail = (code) => new NeedError(code, '员工电脑的工作区或执行连接已变化,本轮没有继续执行。', '当前工作区绑定与员工端执行器');
|
|
@@ -29,7 +30,7 @@ export async function admitEmployeeLocation(options) {
|
|
|
29
30
|
const result = await api.getLocalWorkspaceContext(request);
|
|
30
31
|
signal?.throwIfAborted();
|
|
31
32
|
if (!result.ok)
|
|
32
|
-
throw
|
|
33
|
+
throw connectNeedError(result.error);
|
|
33
34
|
const c = result.value;
|
|
34
35
|
if (c.version !== 1 || c.source !== 'local' || c.instanceId !== connection.instanceId || c.epoch !== connection.epoch || c.workspaceId !== workspace.id || c.bindingId !== item.bindingId || !absolute(c.directory?.path) || c.directory.path !== workspace.path || !Number.isSafeInteger(c.directory.device) || !Number.isSafeInteger(c.directory.inode))
|
|
35
36
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
@@ -6,12 +6,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { connectNeedError } from "./connect-need.js";
|
|
9
10
|
import { CODEX_EMPLOYEE_SCOPE_NOTICE } from "./employee-presentation.js";
|
|
10
|
-
import { CloseDiagnostics, withFailureEvidence, closeCode, closeErrorCodes } from "./close-diagnostics.js";
|
|
11
|
+
import { CloseDiagnostics, withFailureEvidence, withFirstFailureEvidence, closeCode, closeErrorCodes } from "./close-diagnostics.js";
|
|
11
12
|
import { createHash } from 'node:crypto';
|
|
12
13
|
import { admitEmployeeLocation } from "./employee-location.js";
|
|
13
14
|
import { EmployeeExecution } from "./employee-exec.js";
|
|
14
|
-
import { createEmployeeSshTransport
|
|
15
|
+
import { createEmployeeSshTransport } from "./employee-ssh.js";
|
|
15
16
|
import { sshIo } from "./broker.js";
|
|
16
17
|
import { NeedError } from "./remote.js";
|
|
17
18
|
const fail = (code) => new NeedError(code, code === 'EMPLOYEE_OPERATION_UNSUPPORTED' ? '当前本机工作区只支持正在连接的会话;单次运行和断线后的冷恢复暂不支持,请新建会话。' : '本机工作区的执行连接无法继续(' + code + '),本轮没有改用主机执行。', '当前 L 工作区绑定、N/S 员工执行端点与支持本机执行的会话适配器');
|
|
@@ -53,7 +54,7 @@ export async function acceptEmployeeUserTurn(get, employee, agent, api, request)
|
|
|
53
54
|
stable();
|
|
54
55
|
const context = await api.getLocalWorkspaceContext({ instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.id, expectedBindingId: row.bindingId });
|
|
55
56
|
if (!context.ok)
|
|
56
|
-
throw
|
|
57
|
+
throw connectNeedError(context.error);
|
|
57
58
|
stable();
|
|
58
59
|
const c = context.value, d = { ...c.directory };
|
|
59
60
|
if (c.version !== 1 || c.source !== 'local' || c.instanceId !== connection.instanceId || c.epoch !== connection.epoch || c.workspaceId !== workspace.id || c.bindingId !== row.bindingId || !absolute(d?.path) || d.path !== workspace.path || ![d.device, d.inode].every(n => Number.isSafeInteger(n) && n >= 0))
|
|
@@ -63,7 +64,7 @@ export async function acceptEmployeeUserTurn(get, employee, agent, api, request)
|
|
|
63
64
|
throw fail('LOCAL_USER_TURN_UNCONFIRMED');
|
|
64
65
|
const response = await api.acceptLocalWorkspaceUserTurn({ instanceId: connection.instanceId, epoch: connection.epoch, sessionId: request.sessionId, provider: agent.loginProvider, purpose: 'user-turn', workspaceId: workspace.id, expectedBindingId: row.bindingId, expectedDirectory: { device: d.device, inode: d.inode }, expectedUserTurn: { ...expected } });
|
|
65
66
|
if (!response.ok)
|
|
66
|
-
throw
|
|
67
|
+
throw connectNeedError(response.error);
|
|
67
68
|
stable();
|
|
68
69
|
const a = response.value;
|
|
69
70
|
if (!a || a.version !== 1 || typeof a.acceptanceId !== 'string' || !a.acceptanceId.trim() || a.instanceId !== connection.instanceId || a.epoch !== connection.epoch || a.sessionId !== request.sessionId || a.provider !== agent.loginProvider || a.workspaceId !== workspace.id || a.bindingId !== row.bindingId || a.source !== 'local' || a.directory?.path !== d.path || a.directory?.device !== d.device || a.directory?.inode !== d.inode || a.userTurn?.turn !== expected.turn || a.userTurn?.userMessageId !== expected.userMessageId || ![a.userTurn?.startSequence, a.userTurn?.userMessageSequence].every(n => Number.isSafeInteger(n) && n >= 0) || a.userTurn.userMessageSequence <= a.userTurn.startSequence)
|
|
@@ -86,7 +87,7 @@ export async function validateEmployeeUserTurn(get, employee, api, request, conn
|
|
|
86
87
|
const c = await api.getLocalWorkspaceContext({ instanceId: a.instanceId, epoch: a.epoch, workspaceId: a.workspaceId, expectedBindingId: a.bindingId });
|
|
87
88
|
stable();
|
|
88
89
|
if (!c.ok)
|
|
89
|
-
throw
|
|
90
|
+
throw connectNeedError(c.error);
|
|
90
91
|
if (c.value.version !== 1 || c.value.source !== 'local' || c.value.instanceId !== a.instanceId || c.value.epoch !== a.epoch || c.value.workspaceId !== a.workspaceId || c.value.bindingId !== a.bindingId || c.value.directory.path !== a.directory.path || c.value.directory.device !== a.directory.device || c.value.directory.inode !== a.directory.inode)
|
|
91
92
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
92
93
|
const turn = employee.currentUserTurn(request);
|
|
@@ -176,17 +177,25 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
176
177
|
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
177
178
|
let entry = prior && prior.scope === scope && !prior.ended ? prior : undefined;
|
|
178
179
|
let candidate = entry;
|
|
180
|
+
// Admission still verifies the real directory and N generation. Per-RPC gates below
|
|
181
|
+
// reuse the subscribed connection/location/network facts, without another SSH.
|
|
182
|
+
await admitted.validate(request.signal ?? new AbortController().signal);
|
|
183
|
+
details?.record({ kind: 'session', severity: 'info', code: 'EMPLOYEE_EXECUTION_VALIDATION_POLICY', summary: '本机执行核验不可关闭:L接纳核对实际目录与N端点;同代执行通道仅在open时核对一次主机target与固定执行器。之后每条消息只核对内存中的连接、工作区绑定和已接纳输入,撤销沿现有连接/位置/N网络事件及执行SSH关闭处理;不重复SSH,不改用主机执行。' });
|
|
184
|
+
if (entry)
|
|
185
|
+
entry.current = input;
|
|
179
186
|
const base = async (signal) => {
|
|
180
187
|
signal.throwIfAborted();
|
|
181
188
|
if (get() !== api || employee.getNetwork() !== network)
|
|
182
189
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
183
190
|
if (candidate?.ended)
|
|
184
191
|
throw candidate.reason ?? fail('EMPLOYEE_EXEC_CLOSED');
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
const current = candidate?.current ?? input;
|
|
193
|
+
current.request.localAdmissionCheck?.();
|
|
194
|
+
await current.stable(signal);
|
|
187
195
|
if (get() !== api || employee.getNetwork() !== network)
|
|
188
196
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
189
|
-
|
|
197
|
+
current.request.localAdmissionCheck?.();
|
|
198
|
+
await current.stable(signal);
|
|
190
199
|
signal.throwIfAborted();
|
|
191
200
|
};
|
|
192
201
|
let argv = employee.argv;
|
|
@@ -207,11 +216,11 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
207
216
|
} };
|
|
208
217
|
const ssh = { closeDiagnostics, target, endpoint: admitted.endpoint, employeeBinary: native.path, binarySha256: native.sha256, acpMaxMessageBytes: request.acpMaxMessageBytes, argv: argv ?? (() => []), validate: base, io: scopedIo };
|
|
209
218
|
const validate = async (signal) => { try {
|
|
210
|
-
await
|
|
219
|
+
await base(signal);
|
|
211
220
|
}
|
|
212
221
|
catch (error) {
|
|
213
222
|
if (candidate && errorCode(error) !== 'ABORTED')
|
|
214
|
-
|
|
223
|
+
void candidate.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.validation-failure', reason: closeCode(error), action: 'invalidate' }).catch(cleanup => closeDiagnostics.record({ layer: 'runtime', initiator: 'provider.validation-cleanup', reason: closeCode(cleanup, 'REMOTE_CLEANUP_UNCONFIRMED'), action: 'failure' }));
|
|
215
224
|
throw error;
|
|
216
225
|
} };
|
|
217
226
|
await validate(request.signal ?? new AbortController().signal);
|
|
@@ -302,14 +311,14 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
302
311
|
try {
|
|
303
312
|
const result = await execution.handle(params, signal);
|
|
304
313
|
if (result.closed)
|
|
305
|
-
|
|
314
|
+
void entry.dispose(undefined, { layer: 'runtime', initiator: 'provider.rpc-closed-result', reason: 'EMPLOYEE_EXEC_CLOSED', operation: typeof params === 'object' && params && 'operation' in params && typeof params.operation === 'string' ? params.operation : 'unknown', action: 'dispose' }).catch(cleanup => closeDiagnostics.record({ layer: 'runtime', initiator: 'provider.rpc-cleanup', reason: closeCode(cleanup, 'REMOTE_CLEANUP_UNCONFIRMED'), action: 'failure' }));
|
|
306
315
|
return { ...result };
|
|
307
316
|
}
|
|
308
317
|
catch (error) {
|
|
309
318
|
// EmployeeExecution reserves ABORTED for an unconsumed read wait. Lost responses are UNKNOWN.
|
|
310
319
|
if (!(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
|
|
311
|
-
|
|
312
|
-
throw error;
|
|
320
|
+
void entry.dispose(asNeed(error), { layer: 'runtime', initiator: 'provider.rpc-failure', reason: closeCode(error), action: 'dispose' }).catch(cleanup => closeDiagnostics.record({ layer: 'runtime', initiator: 'provider.rpc-cleanup', reason: closeCode(cleanup, 'REMOTE_CLEANUP_UNCONFIRMED'), action: 'failure' }));
|
|
321
|
+
throw withFirstFailureEvidence(error, closeDiagnostics.first?.reason, errorCode(error));
|
|
313
322
|
}
|
|
314
323
|
};
|
|
315
324
|
runtime.dispose = cause => entry.dispose(undefined, cause ?? { layer: 'runtime', initiator: 'runtime.dispose', reason: 'RUNTIME_DISPOSE', action: 'dispose' });
|
package/dist/employee-ssh.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface EmployeeSshOptions {
|
|
|
18
18
|
}
|
|
19
19
|
/** Fixed bootstrap only; cwd for every native operation remains in its original RPC. */
|
|
20
20
|
export declare function employeeExecServerCommand(binary: string, binarySha256: string): string;
|
|
21
|
-
/**
|
|
21
|
+
/** Explicit read-only probe; production dispatch verifies S once in transport open. */
|
|
22
22
|
export declare function validateEmployeeSshTarget(options: EmployeeSshOptions, signal?: AbortSignal): Promise<void>;
|
|
23
23
|
/** Outer L-authenticated host SSH, then S-authenticated employee SSH; no host execution fallback. */
|
|
24
24
|
export declare function createEmployeeSshTransport(options: EmployeeSshOptions, signal?: AbortSignal): Promise<EmployeeExecutionTransport>;
|
package/dist/employee-ssh.js
CHANGED
|
@@ -99,7 +99,7 @@ async function currentTarget(options, signal) {
|
|
|
99
99
|
await gate();
|
|
100
100
|
return { io, args, gate };
|
|
101
101
|
}
|
|
102
|
-
/**
|
|
102
|
+
/** Explicit read-only probe; production dispatch verifies S once in transport open. */
|
|
103
103
|
export async function validateEmployeeSshTarget(options, signal = new AbortController().signal) { await currentTarget(options, signal); }
|
|
104
104
|
/** Outer L-authenticated host SSH, then S-authenticated employee SSH; no host execution fallback. */
|
|
105
105
|
export async function createEmployeeSshTransport(options, signal = new AbortController().signal) {
|
|
@@ -6,6 +6,8 @@ export declare function closeErrorCodes(error: unknown, fallback?: string): stri
|
|
|
6
6
|
export declare function closeFailure(code: string, message: string, requires: string, codes: readonly string[]): NeedError;
|
|
7
7
|
/** Saving succeeded: retain the original trusted error type, code and presentation. */
|
|
8
8
|
export declare function withFirstFailureEvidence<T>(primary: T, firstReason: unknown, primaryCode: string): T;
|
|
9
|
+
/** A known turn failure is final even while its original resource is still closing. */
|
|
10
|
+
export declare function withPendingCleanupEvidence<T>(primary: T, firstReason: unknown, primaryCode: string): T;
|
|
9
11
|
/** Preserve causal roles without copying arbitrary exception text into employee output. */
|
|
10
12
|
export declare function withFailureEvidence(primary: unknown, secondary: readonly unknown[], kind: 'diagnostic' | 'cleanup', firstReason?: unknown, primaryCode?: string): NeedError;
|
|
11
13
|
export declare function primaryFailureCode(error: unknown, fallback: string): string;
|
package/dist/failure-evidence.js
CHANGED
|
@@ -62,6 +62,14 @@ export function withFirstFailureEvidence(primary, firstReason, primaryCode) {
|
|
|
62
62
|
}
|
|
63
63
|
return primary;
|
|
64
64
|
}
|
|
65
|
+
/** A known turn failure is final even while its original resource is still closing. */
|
|
66
|
+
export function withPendingCleanupEvidence(primary, firstReason, primaryCode) {
|
|
67
|
+
if (primary && typeof primary === 'object') {
|
|
68
|
+
const previous = retainedEvidence(primary), first = previous?.first ?? (safeCode(firstReason) ? firstReason : undefined);
|
|
69
|
+
evidence.set(primary, Object.freeze({ ...previous, ...(first ? { first } : {}), original: previous?.original ?? Object.freeze([safeCode(primaryCode) ? primaryCode : 'TURN_FAILED']), diagnostic: previous?.diagnostic ?? Object.freeze([]), cleanup: previous?.cleanup ?? Object.freeze([]), cleanupPending: true }));
|
|
70
|
+
}
|
|
71
|
+
return primary;
|
|
72
|
+
}
|
|
65
73
|
/** Preserve causal roles without copying arbitrary exception text into employee output. */
|
|
66
74
|
export function withFailureEvidence(primary, secondary, kind, firstReason, primaryCode) {
|
|
67
75
|
const previous = retainedEvidence(primary), first = previous?.first ?? (safeCode(firstReason) ? firstReason : undefined);
|
|
@@ -79,5 +87,5 @@ export function failureEvidenceSummary(error) {
|
|
|
79
87
|
const facts = retainedEvidence(error);
|
|
80
88
|
if (!facts)
|
|
81
89
|
return '';
|
|
82
|
-
return '技术详情:' + [...(facts.first ? ['首次观察关闭 ' + facts.first] : []), '运行原因 ' + facts.original.join('、'), ...(facts.diagnostic.length ? ['详情保存未确认 ' + facts.diagnostic.join('、')] : []), ...(facts.cleanup.length ? ['清理未确认 ' + facts.cleanup.join('、')] : [])].join(';');
|
|
90
|
+
return '技术详情:' + [...(facts.first ? ['首次观察关闭 ' + facts.first] : []), '运行原因 ' + facts.original.join('、'), ...(facts.diagnostic.length ? ['详情保存未确认 ' + facts.diagnostic.join('、')] : []), ...(facts.cleanup.length ? ['清理未确认 ' + facts.cleanup.join('、')] : facts.cleanupPending ? ['正在确认主机清理,原资源清理结果尚待确认'] : [])].join(';');
|
|
83
91
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ConnectApi, ConnectTarget, ConnectionState, RuntimeRequest, Runtime } from './provider.ts';
|
|
2
|
+
import type { NativeWorkspaceApi, HostSessionProjectTarget, LoginProvider } from './connect-contract.ts';
|
|
3
|
+
import type { WorkspaceSourceOptions } from './workspace-source.ts';
|
|
4
|
+
export interface HostWorkspaceGuard {
|
|
5
|
+
readonly signal: AbortSignal;
|
|
6
|
+
readonly revocation: AbortSignal;
|
|
7
|
+
resolve(): Promise<HostSessionProjectTarget>;
|
|
8
|
+
assertCurrent(): void;
|
|
9
|
+
observeRuntime(runtime: Runtime): void;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
}
|
|
12
|
+
/** Current official membership and explicit host selection; never infer source from a pathname. */
|
|
13
|
+
export declare function hostWorkspaceGuard(get: () => ConnectApi | undefined, api: ConnectApi & NativeWorkspaceApi, connection: ConnectionState, target: ConnectTarget, request: RuntimeRequest, workspace: WorkspaceSourceOptions['workspace'] | undefined, provider: LoginProvider): Promise<HostWorkspaceGuard | undefined>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { basename } from 'node:path/posix';
|
|
2
|
+
import { awaitL, sameTarget, unwrap } from "./provider.js";
|
|
3
|
+
import { connectNeedError } from "./connect-need.js";
|
|
4
|
+
import { NeedError } from "./remote.js";
|
|
5
|
+
const invalid = () => new NeedError('PROJECT_BINDING_INVALID', '文件来源已改,请新建会话继续;原会话历史仍可查看。', 'L核验的同一主机位置与目录身份');
|
|
6
|
+
const hostIdentity = (v) => JSON.stringify(v ? [v.host, v.username, v.port] : null);
|
|
7
|
+
const absolute = (v) => typeof v === 'string' && v.startsWith('/') && !v.includes('\0');
|
|
8
|
+
function physical(d) {
|
|
9
|
+
if (!d || !absolute(d.requestedPath) || !absolute(d.path) || !(d.parentPath === null || absolute(d.parentPath)) || !d.identity || !d.entryIdentity || ![d.identity.device, d.identity.inode, d.entryIdentity.device, d.entryIdentity.inode].every(x => typeof x === 'string' && x.length > 0) || typeof d.isSymlink !== 'boolean' || !d.permissions || d.permissions.read !== true || d.permissions.execute !== true || !(typeof d.permissions.write === 'boolean' || d.permissions.write === null))
|
|
10
|
+
throw invalid();
|
|
11
|
+
// Directory contents change timestamps; neither timestamp is an execution identity.
|
|
12
|
+
return JSON.stringify([d.requestedPath, d.path, d.parentPath, d.identity.device, d.identity.inode, d.entryIdentity.device, d.entryIdentity.inode, d.isSymlink, d.permissions.read, d.permissions.write, d.permissions.execute]);
|
|
13
|
+
}
|
|
14
|
+
/** Current official membership and explicit host selection; never infer source from a pathname. */
|
|
15
|
+
export async function hostWorkspaceGuard(get, api, connection, target, request, workspace, provider) {
|
|
16
|
+
if (api.getContractInfo().workspaceLocations !== 1 || typeof api.getWorkspaceLocationState !== 'function')
|
|
17
|
+
return;
|
|
18
|
+
const firstState = api.getWorkspaceLocationState();
|
|
19
|
+
if (!workspace) {
|
|
20
|
+
if (firstState.items.some(row => row.source === 'host'))
|
|
21
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要核对当前会话所属工作区,才能使用所选主机文件夹。', '官方会话与唯一workspaceRegistry关系');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const registered = { ...await workspace(request) };
|
|
25
|
+
request.signal?.throwIfAborted();
|
|
26
|
+
const state = api.getWorkspaceLocationState(), rows = state.items.filter(row => row.workspaceId === registered.id), selected = rows[0];
|
|
27
|
+
if (rows.length !== 1 || !selected)
|
|
28
|
+
return;
|
|
29
|
+
if (selected.source !== 'host') {
|
|
30
|
+
if (selected.source !== null && !['local', 'legacy-host'].includes(selected.source))
|
|
31
|
+
throw invalid();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (api.getContractInfo().hostWorkspaceResolution !== 1 || typeof api.onWorkspaceLocationState !== 'function' || typeof api.onConnectionState !== 'function')
|
|
35
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要支持主机位置解析的远程主机连接组件;本轮未执行,原话保留。', 'L >=0.7.20 / hostWorkspaceResolution:1');
|
|
36
|
+
if (state.instanceId !== connection.instanceId || state.epoch !== connection.epoch)
|
|
37
|
+
throw invalid();
|
|
38
|
+
if (state.need)
|
|
39
|
+
throw connectNeedError(state.need);
|
|
40
|
+
if (selected.need)
|
|
41
|
+
throw connectNeedError(selected.need);
|
|
42
|
+
if (!registered.id || !absolute(registered.path) || state.phase !== 'ready' || selected.status !== 'selected' || !selected.bindingId)
|
|
43
|
+
throw invalid();
|
|
44
|
+
const trusted = structuredClone(target);
|
|
45
|
+
if (selected.host && (selected.host.host !== trusted.host || selected.host.username !== trusted.username || selected.host.port !== trusted.port))
|
|
46
|
+
throw invalid();
|
|
47
|
+
const captured = structuredClone(selected), stamp = physical(captured.remote), context = { instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: registered.id, expectedBindingId: selected.bindingId };
|
|
48
|
+
const revoked = new AbortController(), offs = [];
|
|
49
|
+
let runtime, disposed = false;
|
|
50
|
+
const current = () => {
|
|
51
|
+
request.signal?.throwIfAborted();
|
|
52
|
+
revoked.signal.throwIfAborted();
|
|
53
|
+
if (disposed || get() !== api || api.getContractInfo().hostWorkspaceResolution !== 1)
|
|
54
|
+
throw invalid();
|
|
55
|
+
const c = api.getConnectionState(), s = api.getWorkspaceLocationState(), items = s.items.filter(row => row.workspaceId === context.workspaceId), item = items[0];
|
|
56
|
+
if (c.phase !== 'connected' || c.instanceId !== context.instanceId || c.epoch !== context.epoch)
|
|
57
|
+
throw new NeedError('CONNECTION_LOST', '远程主机连接已变化,原派发已停止;请新建会话继续。', '本轮原连接代次');
|
|
58
|
+
if (s.instanceId !== context.instanceId || s.epoch !== context.epoch)
|
|
59
|
+
throw invalid();
|
|
60
|
+
if (s.need)
|
|
61
|
+
throw connectNeedError(s.need);
|
|
62
|
+
if (item?.need)
|
|
63
|
+
throw connectNeedError(item.need);
|
|
64
|
+
if (hostIdentity(item?.host) !== hostIdentity(captured.host))
|
|
65
|
+
throw invalid();
|
|
66
|
+
if (s.phase !== 'ready' || items.length !== 1 || item.bindingId !== context.expectedBindingId || item.workspaceId !== context.workspaceId || item.source !== 'host' || item.status !== 'selected' || item.localPath !== captured.localPath || physical(item.remote) !== stamp)
|
|
67
|
+
throw invalid();
|
|
68
|
+
};
|
|
69
|
+
const revoke = (error) => { if (revoked.signal.aborted)
|
|
70
|
+
return; runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'host-workspace.observer', reason: error instanceof NeedError ? error.code : 'HOST_WORKSPACE_REVOKED', action: 'abort', changes: [{ field: 'host-workspace.valid', before: true, after: false }] }); revoked.abort(error); };
|
|
71
|
+
const changed = () => { if (request.signal?.aborted && !revoked.signal.aborted)
|
|
72
|
+
return; try {
|
|
73
|
+
current();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
revoke(error);
|
|
77
|
+
} };
|
|
78
|
+
const dispose = () => { if (disposed)
|
|
79
|
+
return; disposed = true; for (const off of offs.splice(0))
|
|
80
|
+
off(); };
|
|
81
|
+
const membership = async () => { current(); const now = await workspace(request); current(); if (now.id !== registered.id || now.path !== registered.path)
|
|
82
|
+
throw invalid(); };
|
|
83
|
+
try {
|
|
84
|
+
current();
|
|
85
|
+
offs.push(api.onWorkspaceLocationState(changed));
|
|
86
|
+
offs.push(api.onConnectionState(changed));
|
|
87
|
+
current();
|
|
88
|
+
return { revocation: revoked.signal, signal: AbortSignal.any([revoked.signal, ...(request.signal ? [request.signal] : [])]), assertCurrent: current, observeRuntime(value) { runtime = value; current(); }, dispose,
|
|
89
|
+
async resolve() {
|
|
90
|
+
try {
|
|
91
|
+
await membership();
|
|
92
|
+
const value = unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: provider, purpose: 'user-turn' }, { ...context }), AbortSignal.any([revoked.signal, ...(request.signal ? [request.signal] : [])])));
|
|
93
|
+
await membership();
|
|
94
|
+
if (!value || !('source' in value) || value.source !== 'host' || value.instanceId !== context.instanceId || value.epoch !== context.epoch || value.sessionId !== request.sessionId || value.workspaceId !== context.workspaceId || value.bindingId !== context.expectedBindingId || !Number.isSafeInteger(value.bindingRevision) || value.bindingRevision < 1 || (!value.target || !value.target.session || !sameTarget(trusted, value.target)) || physical(value.directory) !== stamp || value.project?.path !== value.directory.path || value.project?.name !== (basename(value.directory.path) || '/') || 'notice' in value && value.notice !== undefined)
|
|
95
|
+
throw invalid();
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!request.signal?.aborted)
|
|
100
|
+
revoke(error);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
dispose();
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -106,6 +106,8 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
106
106
|
jitterRatio: number;
|
|
107
107
|
};
|
|
108
108
|
private cleanupRuntime;
|
|
109
|
+
/** Publish an already known failure without coupling the official stream to cleanup. */
|
|
110
|
+
private finishFailedTurn;
|
|
109
111
|
disposeSession(sessionId: string): Promise<void>;
|
|
110
112
|
detachSession(sessionId: string): Promise<void>;
|
|
111
113
|
dispose(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { connectNeedMessage } from "./connect-need.js";
|
|
1
2
|
import { CloseDiagnostics, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, closeCode, closeErrorCodes, closeFailure } from "./close-diagnostics.js";
|
|
3
|
+
import { withPendingCleanupEvidence } from "./failure-evidence.js";
|
|
2
4
|
import { runCatalog } from "./catalog-ssh.js";
|
|
3
5
|
import { createCatalogDiagnostics, CatalogDiagnosticsRemote } from "./catalog-diagnostics.js";
|
|
4
6
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
@@ -204,6 +206,35 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
204
206
|
this.cleanups.set(runtime, cleanup);
|
|
205
207
|
return cleanup;
|
|
206
208
|
}
|
|
209
|
+
/** Publish an already known failure without coupling the official stream to cleanup. */
|
|
210
|
+
finishFailedTurn(key, live, primary, code, firstReason) {
|
|
211
|
+
withPendingCleanupEvidence(primary, firstReason, code);
|
|
212
|
+
const runtime = live.runtime;
|
|
213
|
+
const disposal = this.cleanupRuntime(runtime).then(() => {
|
|
214
|
+
if (this.sessions.get(key) !== live || live.disposal !== disposal)
|
|
215
|
+
return;
|
|
216
|
+
live.disposal = undefined;
|
|
217
|
+
if (!live.failure) {
|
|
218
|
+
if (typeof runtime.dispose === 'function') {
|
|
219
|
+
this.sessions.delete(key);
|
|
220
|
+
this.preparationFailures.set(key, code);
|
|
221
|
+
}
|
|
222
|
+
else
|
|
223
|
+
live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
|
|
224
|
+
}
|
|
225
|
+
}, error => {
|
|
226
|
+
if (this.sessions.get(key) === live && live.disposal === disposal)
|
|
227
|
+
live.cleanupFailure = withFailureEvidence(primary, [error], 'cleanup', firstReason, code);
|
|
228
|
+
throw error;
|
|
229
|
+
});
|
|
230
|
+
live.disposal = disposal;
|
|
231
|
+
const report = (reason) => {
|
|
232
|
+
runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-cleanup', reason, action: 'dispose' });
|
|
233
|
+
// State settles before reporting. A delayed D19 write is not another release gate.
|
|
234
|
+
void Promise.resolve().then(() => runtime.details?.flush()).catch(error => runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-cleanup-report', reason: closeCode(error, 'CLOSE_FLUSH_FAILED'), action: 'failure' }));
|
|
235
|
+
};
|
|
236
|
+
void disposal.then(() => report(typeof runtime.dispose === 'function' ? 'RUNTIME_CLEANUP_CONFIRMED' : 'REMOTE_CLEANUP_UNCONFIRMED'), () => report('REMOTE_CLEANUP_UNCONFIRMED'));
|
|
237
|
+
}
|
|
207
238
|
async disposeSession(sessionId) {
|
|
208
239
|
this.sessions.get(sessionId)?.runtime.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.dispose-session', reason: 'SESSION_DISPOSED', action: 'abort' });
|
|
209
240
|
this.turns.get(sessionId)?.abort();
|
|
@@ -258,7 +289,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
258
289
|
if (error instanceof NeedError) {
|
|
259
290
|
const rpc = /RPC 错误码 (-?\d+)/u.exec(error.requires)?.[1];
|
|
260
291
|
const evidence = failureEvidenceSummary(error);
|
|
261
|
-
const message = employeeFailure(error.code, this.agent.loginProvider) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '') + (evidence ? ';' + evidence : '');
|
|
292
|
+
const message = (connectNeedMessage(error) ?? employeeFailure(error.code, this.agent.loginProvider)) + (rpc ? `(技术详情:RPC 错误码 ${rpc})` : '') + (evidence ? ';' + evidence : '');
|
|
262
293
|
const login = originalLoginFailure(error);
|
|
263
294
|
let diagnostic = login?.technical ? JSON.stringify(login.technical) : '';
|
|
264
295
|
if (login?.technical && this.reportLoginFailure) {
|
|
@@ -426,6 +457,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
426
457
|
request.signal = modelSignal;
|
|
427
458
|
modelSignal.throwIfAborted();
|
|
428
459
|
}
|
|
460
|
+
if (runtime.contextSignal) {
|
|
461
|
+
modelSignal = AbortSignal.any([modelSignal, runtime.contextSignal]);
|
|
462
|
+
options.signal = modelSignal;
|
|
463
|
+
request.signal = modelSignal;
|
|
464
|
+
modelSignal.throwIfAborted();
|
|
465
|
+
}
|
|
429
466
|
catalog.assertCurrent();
|
|
430
467
|
if (runtime.connection && (runtime.connection.instanceId !== catalog.target.instanceId || runtime.connection.epoch !== catalog.target.epoch) || runtime.identity !== `${catalog.target.target.username}@${catalog.target.target.host}:${catalog.target.target.port}`)
|
|
431
468
|
throw new ModelConfigurationError('模型目录与当前会话可信员工主机不一致');
|
|
@@ -657,6 +694,12 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
657
694
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
658
695
|
}
|
|
659
696
|
catch (error) {
|
|
697
|
+
// ACP cancellation must retain the source revocation that caused it.
|
|
698
|
+
// User cancellation and unrelated signals keep their existing semantics.
|
|
699
|
+
const sourceReason = runtime?.contextSignal?.aborted ? runtime.contextSignal.reason : undefined;
|
|
700
|
+
if (error instanceof NeedError && error.code === 'ABORTED' && sourceReason instanceof NeedError) {
|
|
701
|
+
error = Object.assign(new NeedError(sourceReason.code, sourceReason.message, sourceReason.requires), { ...(sourceReason.connectNeed ? { connectNeed: sourceReason.connectNeed } : {}), ...(sourceReason.officialReason ? { officialReason: sourceReason.officialReason } : {}), cause: new AggregateError([sourceReason, error], 'HOST_SOURCE_REVOKED') });
|
|
702
|
+
}
|
|
660
703
|
turnFailure = error;
|
|
661
704
|
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
662
705
|
recordInformation();
|
|
@@ -664,22 +707,32 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
664
707
|
if (live && session && session.promptDispatchCount > promptBaseline)
|
|
665
708
|
live.failure = { cause: error };
|
|
666
709
|
const reporting = [];
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
710
|
+
const reportFailure = async () => {
|
|
711
|
+
if (request?.recordEarlyFailure) {
|
|
712
|
+
const record = request.recordEarlyFailure;
|
|
713
|
+
request.recordEarlyFailure = undefined;
|
|
714
|
+
try {
|
|
715
|
+
await record(Object.assign(new NeedError(failureCode ?? 'TURN_PREPARATION_FAILED', '本轮在执行前失败。', '白名单阶段与错误代码'), { cause: error }), earlyStage);
|
|
716
|
+
}
|
|
717
|
+
catch (failure) {
|
|
718
|
+
reporting.push(failure);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
670
721
|
try {
|
|
671
|
-
await
|
|
722
|
+
await runtime?.details?.flush();
|
|
672
723
|
}
|
|
673
724
|
catch (failure) {
|
|
674
725
|
reporting.push(failure);
|
|
675
726
|
}
|
|
727
|
+
};
|
|
728
|
+
if (runtime && key && live && live.runtime === runtime && this.sessions.get(key) === live) {
|
|
729
|
+
const captured = runtime;
|
|
730
|
+
void reportFailure().then(() => { for (const failure of reporting)
|
|
731
|
+
for (const code of closeErrorCodes(failure, 'DIAGNOSTIC_WRITE_FAILED'))
|
|
732
|
+
captured.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-details', reason: code, action: 'failure' }); }, failure => captured.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.failed-turn-details', reason: closeCode(failure, 'DIAGNOSTIC_WRITE_FAILED'), action: 'failure' }));
|
|
676
733
|
}
|
|
677
|
-
|
|
678
|
-
await
|
|
679
|
-
}
|
|
680
|
-
catch (failure) {
|
|
681
|
-
reporting.push(failure);
|
|
682
|
-
}
|
|
734
|
+
else
|
|
735
|
+
await reportFailure();
|
|
683
736
|
if (activeMapper)
|
|
684
737
|
yield* activeMapper.flush();
|
|
685
738
|
const firstReason = request?.closeDiagnostics?.first?.reason ?? runtime?.closeDiagnostics?.first?.reason;
|
|
@@ -703,15 +756,19 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
703
756
|
else if (!succeeded || !key || mode !== 'session') {
|
|
704
757
|
session?.close({ layer: 'adapter', initiator: 'adapter.turn-finally', reason: succeeded ? 'TURN_FINISHED' : 'TURN_FAILED', action: 'close' });
|
|
705
758
|
if (runtime) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
759
|
+
if (!succeeded && turnFailure && key && live && live.runtime === runtime && this.sessions.get(key) === live)
|
|
760
|
+
this.finishFailedTurn(key, live, turnFailure, failureCode ?? 'TURN_PREPARATION_FAILED', request?.closeDiagnostics?.first?.reason ?? runtime.closeDiagnostics?.first?.reason);
|
|
761
|
+
else {
|
|
762
|
+
await this.cleanupRuntime(runtime);
|
|
763
|
+
if (key && live && !live.failure && this.sessions.get(key) === live) {
|
|
764
|
+
if (typeof runtime.dispose === 'function') {
|
|
765
|
+
this.sessions.delete(key);
|
|
766
|
+
if (!succeeded)
|
|
767
|
+
this.preparationFailures.set(key, failureCode ?? 'TURN_PREPARATION_FAILED');
|
|
768
|
+
}
|
|
769
|
+
else
|
|
770
|
+
live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
|
|
712
771
|
}
|
|
713
|
-
else
|
|
714
|
-
live.cleanupFailure = new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '缺少原实例结束证明,不能重开或重发', '原实例的条件结束与list消失证明');
|
|
715
772
|
}
|
|
716
773
|
}
|
|
717
774
|
}
|
package/dist/presentation.js
CHANGED
|
@@ -9,6 +9,7 @@ export function employeeFailure(code, brand = 'codex') {
|
|
|
9
9
|
WORKSPACE_SOURCE_CONFIRMATION_UNAVAILABLE: '我需要打开工作区来源确认,目前无法显示确认动作;本轮未执行,你的原话仍保留。',
|
|
10
10
|
WORKSPACE_SOURCE_SELECTION_UNCONFIRMED: '无法核对工作区来源的确认结果,本轮已停止;你的原话仍保留。',
|
|
11
11
|
WORKSPACE_SOURCE_CONFIRMATION_FAILED: '工作区来源确认未完成,本轮已停止;你的原话仍保留。',
|
|
12
|
+
PROJECT_BINDING_INVALID: '文件来源已改,请新建会话继续;原会话历史仍可查看。',
|
|
12
13
|
WORKSPACE_LOCATION_REQUIRED: '我需要你确认这个工作区使用哪一个文件夹,本轮未执行;你的原话仍保留。',
|
|
13
14
|
WORKSPACE_LOCATION_CONFLICT: '工作区来源选择已更新,本次确认不能覆盖新的选择;本轮未执行,你的原话仍保留。',
|
|
14
15
|
LOCAL_USER_TURN_CHANGED: '本次输入已变化,本轮已停止,请确认对话后重新发送。',
|
package/dist/provider.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface EmployeeExecution {
|
|
|
29
29
|
export interface Runtime {
|
|
30
30
|
identity: string;
|
|
31
31
|
cwd: string;
|
|
32
|
+
contextSignal?: AbortSignal;
|
|
32
33
|
execution?: EmployeeExecution;
|
|
33
34
|
acpTransport?: {
|
|
34
35
|
writable: WritableStream<Uint8Array>;
|
|
@@ -141,7 +142,7 @@ type Result<T> = {
|
|
|
141
142
|
code: string;
|
|
142
143
|
message: string;
|
|
143
144
|
requires: string;
|
|
144
|
-
} & Pick<ConnectNeed, 'officialReason'
|
|
145
|
+
} & Partial<Pick<ConnectNeed, 'officialReason' | 'layer' | 'actor' | 'action' | 'lastSeen'>>;
|
|
145
146
|
};
|
|
146
147
|
/** 消费正式connect-api 0.7公开子集;X不响应L登录票据、不直接调用登录。 */
|
|
147
148
|
export interface ConnectApi extends Pick<LConnectApi, 'getConnectionState' | 'getLoginPromptState' | 'requestLoginPrompt' | 'refreshLoginPrompts'>, Partial<NativeWorkspaceApi>, Partial<Pick<LConnectApi, 'getWorkspaceBindingState' | 'getModelCatalogTarget' | 'onConnectionState' | 'onLoginPromptState' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'getWorkspaceLocationState' | 'getLocalWorkspaceContext' | 'onWorkspaceLocationState' | 'acceptLocalWorkspaceUserTurn' | 'selectWorkspaceLocation'>> {
|
package/dist/provider.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hostWorkspaceGuard } from "./host-workspace.js";
|
|
2
|
+
import { connectNeedError } from "./connect-need.js";
|
|
1
3
|
import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
|
|
2
4
|
import { NeedError } from "./remote.js";
|
|
3
5
|
import { CONNECTION_REQUIRED, TEST_HOST_NOTICE } from "./presentation.js";
|
|
@@ -38,8 +40,11 @@ function verifiedLogin(api, brand, expected) {
|
|
|
38
40
|
throw new NeedError('CONNECTION_LOST', 'connection binding changed', 'current employee connection');
|
|
39
41
|
requireLoginState(login);
|
|
40
42
|
const reason = loginReason(login, connection, brand);
|
|
41
|
-
if (reason)
|
|
42
|
-
|
|
43
|
+
if (reason) {
|
|
44
|
+
const original = (reason === 'LOGIN_PROVIDER_NEED' || reason === 'LOGIN_PHASE_BLOCKED') && login.instanceId === connection.instanceId ? login.providerNeeds[brand] : undefined;
|
|
45
|
+
const error = original ? connectNeedError(original) : new NeedError(reason === 'LOGIN_REQUIRED' ? 'LOGIN_REQUIRED' : 'AUTH_STATUS_INVALID', reason === 'LOGIN_REQUIRED' ? '需要登录,请到设置 → 远程主机连接 点登录' : '无法确认当前品牌的登录状态', 'L核验的本品牌登录状态');
|
|
46
|
+
throw Object.assign(error, { technical: loginDiagnostic('verified-login', reason, brand, connection, login), ...(typeof login.providerNeeds[brand]?.officialReason === 'string' ? { officialReason: login.providerNeeds[brand].officialReason } : {}) });
|
|
47
|
+
}
|
|
43
48
|
return { ...connection };
|
|
44
49
|
}
|
|
45
50
|
/** Wait only for L's explicit in-progress evidence. No timer, polling, refresh or authorization. */
|
|
@@ -102,12 +107,12 @@ async function waitForLogin(api, brand, expected, get, signal) {
|
|
|
102
107
|
async function ensureReady(api, brand, signal) {
|
|
103
108
|
const result = await awaitL(() => api.ensureReady(brand), signal);
|
|
104
109
|
if (!result.ok)
|
|
105
|
-
throw Object.assign(
|
|
110
|
+
throw Object.assign(connectNeedError(result.error), { technical: loginDiagnostic('ensure-ready', 'L_ENSURE_READY_FAILED', brand, api.getConnectionState(), api.getLoginPromptState(), result.error.officialReason), ...(typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {}) });
|
|
106
111
|
return result.value;
|
|
107
112
|
}
|
|
108
113
|
export function unwrap(result) {
|
|
109
114
|
if (!result.ok)
|
|
110
|
-
throw Object.assign(
|
|
115
|
+
throw Object.assign(connectNeedError(result.error), typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {});
|
|
111
116
|
return result.value;
|
|
112
117
|
}
|
|
113
118
|
export function validateTarget(t) {
|
|
@@ -155,6 +160,30 @@ export function sameTarget(a, b) {
|
|
|
155
160
|
}
|
|
156
161
|
export function connectProvider(get, options = {}) {
|
|
157
162
|
const agent = options.agent ?? CODEX_AGENT;
|
|
163
|
+
const hostScopes = new WeakMap();
|
|
164
|
+
const hostMethods = new WeakMap();
|
|
165
|
+
const retainHostScope = (runtime, scope) => {
|
|
166
|
+
hostScopes.get(runtime)?.dispose();
|
|
167
|
+
hostScopes.set(runtime, scope);
|
|
168
|
+
runtime.contextSignal = scope.revocation;
|
|
169
|
+
if (hostMethods.has(runtime))
|
|
170
|
+
return;
|
|
171
|
+
const original = { dispose: runtime.dispose, detach: runtime.detach };
|
|
172
|
+
hostMethods.set(runtime, original);
|
|
173
|
+
const release = () => { hostScopes.get(runtime)?.dispose(); hostScopes.delete(runtime); };
|
|
174
|
+
runtime.dispose = async (cause) => { try {
|
|
175
|
+
await original.dispose?.(cause);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
release();
|
|
179
|
+
} };
|
|
180
|
+
runtime.detach = async (cause) => { try {
|
|
181
|
+
await original.detach?.(cause);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
release();
|
|
185
|
+
} };
|
|
186
|
+
};
|
|
158
187
|
// Keep the legacy source SDK wrapper's strip-only import graph free of broker runtime syntax.
|
|
159
188
|
// Cache the initialization promise so concurrent sessions share this provider's binding lifetimes.
|
|
160
189
|
let employee;
|
|
@@ -203,6 +232,9 @@ export function connectProvider(get, options = {}) {
|
|
|
203
232
|
request.localAdmissionCheck?.();
|
|
204
233
|
admissions.set(request, { api, target, connection });
|
|
205
234
|
sourceStage = 'local-admission';
|
|
235
|
+
const locations = api.getContractInfo().workspaceLocations === 1 ? api.getWorkspaceLocationState?.() : undefined;
|
|
236
|
+
if (locations && (locations.phase === 'failed' || locations.phase === 'disposed') && locations.instanceId === connection.instanceId && locations.epoch === connection.epoch && locations.need)
|
|
237
|
+
throw connectNeedError(locations.need);
|
|
206
238
|
await options.local?.admit?.(request, api, connection, target);
|
|
207
239
|
confirmedSource?.assertCurrent();
|
|
208
240
|
}
|
|
@@ -228,88 +260,109 @@ export function connectProvider(get, options = {}) {
|
|
|
228
260
|
request.localAdmissionCheck?.();
|
|
229
261
|
if (!request.sessionId)
|
|
230
262
|
throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
local
|
|
236
|
-
|
|
263
|
+
const host = await hostWorkspaceGuard(get, api, connection, target, request, options.workspaceSource?.workspace, agent.loginProvider);
|
|
264
|
+
try {
|
|
265
|
+
if (!host && options.employee) {
|
|
266
|
+
employee ??= import("./employee-provider.js").then(module => module.createEmployeeProvider(get, options, agent));
|
|
267
|
+
const local = await (await employee)({ api, target, connection, request, stable: signal => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, signal)) });
|
|
268
|
+
if (local) {
|
|
269
|
+
local.loginRequired = () => offerLogin(api, request, get, agent);
|
|
270
|
+
return local;
|
|
271
|
+
}
|
|
237
272
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
273
|
+
if (!host && agent.loginProvider === 'claude' && options.local) {
|
|
274
|
+
const runtime = await options.local(request, api, connection, target);
|
|
275
|
+
if (runtime) {
|
|
276
|
+
try {
|
|
277
|
+
await stable();
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
await runtime.dispose?.();
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
return runtime;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!host) {
|
|
287
|
+
await options.notice?.reconcile(request, api);
|
|
288
|
+
await stable();
|
|
289
|
+
}
|
|
290
|
+
const resolved = await request.timing.run('workspace-resolve', async () => host ? host.resolve() : unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal)));
|
|
291
|
+
await stable();
|
|
292
|
+
host?.assertCurrent();
|
|
293
|
+
if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
|
|
294
|
+
throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
|
|
295
|
+
validateTarget(resolved.target);
|
|
296
|
+
const project = resolved.project;
|
|
297
|
+
const source = resolved.source;
|
|
298
|
+
if (resolved.sessionId !== request.sessionId || !resolved.workspaceId || !resolved.bindingId || !Number.isSafeInteger(resolved.bindingRevision) || resolved.bindingRevision < 1 || !sameTarget(target, resolved.target) || !project || (!host && (source !== undefined && source !== 'legacy-host' || typeof project.name !== 'string' || !project.name || project.name === '.' || project.name === '..' || /[\\/\x00-\x1f\x7f]/.test(project.name) || project.path !== resolved.target.projectRoot + '/' + project.name)))
|
|
299
|
+
throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
|
|
300
|
+
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
301
|
+
throw new NeedError(host ? 'PROJECT_BINDING_INVALID' : 'SESSION_CONTEXT_MISMATCH', host ? '文件来源已改,请新建会话继续;原会话历史仍可查看。' : '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
302
|
+
const details = await request.timing.run('details-open', async () => options.details?.(request, api, connection, target));
|
|
303
|
+
try {
|
|
304
|
+
if (details)
|
|
305
|
+
request.timing.bind(detail => details.record(detail));
|
|
306
|
+
await stable();
|
|
307
|
+
host?.assertCurrent();
|
|
308
|
+
let workspaceNotice = undefined;
|
|
309
|
+
const notice = !host && 'notice' in resolved ? resolved.notice : undefined;
|
|
310
|
+
if (notice) {
|
|
311
|
+
if (!options.notice)
|
|
312
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
|
|
313
|
+
workspaceNotice = await request.timing.run('workspace-notice', () => options.notice.deliver(notice, resolved, request, api));
|
|
314
|
+
await stable();
|
|
315
|
+
}
|
|
316
|
+
if (!options.open)
|
|
317
|
+
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
318
|
+
const observedBinding = host ? undefined : api.getWorkspaceBindingState?.();
|
|
319
|
+
const validateContext = async (ignoreCancellation = false) => {
|
|
320
|
+
const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
|
|
321
|
+
await check();
|
|
322
|
+
if (host && !ignoreCancellation)
|
|
323
|
+
await host.resolve();
|
|
324
|
+
if (observedBinding) {
|
|
325
|
+
const current = api.getWorkspaceBindingState();
|
|
326
|
+
if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
|
|
327
|
+
throw new NeedError('SESSION_CONTEXT_MISMATCH', '当前工作区绑定已改变,原会话操作未执行', '原生会话原绑定');
|
|
328
|
+
}
|
|
329
|
+
await check();
|
|
330
|
+
if (!ignoreCancellation)
|
|
331
|
+
host?.assertCurrent();
|
|
332
|
+
};
|
|
333
|
+
host?.assertCurrent();
|
|
334
|
+
const runtime = await options.open(resolved.target, project, { ...request, ...(host ? { signal: host.signal } : {}), agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details, validateContext });
|
|
242
335
|
try {
|
|
243
336
|
await stable();
|
|
337
|
+
host?.observeRuntime(runtime);
|
|
244
338
|
}
|
|
245
339
|
catch (error) {
|
|
246
340
|
await runtime.dispose?.();
|
|
247
341
|
throw error;
|
|
248
342
|
}
|
|
343
|
+
if (host)
|
|
344
|
+
retainHostScope(runtime, host);
|
|
345
|
+
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
346
|
+
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
347
|
+
runtime.details = details ?? runtime.details;
|
|
348
|
+
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
349
|
+
runtime.connection = { ...connection };
|
|
350
|
+
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
249
351
|
return runtime;
|
|
250
352
|
}
|
|
251
|
-
}
|
|
252
|
-
await options.notice?.reconcile(request, api);
|
|
253
|
-
await stable();
|
|
254
|
-
const resolved = await request.timing.run('workspace-resolve', async () => unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal)));
|
|
255
|
-
await stable();
|
|
256
|
-
if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
|
|
257
|
-
throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
|
|
258
|
-
validateTarget(resolved.target);
|
|
259
|
-
const project = resolved.project;
|
|
260
|
-
if (resolved.sessionId !== request.sessionId || !resolved.workspaceId || !resolved.bindingId || !Number.isSafeInteger(resolved.bindingRevision) || resolved.bindingRevision < 1 || !sameTarget(target, resolved.target) || !project || typeof project.name !== 'string' || !project.name || project.name === '.' || project.name === '..' || /[\\/\x00-\x1f\x7f]/.test(project.name) || project.path !== resolved.target.projectRoot + '/' + project.name)
|
|
261
|
-
throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
|
|
262
|
-
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
263
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
264
|
-
const details = await request.timing.run('details-open', async () => options.details?.(request, api, connection, target));
|
|
265
|
-
try {
|
|
266
|
-
if (details)
|
|
267
|
-
request.timing.bind(detail => details.record(detail));
|
|
268
|
-
await stable();
|
|
269
|
-
let workspaceNotice = undefined;
|
|
270
|
-
if (resolved.notice) {
|
|
271
|
-
if (!options.notice)
|
|
272
|
-
throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
|
|
273
|
-
workspaceNotice = await request.timing.run('workspace-notice', () => options.notice.deliver(resolved.notice, resolved, request, api));
|
|
274
|
-
await stable();
|
|
275
|
-
}
|
|
276
|
-
if (!options.open)
|
|
277
|
-
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
278
|
-
const observedBinding = api.getWorkspaceBindingState?.();
|
|
279
|
-
const validateContext = async (ignoreCancellation = false) => {
|
|
280
|
-
const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
|
|
281
|
-
await check();
|
|
282
|
-
if (observedBinding) {
|
|
283
|
-
const current = api.getWorkspaceBindingState();
|
|
284
|
-
if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
|
|
285
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '当前工作区绑定已改变,原会话操作未执行', '原生会话原绑定');
|
|
286
|
-
}
|
|
287
|
-
await check();
|
|
288
|
-
};
|
|
289
|
-
const runtime = await options.open(resolved.target, project, { ...request, agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details, validateContext });
|
|
290
|
-
try {
|
|
291
|
-
await stable();
|
|
292
|
-
}
|
|
293
353
|
catch (error) {
|
|
294
|
-
|
|
354
|
+
try {
|
|
355
|
+
await details?.flush();
|
|
356
|
+
}
|
|
357
|
+
catch (diagnostic) {
|
|
358
|
+
const code = (value) => value instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'UNKNOWN_FAILURE';
|
|
359
|
+
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能保存;本轮已停止', `原错误代码 ${code(error)};诊断错误代码 ${code(diagnostic)}`);
|
|
360
|
+
}
|
|
295
361
|
throw error;
|
|
296
362
|
}
|
|
297
|
-
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
298
|
-
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
299
|
-
runtime.details = details ?? runtime.details;
|
|
300
|
-
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
301
|
-
runtime.connection = { ...connection };
|
|
302
|
-
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
303
|
-
return runtime;
|
|
304
363
|
}
|
|
305
364
|
catch (error) {
|
|
306
|
-
|
|
307
|
-
await details?.flush();
|
|
308
|
-
}
|
|
309
|
-
catch (diagnostic) {
|
|
310
|
-
const code = (value) => value instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'UNKNOWN_FAILURE';
|
|
311
|
-
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能保存;本轮已停止', `原错误代码 ${code(error)};诊断错误代码 ${code(diagnostic)}`);
|
|
312
|
-
}
|
|
365
|
+
host?.dispose();
|
|
313
366
|
throw error;
|
|
314
367
|
}
|
|
315
368
|
};
|
package/dist/remote.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConnectNeed } from '@ai-workbench/connect/api';
|
|
1
2
|
import type { LoginDiagnostic } from './login-diagnostic.ts';
|
|
2
3
|
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
3
4
|
/** Development-only direct SSH. Product transports must consume L's exported sshArgs (v0.2). */
|
|
@@ -19,6 +20,7 @@ export declare class NeedError extends Error {
|
|
|
19
20
|
requires: string;
|
|
20
21
|
technical?: LoginDiagnostic;
|
|
21
22
|
officialReason?: string;
|
|
23
|
+
connectNeed?: Readonly<ConnectNeed>;
|
|
22
24
|
constructor(code: string, message: string, requires: string);
|
|
23
25
|
}
|
|
24
26
|
export declare function shellQuote(value: string): string;
|
package/dist/remote.js
CHANGED
package/dist/workspace-source.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withFailureEvidence } from "./failure-evidence.js";
|
|
2
|
+
import { connectNeedError } from "./connect-need.js";
|
|
2
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
import { sanitizeDisplayLabel } from "./conversation-details.js";
|
|
4
5
|
import { NeedError } from "./remote.js";
|
|
@@ -82,7 +83,7 @@ export async function confirmLegacyWorkspaceSource(get, api, request, hooks) {
|
|
|
82
83
|
const selected = await api.selectWorkspaceLocation({ instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.id, expectedBindingId: frozen.bindingId, source: 'local' });
|
|
83
84
|
request.signal?.throwIfAborted();
|
|
84
85
|
if (!selected.ok)
|
|
85
|
-
throw
|
|
86
|
+
throw connectNeedError(selected.error);
|
|
86
87
|
const current = await stable(false), value = selected.value, returned = value.items.filter(item => item.workspaceId === workspace.id);
|
|
87
88
|
if (value.phase !== 'ready' || value.instanceId !== connection.instanceId || value.epoch !== connection.epoch || returned.length !== 1 || returned[0].source !== 'local' || returned[0].status !== 'selected' || !returned[0].bindingId || returned[0].bindingId === frozen.bindingId || returned[0].localPath !== workspace.path || current.source !== 'local' || current.status !== 'selected' || current.bindingId !== returned[0].bindingId)
|
|
88
89
|
throw fail('WORKSPACE_SOURCE_SELECTION_UNCONFIRMED');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-workbench/remote-agent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"zod": "4.6.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.
|
|
42
|
+
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.20.tgz",
|
|
43
43
|
"@ai-workbench/network": "file:test/fixtures/ai-workbench-network-0.4.4.tgz",
|
|
44
44
|
"@deepseek-ai/cordis": "4.0.4",
|
|
45
45
|
"@deepseek-ai/cordis-plugin-group": "1.0.4",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"zustand": "4.4.7"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@ai-workbench/connect": "~0.7.
|
|
71
|
+
"@ai-workbench/connect": "~0.7.20",
|
|
72
72
|
"@ai-workbench/network": "~0.4.4",
|
|
73
73
|
"@deepseek-ai/cordis": "4.0.4",
|
|
74
74
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|