@ai-workbench/remote-agent 0.8.15 → 0.8.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- **`@ai-workbench/remote-agent@0.8.15` 发布形态包。模型菜单逐项解析与同次展示目录保持一致;失效选择明确提示重新选择,发送准备和每轮发送仍独立查可信目录。模型型号、推理等级和来源只进技术详情;本机执行说明每个会话首次一句,完整限制可在技术详情查阅。编排已回证0.8.14 Claude在leo本地文件夹首轮成功;0.8.15仍待真机复验,Codex历史EMPLOYEE_EXEC_CLOSED根因仍未查明。X2出固定包及SHA,不publish。通过官方插件 → 添加插件安装,由编排发布;X2不publish。**
1
+ **`@ai-workbench/remote-agent@0.8.17` 发布形态包。Claude、Codex 接入 L0.7.20 已选择的主机文件夹;成功核验来源和目录后才打开技术详情与派发,每次派发前重新核验。L 的具体原因和下一步动作直接给员工。包含0.8.16首次关闭、运行、诊断保存和清理结果留痕;历史原始撤销字段与61秒慢查询根因仍未查明。本版 leo 主机位置/真实员工链路尚未复验。通过官方插件 → 添加插件安装;固定包由编排发布,X2 不 publish。**
2
2
 
3
3
  # 远程ACP插件
4
4
 
5
5
  一个插件提供 **Claude(远程主机)** 和 **Codex(远程主机)**,通过员工主机上的官方CLI运行。Codex保留单次模式;Claude固定会话模式。只用官方模型列表、插件管理和配置入口,不提供自制面板、问答或登录按钮。登录和原生工作区绑定唯一归连接插件L。
6
6
 
7
- 当前版本号为0.8.15;包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;本轮按编排放行准备发布形态包,供 leo 真机自验;X 不执行实际发布,由编排统一发布。开发fixture逐字节保留L0.7.19固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的不完整实现会明确报缺项。依赖范围为`~0.7.19`,L由官方插件管理单独安装,不打进X。组件验证不代表真实开通、模型或leo E通过;HanaMesh另验。
7
+ 当前版本0.8.17,由X2按编排裁定基于0.8.16交付主机来源执行和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.17沿唯一“远程主机连接”设置节记录文件来源;主机来源沿用L既有解析;本地来源接入N0.4.4/S0.4.11员工端点。旧映射可在本轮官方一次确认中选择改用本机文件夹;拒绝、取消或输入/来源变化时不切换、不执行。也可在L设置逐工作区确认沿用原主机关联,新主机位置仍按L现有合同停止为WORKSPACE_LOCATION_PROVIDER_REQUIRED。本地六类工具经Claude公开toolAliases路由到员工电脑:cwd是员工选定目录,Git使用本机仓库,不回退主机同名目录。本地真实员工输入先取得官方当前轮次CAS预期,再调用L0.7.17 acceptLocalWorkspaceUserTurn;核回包后才打开D19和本轮主机目录/执行。每轮localAcceptanceId冻结进原详情sink,迟到结果不借新轮ID;LOCAL_USER_TURN_CHANGED立即停止本轮,不重接纳或退legacy。已用固定真包完成本地D19组件联测;编排已回证0.8.14 Claude/Opus5/Default在leo本地目录首轮写文件成功;该回证不覆盖本0.8.15或Codex,产品验收以各版本leo实际结果为准。本地文件不自动上传,X不猜目录、不选首项。首次说明使用官方普通回复;先经官方会话提交、持久化并读回核对,才向L确认。持久记录不等于员工已看见,GUI另验;日志丢失后的未知claim由L保留,不宣称端到端恰好一次。
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.17或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.18`),失败来源及固定分支保留到 L“登录前技术详情”(`loginDiagnostics:1`),员工正文保持原文。按 L 明确的本品牌 queued/checking/authorizing 状态订阅等待;不以全局checking猜测在途,不新增刷新、轮询、超时或重试。每轮ensureReady仍是合同规定的显式检查,L的新可信结果可以替代旧检查,较早返回不得覆盖它。
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.15对齐L `~0.7.19`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理,X新增设置0。
83
+ 0.8.5统一包原依赖L `~0.7.13`;0.8.16原对齐L `~0.7.19`;当前0.8.17对齐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
 
@@ -124,3 +128,5 @@ D20(0.8.8开发记录):本地来源的Codex原生文件与命令通过固
124
128
  每次目录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
129
 
126
130
  0.8.14 不把空菜单、非穷举 picker、可选 UI category/label 或同值配置通知当作额外发送门槛;每轮仍要求可靠模型入口与 fresh exact setter ACK。本轮完成后的配置变化只记录 warning,保留真实回复与 finish,并明确未获单独的实际执行型号回报。同一身份、代际与可信目标的 needCode/诊断变化只记 info;真实身份、代际、目标或能力失效仍撤回。每次关闭/取消保留 caller、字段前后值和首次观测原因(不等于根因)。新增配置、阈值、超时、重试为 0。
131
+
132
+ 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.15' },
393
+ protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.17' },
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);
@@ -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];
@@ -1,5 +1,4 @@
1
1
  import type { DetailObservation } from './conversation-details.ts';
2
- import { NeedError } from './remote.ts';
3
2
  export interface CloseChange {
4
3
  field: string;
5
4
  before: string | number | boolean | null;
@@ -18,11 +17,7 @@ export interface CloseCause {
18
17
  changes?: readonly CloseChange[];
19
18
  }
20
19
  export type CloseObserver = (cause: CloseCause) => void;
21
- /** Only typed codes, never exception messages, causes, stderr or RPC data. */
22
- export declare function closeCode(error: unknown, fallback?: string): string;
23
- /** Retain trusted typed codes through wrappers; never traverse error text or invoke getters. */
24
- export declare function closeErrorCodes(error: unknown, fallback?: string): string[];
25
- export declare function closeFailure(code: string, message: string, requires: string, codes: readonly string[]): NeedError;
20
+ export { closeCode, closeErrorCodes, closeFailure, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, primaryFailureCode } from './failure-evidence.ts';
26
21
  /** Assign order before abort/kill; persist through the existing asynchronous D19 outbox. */
27
22
  export declare class CloseDiagnostics {
28
23
  private sink;
@@ -1,37 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { NeedError } from "./remote.js";
3
- /** Only typed codes, never exception messages, causes, stderr or RPC data. */
4
- export function closeCode(error, fallback = 'CLOSE_REASON_UNKNOWN') {
5
- return error instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : fallback;
6
- }
7
- /** Retain trusted typed codes through wrappers; never traverse error text or invoke getters. */
8
- export function closeErrorCodes(error, fallback = 'CLOSE_REASON_UNKNOWN') {
9
- const queue = [error], seen = new Set(), codes = new Set();
10
- while (queue.length) {
11
- const next = queue.pop();
12
- if (!(next instanceof NeedError || next instanceof AggregateError) || seen.has(next))
13
- continue;
14
- seen.add(next);
15
- if (next instanceof NeedError) {
16
- const code = Object.getOwnPropertyDescriptor(next, 'code');
17
- if (code && 'value' in code && typeof code.value === 'string' && /^[A-Z][A-Z0-9_]*$/.test(code.value))
18
- codes.add(code.value);
19
- }
20
- const cause = Object.getOwnPropertyDescriptor(next, 'cause');
21
- if (cause && 'value' in cause)
22
- queue.push(cause.value);
23
- if (next instanceof AggregateError) {
24
- const errors = Object.getOwnPropertyDescriptor(next, 'errors');
25
- if (errors && 'value' in errors && Array.isArray(errors.value))
26
- for (const child of errors.value)
27
- queue.push(child);
28
- }
29
- }
30
- return codes.size ? [...codes] : [fallback];
31
- }
32
- export function closeFailure(code, message, requires, codes) {
33
- return Object.assign(new NeedError(code, message, requires), { cause: new AggregateError([...new Set(codes)].map(value => new NeedError(value, '关闭收尾代码 ' + value, '按安全代码核对')), 'CLOSE_AND_DIAGNOSTIC_FAILURES') });
34
- }
3
+ import { closeCode, closeErrorCodes, withFailureEvidence } from "./failure-evidence.js";
4
+ export { closeCode, closeErrorCodes, closeFailure, withFailureEvidence, withFirstFailureEvidence, failureEvidenceSummary, primaryFailureCode } from "./failure-evidence.js";
35
5
  /** Assign order before abort/kill; persist through the existing asynchronous D19 outbox. */
36
6
  export class CloseDiagnostics {
37
7
  sink;
@@ -71,7 +41,7 @@ export class CloseDiagnostics {
71
41
  const payload = Object.freeze({ ...metadata, pendingOperations: Object.freeze([...this.pending.keys()]), closeId: this.id, ...this.parentId ? { parentCloseId: this.parentId } : {}, closeSequence: ++this.sequence, first, stage: this.stage, clock: 'client-monotonic', elapsedMs: performance.now() - this.started });
72
42
  this.firstValue ??= payload;
73
43
  try {
74
- sink({ kind: 'session', severity: 'warning', code: first ? 'REMOTE_CLOSE_FIRST' : 'REMOTE_CLOSE_NEXT', summary: first ? '首次观察到关闭(不等同根因)' : '后续关闭或清理', payload });
44
+ sink({ kind: 'session', severity: 'warning', code: first ? 'REMOTE_CLOSE_FIRST' : 'REMOTE_CLOSE_NEXT', summary: cause.changedFields?.includes('selected-model') ? '最新可信目录已不再提供本轮所选型号,本轮已停止;请在模型菜单核对选择。' : first ? '首次观察到关闭(不等同根因)' : '后续关闭或清理', payload });
75
45
  }
76
46
  catch (error) {
77
47
  this.state.failure ??= { error };
@@ -97,7 +67,7 @@ export async function flushCloseDiagnostics(diagnostics, flush, primary) {
97
67
  }
98
68
  if (reporting.length) {
99
69
  const primaryCodes = primary ? closeErrorCodes(primary.error, 'REMOTE_CLEANUP_UNCONFIRMED') : [];
100
- throw closeFailure('CONVERSATION_DETAILS_FAILED', '关闭诊断保存未完成;原关闭与清理结果需核对。', '首次关闭 ' + String(diagnostics.first?.reason ?? 'CLOSE_REASON_UNKNOWN') + ';清理 ' + (primaryCodes.join(',') || 'closed') + ';保存 ' + reporting.join(','), [...primaryCodes, ...reporting]);
70
+ throw withFailureEvidence(primary?.error ?? new NeedError(primaryCodes[0] ?? 'REMOTE_CLEANUP_UNCONFIRMED', '关闭收尾未确认', '核对实际结果'), reporting.map(code => new NeedError(code, '诊断保存未确认', '只读核对')), 'diagnostic', diagnostics.first?.reason);
101
71
  }
102
72
  if (primary)
103
73
  throw primary.error;
@@ -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<SessionProjectTarget>>;
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
+ }
@@ -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);
@@ -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 new NeedError(result.error.code, result.error.message, result.error.requires);
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,8 +6,9 @@ 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, closeCode, closeErrorCodes } from "./close-diagnostics.js";
11
+ import { CloseDiagnostics, withFailureEvidence, 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";
@@ -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 new NeedError(context.error.code, '无法核验本机工作区,本轮已停止。', '当前本机工作区');
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 new NeedError(response.error.code, '本次员工输入或工作区无法核验,本轮已停止。', 'L 本地真实输入接纳');
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 new NeedError(c.error.code, '无法核验本机工作区,本轮已停止。', '当前本机工作区');
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);
@@ -336,13 +337,21 @@ export function createEmployeeProvider(get, options, agent) {
336
337
  return runtime;
337
338
  }
338
339
  catch (error) {
339
- details?.record({ kind: 'need', severity: 'action-required', code: errorCode(error), summary: asNeed(error).message, payload: { result: 'UNKNOWN' } });
340
+ const reporting = [];
341
+ try {
342
+ details?.record({ kind: 'need', severity: 'action-required', code: errorCode(error), summary: asNeed(error).message, payload: { result: 'UNKNOWN' } });
343
+ }
344
+ catch (failure) {
345
+ reporting.push(failure);
346
+ }
340
347
  try {
341
348
  await details?.flush();
342
349
  }
343
- catch {
344
- throw fail('CONVERSATION_DETAILS_FAILED');
350
+ catch (failure) {
351
+ reporting.push(failure);
345
352
  }
353
+ if (reporting.length)
354
+ throw withFailureEvidence(error, reporting, 'diagnostic', closeDiagnostics?.first?.reason);
346
355
  throw error;
347
356
  }
348
357
  };
@@ -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
+ }
@@ -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
+ }