@ai-workbench/remote-agent 0.8.12 → 0.8.14
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 +11 -7
- package/dist/acp.d.ts +12 -7
- package/dist/acp.js +65 -41
- package/dist/broker-runtime.js +46 -20
- package/dist/broker.d.ts +4 -2
- package/dist/broker.js +18 -9
- package/dist/claude-local.d.ts +2 -1
- package/dist/claude-local.js +61 -34
- package/dist/close-diagnostics.d.ts +56 -0
- package/dist/close-diagnostics.js +104 -0
- package/dist/conversation-details.js +67 -9
- package/dist/employee-endpoint.d.ts +3 -1
- package/dist/employee-endpoint.js +8 -6
- package/dist/employee-exec.d.ts +7 -4
- package/dist/employee-exec.js +39 -29
- package/dist/employee-mcp.d.ts +6 -3
- package/dist/employee-mcp.js +75 -39
- package/dist/employee-provider.js +58 -36
- package/dist/employee-shell.d.ts +2 -0
- package/dist/employee-shell.js +17 -5
- package/dist/employee-ssh.d.ts +2 -0
- package/dist/employee-ssh.js +6 -4
- package/dist/employee-tools.d.ts +3 -1
- package/dist/employee-tools.js +6 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +133 -44
- package/dist/local-user-turn.js +2 -1
- package/dist/model-catalog.d.ts +9 -1
- package/dist/model-catalog.js +63 -5
- package/dist/model-configuration.d.ts +14 -1
- package/dist/model-configuration.js +122 -89
- package/dist/presentation.js +3 -2
- package/dist/provider.d.ts +8 -2
- package/dist/recovery.d.ts +4 -0
- package/dist/rpc-namespace.d.ts +2 -2
- package/dist/rpc-namespace.js +2 -1
- package/dist/session-header.d.ts +4 -0
- package/dist/session-header.js +12 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
**`@ai-workbench/remote-agent@0.8.
|
|
1
|
+
**`@ai-workbench/remote-agent@0.8.14` 发布形态包;消费审定的 B1–B9 校验分类及 X2 首次关闭原因观测,供 leo 真机复验。Claude 型号按本轮 setter 精确回执与同域目录映射确认,并如实注明来源。Codex 上次 `EMPLOYEE_EXEC_CLOSED` 的真机根因仍未查明,本版补充首因留痕,不宣称其已在真机修复。由编排发布后,通过官方插件 → 添加插件安装;X 不执行 publish。**
|
|
2
2
|
|
|
3
3
|
# 远程ACP插件
|
|
4
4
|
|
|
5
5
|
一个插件提供 **Claude(远程主机)** 和 **Codex(远程主机)**,通过员工主机上的官方CLI运行。Codex保留单次模式;Claude固定会话模式。只用官方模型列表、插件管理和配置入口,不提供自制面板、问答或登录按钮。登录和原生工作区绑定唯一归连接插件L。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
当前版本号为0.8.14;包名采用`@ai-workbench/remote-agent`以涵盖两个品牌;本轮按编排放行准备发布形态包,供 leo 真机自验;X 不执行实际发布,由编排统一发布。开发fixture逐字节保留L0.7.19固定包,公开类型与SSH导出从该包消费。运行时仍要求L的`getContractInfo()`同时返回API0.7.0、implemented、native-local-v1;旧L包、draft.3或只有版本号的不完整实现会明确报缺项。依赖范围为`~0.7.19`,L由官方插件管理单独安装,不打进X。组件验证不代表真实开通、模型或leo E通过;HanaMesh另验。
|
|
8
8
|
|
|
9
|
-
L0.7.18
|
|
9
|
+
已包含L0.7.18消费增量:来源确认、输入接纳、登录、本机接纳、输入准备、模型目录及运行时解析的早期失败,用正式`preflightDiagnostics:1` / `preflight-check`进入L「连接前技术详情(含登录与工作区确认)」。阶段、真实来源/状态和错误代码可展开;未知来源为null,旧login-check的完整安全原因保留。记录不选择来源、不接纳输入、不自动重发;无原生会话的菜单查询仍在模型设置报告。当前工程和设置组件验证不等于leo GUI验收。
|
|
10
|
+
|
|
11
|
+
0.8.13包含L0.7.19消费增量:官方JSONL将省略的`delegationDepth`保存为`0`;X与L只在live/persisted header核对中承认这两种表示等价,实时身份、轮次和其他字段仍严格核验。`REMOTE_DETAIL_SCOPE_INVALID`说明本次输入与当前会话的关联无法核对,不再一律提示组件过旧。已确认的本地来源不重复询问;本轮leo只读取证,修复后的真机路径另验。
|
|
10
12
|
|
|
11
13
|
## 员工路径
|
|
12
14
|
|
|
@@ -22,11 +24,11 @@ L0.7.18消费增量待X统一0.8.12:来源确认、输入接纳、登录、本
|
|
|
22
24
|
|
|
23
25
|
文本、思考、工具步骤/正文/补丁、计划、通知映射为官方普通流;远端工具不变成本机工具调用。图片输入由官方附件服务读取已核验字节,经ACP传给主机,不传员工文件路径。非文本输出无法得到官方持久附件引用时,明确说明收到的内容类型与所缺能力,不假造图片引用。远端结构化提问、未映射扩展会明确提示并取消,不静默丢弃。
|
|
24
26
|
|
|
25
|
-
首次系统/开发者上下文按协商的ACP系统能力传入。**首版不支持:已有Claude会话中立即生效的系统/开发者上下文变更。** 固定适配器说明恢复后的变更要到下次上下文整理才生效;X
|
|
27
|
+
首次系统/开发者上下文按协商的ACP系统能力传入。**首版不支持:已有Claude会话中立即生效的系统/开发者上下文变更。** 固定适配器说明恢复后的变更要到下次上下文整理才生效;X按已有公开 resume/load 传送新要求,技术详情注明延迟生效,继续本轮;不宣称本轮已立即采用,不自动整理或重试。实际 RPC 拒绝或缺少公开更新能力仍会明确停止。未声明系统能力的agent会明确报缺项。
|
|
26
28
|
|
|
27
29
|
`usage_update`的used/size是上下文占用,不能当作本轮输入/输出计费量;远端真实prompt响应usage及Claude quota令牌计数是会话累计,按保存在当前实例/恢复标识中的可信基线减差后进入DSH本轮用量;重复值不重复结算。恢复缺少基线、计数缺失、回退或来源改变时明确不计入该轮,重建可信基线,不能把历史用量算成本轮。
|
|
28
30
|
|
|
29
|
-
会话模式跨轮保留同一ACP客户端和主机进程;关闭实际agent/session或插件时精确清理。重开恢复优先协商resume,无能力时明确使用load,不静默新建。恢复标识绑定品牌、员工目标、DSH会话和L工作区;复制/分叉不能沿用父远端会话。主机来源沿用D24同实例自动恢复/安全重建。本地来源重开后原MCP socket失效,先由既有S同实例条件流程结束旧broker再重建,不代答旧审批;未获真实清理确认时保留未知,不自动重放。无TTL
|
|
31
|
+
会话模式跨轮保留同一ACP客户端和主机进程;关闭实际agent/session或插件时精确清理。重开恢复优先协商resume,无能力时明确使用load,不静默新建。恢复标识绑定品牌、员工目标、DSH会话和L工作区;复制/分叉不能沿用父远端会话。主机来源沿用D24同实例自动恢复/安全重建。本地来源重开后原MCP socket失效,先由既有S同实例条件流程结束旧broker再重建,不代答旧审批;未获真实清理确认时保留未知,不自动重放。无TTL或隐式淘汰上限。缺私有恢复 pointer 只撤回恢复资格;只有原 slot 不存在已核实,或同 UUID 条件结束与 list 消失均确认,才新建并仅使用官方本次接纳的输入 ID。纯发送前失败经确认清理后不永久封住新轮;真实输入送达未知、dirty 或清理未知仍阻断重放。
|
|
30
32
|
|
|
31
33
|
Codex单次模式首版仅文本,不转接中途审批;图片或系统/开发者上下文需使用会话模式。文件传输、远端VFS与自动同步不在本轮。其他模型不锁定、不拦截、不记录。
|
|
32
34
|
|
|
@@ -72,9 +74,9 @@ D22阶段耗时进入技术详情;D23文件审批用中文动作,原工具
|
|
|
72
74
|
|
|
73
75
|
D21(0.8.5):两个品牌的官方模型菜单从 L 的 `modelCatalogTarget:1` / `getModelCatalogTarget` 取得当前可信员工目标,再只读调用 S 的 `aw-model-catalog --provider codex|claude`。目录按 host-user 作用域展示完整模型与逐模型推理等级,保留远端标识原值;失败显示“无法获取模型列表:原因”,不登录、不创建项目、不返回替代模型。L 必须宣告该正式能力,S 必须安装保护入口;这两项未就绪时不能据组件测试声称菜单可用。
|
|
74
76
|
|
|
75
|
-
每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude
|
|
77
|
+
每轮先核对目录代次和所选模型,再经 ACP 逐项配置模型及推理等级,收到完整配置确认后才发送输入。Claude 别名每轮须有精确的模型配置成功回执,并由同一员工主机、同一连接代际的目录唯一映射到具体型号。具体型号注明来自主机目录,会话未单独回报;旧恢复记录不作本轮证据。目录缺项、映射不唯一、回执不符或身份/代际变化时不发送输入。真实回复追加所选值和远端确认值,并在恢复标识与技术详情保留,确认配置不等于验证推理效果。连接/身份/目录变化清除旧确认;远端清理待定或结果未知时阻止派发,不自动清理重试或另开替代会话。Codex 单次模式当前不能确认此选择,会在发送前明确拒绝。新增设置 0;本轮工程证据、真实只读链路与产品未验项见 `docs/evidence/2026-10-22-X2-D21/`。
|
|
76
78
|
|
|
77
|
-
0.8.5统一包原依赖L `~0.7.13`;当前0.8.
|
|
79
|
+
0.8.5统一包原依赖L `~0.7.13`;当前0.8.14对齐L `~0.7.19`。D8模型目录双事件长期监听保留;同epoch的recovery提示/失败消息变化不触发执行状态失效,真实实例/epoch/phase/可信目标/登录变化仍失效。完整D9两品牌正文/状态/就绪门呈现按编排00:50允许的范围记下一版,本版不宣称已消费完成;L的connectionMessages.administratorAfterMs仍只由L管理,X新增设置0。
|
|
78
80
|
|
|
79
81
|
D18(0.8.6):Codex 需要主机自有 adapter `2.2.2-aw.1` 的明确新建会话上下文政策,官方 CLI 仍为未改的0.160.1。系统/开发者要求仅在新建时生效;恢复时与初建摘要相同则正常恢复且省略新建专用字段,不声称恢复接口会更新指令。要求不同或清空时,在同一真实回复说明“会话要求已变化,已新建 Codex 会话继续处理,原会话仍保留。”,并新建远端承接公开对话,保留旧标识和查询记录;旧/非法初建摘要明确拒绝,不能猜测。
|
|
80
82
|
|
|
@@ -120,3 +122,5 @@ D20(0.8.8开发记录):本地来源的Codex原生文件与命令通过固
|
|
|
120
122
|
0.8.12目录诊断修复:入口为“设置 → 模型 → 模型目录诊断”,官方既有 settings.models.footer 公开扩展位直接显示;“内置插件 → 远程助手参数”也可只读查看,即使参数命名空间暂未提供也保留入口;参数编辑使用当前插件公开Loader entry id对应的官方ConfigForms。该页只读本机记录,刷新不查询主机、不登录、不发送待发送原话。模型页原错误行仍通过公开提供方目录发布,诊断数据不再调用directory.replace或发布适配器拓扑变化;changed只负责真实连接/目标代次变化,菜单提示指向上述实际入口。
|
|
121
123
|
|
|
122
124
|
每次目录SSH保独立事件ID、品牌、UTC起止、耗时、PID、L实例/epoch/phase、严格L argv/目标/known_hosts/ProxyCommand、退出码/信号、完整脱敏stderr与原字节数/SHA256、stdout字节数/SHA及主机typed错误;不记录凭据或整份模型目录。日志在官方profile的 `ai-workbench-remote-agent/catalog-diagnostics/<eventId>-started.json` 与 `-closed.json`,700目录/600文件、fsync;无自动过期,最新两品牌在界面可展开,旧调用保留不可变历史。容量沿现有`acpMaxMessageBytes`,超限停止并标记stderrComplete=false,UTF-8替换另显式标记,未返回部分模型;保存失败明确显示仅内存,不冒称耐久。旧0811丢失的stderr无法还原。同品牌、instanceId、epoch只合并仍在进行的查询;完成后不缓存,身份/代际/可信目标改变即作废。调用方各持独立lease/取消与错误身份,一个取消不会取消其他等待者;全员取消才撤销该次查询,取消中的旧进程结束前同作用域新调用等待清理。登录检查的显示变化不会取消无需登录的host-user目录。此次没有增加超时、重试、SSH连接复用或产品设置;不等于255、Claude主机错误及N层根因已修复,也未进行leo GUI验收。
|
|
125
|
+
|
|
126
|
+
0.8.14 不把空菜单、非穷举 picker、可选 UI category/label 或同值配置通知当作额外发送门槛;每轮仍要求可靠模型入口与 fresh exact setter ACK。本轮完成后的配置变化只记录 warning,保留真实回复与 finish,并明确未获单独的实际执行型号回报。同一身份、代际与可信目标的 needCode/诊断变化只记 info;真实身份、代际、目标或能力失效仍撤回。每次关闭/取消保留 caller、字段前后值和首次观测原因(不等于根因)。新增配置、阈值、超时、重试为 0。
|
package/dist/acp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CloseDiagnostics, type CloseCause } from './close-diagnostics.ts';
|
|
1
2
|
import * as stable from '@agentclientprotocol/sdk';
|
|
2
3
|
import * as acp from '@agentclientprotocol/sdk/unstable';
|
|
3
4
|
import { NeedError, type Need } from './remote.ts';
|
|
@@ -9,6 +10,7 @@ import { type ModelConfigurationSnapshot } from './model-configuration.ts';
|
|
|
9
10
|
import type { EmployeeRouting } from './employee-relay.ts';
|
|
10
11
|
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
11
12
|
export interface AcpHooks {
|
|
13
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
12
14
|
timing?: StageTiming;
|
|
13
15
|
onUpdate?: (notification: acp.SessionNotification) => void;
|
|
14
16
|
onResponse?: (response: acp.PromptResponse) => void;
|
|
@@ -36,6 +38,7 @@ export type AcpStartMethod = 'session/new' | 'session/resume' | 'session/load';
|
|
|
36
38
|
export declare class AcpSession {
|
|
37
39
|
private connection;
|
|
38
40
|
private hooks;
|
|
41
|
+
private closeDiagnostics;
|
|
39
42
|
private sessionId?;
|
|
40
43
|
private turnSignal?;
|
|
41
44
|
private turnToken?;
|
|
@@ -44,6 +47,7 @@ export declare class AcpSession {
|
|
|
44
47
|
private negotiated;
|
|
45
48
|
private method?;
|
|
46
49
|
private ready;
|
|
50
|
+
private closeRequested;
|
|
47
51
|
private updating;
|
|
48
52
|
private cwd?;
|
|
49
53
|
private systemPrompt;
|
|
@@ -55,15 +59,13 @@ export declare class AcpSession {
|
|
|
55
59
|
private configRevision;
|
|
56
60
|
private selected?;
|
|
57
61
|
private selecting;
|
|
58
|
-
private resolvedEvidence?;
|
|
59
62
|
private pendingConfigurations;
|
|
63
|
+
private dispatchedPrompts;
|
|
64
|
+
get promptDispatchCount(): number;
|
|
60
65
|
get modelConfiguration(): ModelConfigurationSnapshot | undefined;
|
|
61
|
-
get resolvedModelEvidence(): Readonly<{
|
|
62
|
-
modelValue: string;
|
|
63
|
-
resolvedModel: string;
|
|
64
|
-
}> | undefined;
|
|
65
66
|
get models(): readonly import("./model-configuration.ts").ModelCatalogValue[];
|
|
66
67
|
private receiveConfiguration;
|
|
68
|
+
private pendingConfiguration;
|
|
67
69
|
private resetConfiguration;
|
|
68
70
|
private lifecycleConfiguration;
|
|
69
71
|
selectModel(model: string, effort?: string, signal?: AbortSignal): Promise<ModelConfigurationSnapshot>;
|
|
@@ -77,6 +79,9 @@ export declare class AcpSession {
|
|
|
77
79
|
private need;
|
|
78
80
|
private checkReporting;
|
|
79
81
|
constructor(transport: acp.Stream | acp.AgentApp | stable.AgentApp, hooks: AcpHooks);
|
|
82
|
+
/** A rejected request on an already-closed SDK never reaches this write.
|
|
83
|
+
* Once a transport write starts, failure remains delivery-unknown. */
|
|
84
|
+
private observeDispatch;
|
|
80
85
|
private observe;
|
|
81
86
|
get id(): string | undefined;
|
|
82
87
|
get capabilities(): Readonly<acp.AgentCapabilities>;
|
|
@@ -92,8 +97,8 @@ export declare class AcpSession {
|
|
|
92
97
|
start(cwd: string, resumeId?: string, signal?: AbortSignal, context?: AcpStartContext, prompt?: acp.ContentBlock[]): Promise<string>;
|
|
93
98
|
/** Codex context is immutable after creation; keep the old remote session intact. */
|
|
94
99
|
newContextSession(context: AcpStartContext, signal?: AbortSignal): Promise<string>;
|
|
95
|
-
updateContext(context: AcpStartContext, signal?: AbortSignal): Promise<AcpStartMethod | undefined>;
|
|
100
|
+
updateContext(context: AcpStartContext, signal?: AbortSignal, force?: boolean): Promise<AcpStartMethod | undefined>;
|
|
96
101
|
prompt(input: string | acp.ContentBlock[], signal?: AbortSignal): Promise<acp.StopReason>;
|
|
97
|
-
close(): void;
|
|
102
|
+
close(cause?: CloseCause, error?: Error): void;
|
|
98
103
|
}
|
|
99
104
|
export declare function acpNeed(error: unknown): NeedError;
|
package/dist/acp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CloseDiagnostics } from "./close-diagnostics.js";
|
|
1
2
|
import * as acp from '@agentclientprotocol/sdk/unstable';
|
|
2
3
|
import { NeedError } from "./remote.js";
|
|
3
4
|
import { employeeFailure } from "./presentation.js";
|
|
@@ -10,6 +11,7 @@ import { ModelConfiguration, ModelConfigurationError } from "./model-configurati
|
|
|
10
11
|
export class AcpSession {
|
|
11
12
|
connection;
|
|
12
13
|
hooks;
|
|
14
|
+
closeDiagnostics;
|
|
13
15
|
sessionId;
|
|
14
16
|
turnSignal;
|
|
15
17
|
turnToken;
|
|
@@ -18,6 +20,7 @@ export class AcpSession {
|
|
|
18
20
|
negotiated = {};
|
|
19
21
|
method;
|
|
20
22
|
ready = false;
|
|
23
|
+
closeRequested = false;
|
|
21
24
|
updating = false;
|
|
22
25
|
cwd;
|
|
23
26
|
systemPrompt = '';
|
|
@@ -29,33 +32,29 @@ export class AcpSession {
|
|
|
29
32
|
configRevision = 0;
|
|
30
33
|
selected;
|
|
31
34
|
selecting = false;
|
|
32
|
-
resolvedEvidence;
|
|
33
35
|
pendingConfigurations = new Map();
|
|
36
|
+
dispatchedPrompts = 0;
|
|
37
|
+
get promptDispatchCount() { return this.dispatchedPrompts; }
|
|
34
38
|
get modelConfiguration() { return this.configuration?.snapshot; }
|
|
35
|
-
get resolvedModelEvidence() { return this.resolvedEvidence; }
|
|
36
39
|
get models() { if (!this.configuration)
|
|
37
40
|
throw new ModelConfigurationError('远端没有提供完整配置列表'); return this.configuration.models; }
|
|
38
|
-
receiveConfiguration(options) {
|
|
41
|
+
receiveConfiguration(options, routing) {
|
|
39
42
|
this.configRevision++;
|
|
40
43
|
if (this.configuration)
|
|
41
44
|
this.configuration.update(options);
|
|
42
45
|
else
|
|
43
46
|
this.configuration = new ModelConfiguration(options, async (configId, value) => {
|
|
44
47
|
const result = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId, value }, { cancellationSignal: this.turnSignal });
|
|
45
|
-
if (this.resolvedEvidence && (this.configuration?.snapshot.model !== this.resolvedEvidence.modelValue || (configId === this.configuration?.snapshot.modelConfigId && value !== this.resolvedEvidence.modelValue)))
|
|
46
|
-
this.resolvedEvidence = undefined;
|
|
47
48
|
this.hooks.broker?.acknowledge(acp.methods.agent.session.setConfigOption);
|
|
48
49
|
return result;
|
|
49
|
-
});
|
|
50
|
-
if (this.resolvedEvidence && this.configuration.snapshot.model !== this.resolvedEvidence.modelValue)
|
|
51
|
-
this.resolvedEvidence = undefined;
|
|
50
|
+
}, reason => this.detail({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_INFORMATION', summary: ({ PICKER_INCOMPLETE: '远端菜单没有穷举当前或所选值;以本轮精确配置回执为准。', CONFIG_UI_METADATA_INVALID: '远端配置的显示资料不完整;没有改变本轮选择。', UNRELATED_CONFIG_IGNORED: '远端有未使用的界面配置;未把它当模型配置入口。', OPTIONAL_CATEGORY_OMITTED: '远端省略了显示类别;继续使用本会话已核实的配置入口。', SAME_SELECTION_UPDATE: '收到同值或无关配置更新,本轮真实配置回执仍有效。' })[reason], payload: { reason } }), routing);
|
|
52
51
|
}
|
|
53
|
-
|
|
52
|
+
pendingConfiguration(sessionId, options) { const pending = this.pendingConfigurations.get(sessionId) ?? []; pending.push(options); this.pendingConfigurations.set(sessionId, pending); }
|
|
53
|
+
resetConfiguration(clearRoles = true) {
|
|
54
54
|
this.selected = undefined;
|
|
55
|
-
this.resolvedEvidence = undefined;
|
|
56
55
|
if (this.configuration) {
|
|
57
56
|
try {
|
|
58
|
-
this.
|
|
57
|
+
this.configuration.invalidate(clearRoles);
|
|
59
58
|
}
|
|
60
59
|
catch { /* Invalidated until this lifecycle operation supplies new evidence. */ }
|
|
61
60
|
}
|
|
@@ -64,14 +63,22 @@ export class AcpSession {
|
|
|
64
63
|
lifecycleConfiguration(result, revision) {
|
|
65
64
|
const pending = this.pendingConfigurations.get(this.sessionId);
|
|
66
65
|
this.pendingConfigurations.clear();
|
|
67
|
-
if (pending)
|
|
68
|
-
this
|
|
66
|
+
if (pending) {
|
|
67
|
+
// A lifecycle response can identify selectors for this same session even
|
|
68
|
+
// when newer notifications omit UX categories. It never supplies an ACK.
|
|
69
|
+
if (result.configOptions)
|
|
70
|
+
try {
|
|
71
|
+
this.receiveConfiguration(result.configOptions);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
this.detail({ kind: 'session', severity: 'info', code: 'MODEL_CONFIGURATION_INFORMATION', summary: '会话建立回执的配置资料由本会话较新通知重新核对,仍需本轮真实配置回执。', payload: { reason: 'LIFECYCLE_CONFIGURATION_SUPERSEDED' } });
|
|
75
|
+
}
|
|
76
|
+
for (const options of pending)
|
|
77
|
+
this.receiveConfiguration(options);
|
|
78
|
+
}
|
|
69
79
|
// A notification observed during the operation is newer than its in-flight response.
|
|
70
80
|
else if (this.configRevision === revision && result.configOptions)
|
|
71
81
|
this.receiveConfiguration(result.configOptions);
|
|
72
|
-
const model = this.configuration?.snapshot.model;
|
|
73
|
-
if (!pending && this.configRevision === revision + 1 && model && typeof result.models?.currentModelId === 'string' && result.models.currentModelId)
|
|
74
|
-
this.resolvedEvidence = { modelValue: model, resolvedModel: result.models.currentModelId };
|
|
75
82
|
}
|
|
76
83
|
async selectModel(model, effort, signal) {
|
|
77
84
|
if (!this.ready || !this.sessionId)
|
|
@@ -104,7 +111,14 @@ export class AcpSession {
|
|
|
104
111
|
reports = [];
|
|
105
112
|
reportingFailure;
|
|
106
113
|
timing = new StageTiming();
|
|
107
|
-
stage(phase, operation) {
|
|
114
|
+
async stage(phase, operation) { this.closeDiagnostics.stage = phase; try {
|
|
115
|
+
return await this.closeDiagnostics.run(phase, () => (this.hooks.timing ?? this.timing).run(phase, operation));
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (['acp-session-new', 'acp-session-resume', 'acp-session-load', 'acp-initialize'].includes(phase))
|
|
119
|
+
this.closeDiagnostics.record({ layer: 'acp', initiator: phase === 'acp-session-new' ? 'acp.session-new.failure' : 'acp.lifecycle.failure', reason: acpNeed(error).code, operation: phase, action: 'failure' });
|
|
120
|
+
throw error;
|
|
121
|
+
} }
|
|
108
122
|
get diagnostics() { return this.reports.map(entry => sanitizeObservation(entry)); }
|
|
109
123
|
detail(observation, hooks = this.hooks) {
|
|
110
124
|
const safe = sanitizeObservation(observation);
|
|
@@ -113,8 +127,10 @@ export class AcpSession {
|
|
|
113
127
|
hooks.onDetail?.(safe);
|
|
114
128
|
}
|
|
115
129
|
catch (cause) {
|
|
130
|
+
const first = !this.reportingFailure;
|
|
116
131
|
this.reportingFailure ??= { cause };
|
|
117
|
-
|
|
132
|
+
if (first)
|
|
133
|
+
this.close({ layer: 'acp', initiator: 'acp.reporting-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' });
|
|
118
134
|
}
|
|
119
135
|
}
|
|
120
136
|
need(need, hooks = this.hooks) {
|
|
@@ -125,12 +141,13 @@ export class AcpSession {
|
|
|
125
141
|
throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能记录;本轮未继续发送', '可用的技术详情记录渠道'); }
|
|
126
142
|
constructor(transport, hooks) {
|
|
127
143
|
this.hooks = hooks;
|
|
144
|
+
this.closeDiagnostics = hooks.closeDiagnostics ?? new CloseDiagnostics(event => this.detail(event));
|
|
128
145
|
this.timing.bind(detail => this.detail(detail));
|
|
129
146
|
const app = acp.client({ name: '@ai-workbench/remote-agent' })
|
|
130
147
|
.onNotification(acp.methods.client.session.update, ({ params: notification }) => {
|
|
131
148
|
if (this.creatingReplacement) {
|
|
132
149
|
if (notification.sessionId !== this.sessionId && notification.update.sessionUpdate === 'config_option_update')
|
|
133
|
-
this.
|
|
150
|
+
this.pendingConfiguration(notification.sessionId, notification.update.configOptions);
|
|
134
151
|
this.detail({ kind: 'session', severity: 'info', code: 'ACP_IDLE_UPDATE', summary: '新会话建立期间的轨迹,未作为正文处理', method: 'session/update', payload: notification });
|
|
135
152
|
return;
|
|
136
153
|
}
|
|
@@ -140,9 +157,8 @@ export class AcpSession {
|
|
|
140
157
|
}
|
|
141
158
|
const meta = record(notification._meta);
|
|
142
159
|
if (notification.update.sessionUpdate === 'config_option_update') {
|
|
143
|
-
this.resolvedEvidence = undefined;
|
|
144
160
|
if (!this.sessionId)
|
|
145
|
-
this.
|
|
161
|
+
this.pendingConfiguration(notification.sessionId, notification.update.configOptions);
|
|
146
162
|
else
|
|
147
163
|
try {
|
|
148
164
|
this.receiveConfiguration(notification.update.configOptions);
|
|
@@ -258,21 +274,27 @@ export class AcpSession {
|
|
|
258
274
|
this.detail({ kind: 'extension', severity: 'warning', code: 'ACP_UNKNOWN_NOTIFICATION', summary: '收到未知远端扩展通知,已保留技术记录', method, payload: params });
|
|
259
275
|
});
|
|
260
276
|
if ('readable' in transport && 'writable' in transport)
|
|
261
|
-
this.connection = app.connect(isolateRpcIds(this.observe(transport)));
|
|
277
|
+
this.connection = app.connect(isolateRpcIds(this.observe(transport), message => this.observeDispatch(message)));
|
|
262
278
|
else {
|
|
263
279
|
// Stable and unstable apps use distinct private symbols. Bridge only their
|
|
264
280
|
// public stream interfaces so either SDK entry point remains supported.
|
|
265
281
|
const outward = new TransformStream();
|
|
266
282
|
const inward = new TransformStream();
|
|
267
283
|
const peer = transport.connect({ readable: outward.readable, writable: inward.writable });
|
|
268
|
-
this.connection = app.connect(isolateRpcIds(this.observe({ readable: inward.readable, writable: outward.writable })));
|
|
269
|
-
|
|
270
|
-
void
|
|
284
|
+
this.connection = app.connect(isolateRpcIds(this.observe({ readable: inward.readable, writable: outward.writable }), message => this.observeDispatch(message)));
|
|
285
|
+
const observeConnection = this.closeDiagnostics.capture();
|
|
286
|
+
void this.connection.closed.then(() => { observeConnection({ layer: 'acp', initiator: 'acp.connection-closed', reason: 'ACP_CONNECTION_CLOSED', action: 'close' }); peer.close(); });
|
|
287
|
+
void peer.closed.then(() => { observeConnection({ layer: 'acp', initiator: 'acp.peer-closed', reason: 'ACP_PEER_CLOSED', action: 'close' }); this.connection.close(); });
|
|
271
288
|
}
|
|
272
289
|
}
|
|
290
|
+
/** A rejected request on an already-closed SDK never reaches this write.
|
|
291
|
+
* Once a transport write starts, failure remains delivery-unknown. */
|
|
292
|
+
observeDispatch(message) { if (record(message)?.method === 'session/prompt' && Object.hasOwn(message, 'id'))
|
|
293
|
+
this.dispatchedPrompts++; }
|
|
273
294
|
observe(stream) {
|
|
274
295
|
// Observe already-parsed public Stream messages: no NDJSON parsing, second
|
|
275
296
|
// byte buffer, message-size policy, or transport fallback is introduced.
|
|
297
|
+
const observe = this.closeDiagnostics.capture();
|
|
276
298
|
const readable = stream.readable.pipeThrough(new TransformStream({ transform: (message, controller) => {
|
|
277
299
|
const raw = record(message);
|
|
278
300
|
const params = record(raw?.params);
|
|
@@ -289,13 +311,13 @@ export class AcpSession {
|
|
|
289
311
|
return;
|
|
290
312
|
}
|
|
291
313
|
controller.enqueue(message);
|
|
292
|
-
} }));
|
|
314
|
+
}, flush: () => observe({ layer: 'acp', initiator: 'acp.read-eof', reason: 'ACP_STREAM_EOF', action: 'eof' }) }));
|
|
293
315
|
return { readable, writable: stream.writable };
|
|
294
316
|
}
|
|
295
317
|
get id() { return this.sessionId; }
|
|
296
318
|
get capabilities() { return this.negotiated; }
|
|
297
319
|
get startMethod() { return this.method; }
|
|
298
|
-
get checkpoint() { return this.ready && !this.busy && !this.updating && !this.selecting && this.sessionId ? { sessionId: this.sessionId, capabilities: recoveryCapabilities(this.negotiated), ...(this.configuration ? { modelConfiguration: { configOptions: JSON.parse(JSON.stringify(this.configuration.snapshot.configOptions)),
|
|
320
|
+
get checkpoint() { return this.ready && !this.busy && !this.updating && !this.selecting && this.sessionId ? { sessionId: this.sessionId, capabilities: recoveryCapabilities(this.negotiated), ...(this.configuration ? { modelConfiguration: { configOptions: JSON.parse(JSON.stringify(this.configuration.snapshot.configOptions)), selectorRoles: { model: this.configuration.snapshot.modelConfigId, effort: this.configuration.snapshot.effortConfigId } } } : {}) } : undefined; }
|
|
299
321
|
restore(cwd, saved, context, verifiedSameContext = false) {
|
|
300
322
|
if (context.employeeRouting)
|
|
301
323
|
throw new NeedError('EMPLOYEE_CHECKPOINT_STALE', '本机工具通道需要重新建立,旧审批没有代答。', '按原主机实例安全结束后新建本机会话');
|
|
@@ -311,13 +333,12 @@ export class AcpSession {
|
|
|
311
333
|
// Old journals may lack new-only metadata; never manufacture it for them.
|
|
312
334
|
if (this.codexNewSessionOnly && !verifiedSameContext)
|
|
313
335
|
this.validateNewContext(this.systemPrompt, context.systemPrompt !== undefined);
|
|
336
|
+
// Legacy currentModelId/resolvedEvidence is not a per-turn setter ACK and
|
|
337
|
+
// never proves the concrete model. Only restore selectable config options.
|
|
314
338
|
if (saved.modelConfiguration) {
|
|
315
|
-
this.receiveConfiguration(saved.modelConfiguration.configOptions);
|
|
316
|
-
|
|
317
|
-
for (const update of saved.configUpdates ?? []) {
|
|
318
|
-
this.resolvedEvidence = undefined;
|
|
339
|
+
this.receiveConfiguration(saved.modelConfiguration.configOptions, saved.modelConfiguration.selectorRoles);
|
|
340
|
+
for (const update of saved.configUpdates ?? [])
|
|
319
341
|
this.receiveConfiguration(update);
|
|
320
|
-
}
|
|
321
342
|
}
|
|
322
343
|
this.hooks.broker?.finishRecovery();
|
|
323
344
|
this.detail({ kind: 'need', severity: 'info', code: 'ACP_CLIENT_REATTACHED', summary: '已接回原模型会话', payload: { sessionId: saved.sessionId } });
|
|
@@ -364,12 +385,12 @@ export class AcpSession {
|
|
|
364
385
|
throw new NeedError('SESSION_ALREADY_STARTED', '此连接已用于建立会话,不能转用工作区或身份', '新的独立远端连接');
|
|
365
386
|
this.startAttempted = true;
|
|
366
387
|
this.employeeMeta = context?.employeeExecution;
|
|
367
|
-
const abort = () => this.
|
|
388
|
+
const abort = () => this.close({ layer: 'acp', initiator: 'acp.start-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '会话建立已取消', '重新连接主机'));
|
|
368
389
|
signal?.throwIfAborted();
|
|
369
390
|
signal?.addEventListener('abort', abort, { once: true });
|
|
370
391
|
try {
|
|
371
392
|
const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
|
|
372
|
-
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.14' },
|
|
373
394
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1 } } } : {}) },
|
|
374
395
|
}, { cancellationSignal: signal }));
|
|
375
396
|
this.hooks.broker?.acknowledge(acp.methods.agent.initialize);
|
|
@@ -453,7 +474,7 @@ export class AcpSession {
|
|
|
453
474
|
this.ready = false;
|
|
454
475
|
this.creatingReplacement = true;
|
|
455
476
|
this.pendingConfigurations.clear();
|
|
456
|
-
const abort = () => this.
|
|
477
|
+
const abort = () => this.close({ layer: 'acp', initiator: 'acp.new-context-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '新会话建立已取消,远端状态需确认', '管理员核对远端状态'));
|
|
457
478
|
signal?.addEventListener('abort', abort, { once: true });
|
|
458
479
|
try {
|
|
459
480
|
const revision = this.resetConfiguration();
|
|
@@ -481,32 +502,34 @@ export class AcpSession {
|
|
|
481
502
|
signal?.removeEventListener('abort', abort);
|
|
482
503
|
}
|
|
483
504
|
}
|
|
484
|
-
async updateContext(context, signal) {
|
|
505
|
+
async updateContext(context, signal, force = false) {
|
|
485
506
|
this.checkReporting();
|
|
486
507
|
if (!this.ready || !this.sessionId || !this.cwd)
|
|
487
508
|
throw new NeedError('SESSION_REQUIRED', '我需要先建立远端会话', 'ACP 会话');
|
|
488
509
|
if (this.busy || this.updating || this.selecting)
|
|
489
510
|
throw new NeedError('TURN_BUSY', '此远端会话正在处理上一轮或更新配置', '上一轮结束');
|
|
490
511
|
const text = context.systemPrompt ?? '';
|
|
491
|
-
if (text === this.systemPrompt)
|
|
512
|
+
if (!force && text === this.systemPrompt)
|
|
492
513
|
return undefined;
|
|
493
514
|
this.validateContext(context);
|
|
515
|
+
if (force && (!this.negotiated.sessionCapabilities?.systemPrompt || !this.negotiated.sessionCapabilities?.resume && !this.negotiated.loadSession))
|
|
516
|
+
throw new NeedError('SYSTEM_PROMPT_UPDATE_UNSUPPORTED', '远端没有可传送更新要求的公开恢复能力', 'session/resume 或 session/load 的 systemPrompt 能力');
|
|
494
517
|
const resume = !!this.negotiated.sessionCapabilities?.resume;
|
|
495
518
|
signal?.throwIfAborted();
|
|
496
519
|
this.updating = true;
|
|
497
520
|
this.ready = false;
|
|
498
|
-
const abort = () => this.
|
|
521
|
+
const abort = () => this.close({ layer: 'acp', initiator: 'acp.update-context-signal', reason: 'ABORTED', action: 'abort' }, new NeedError('ABORTED', '上下文更新已取消,原会话状态需确认', '重新连接原会话'));
|
|
499
522
|
signal?.addEventListener('abort', abort, { once: true });
|
|
500
523
|
try {
|
|
501
524
|
const method = resume ? acp.methods.agent.session.resume : acp.methods.agent.session.load;
|
|
502
525
|
const params = { sessionId: this.sessionId, cwd: this.cwd, mcpServers: [], systemPrompt: { mode: 'append', content: [{ type: 'text', text }] } };
|
|
503
|
-
const configRevision = this.resetConfiguration();
|
|
526
|
+
const configRevision = this.resetConfiguration(false);
|
|
504
527
|
const result = await this.stage(resume ? 'acp-session-resume' : 'acp-session-load', () => this.connection.agent.request(method, params, { cancellationSignal: signal }));
|
|
505
528
|
this.lifecycleConfiguration(result, configRevision);
|
|
506
529
|
this.hooks.broker?.acknowledge(method);
|
|
507
530
|
this.systemPrompt = text;
|
|
508
531
|
this.ready = true;
|
|
509
|
-
this.
|
|
532
|
+
this.detail({ kind: 'session', severity: 'warning', code: 'SYSTEM_PROMPT_UPDATE_DEFERRED', summary: '系统与开发者要求已通过原会话恢复接口传送;新要求在下次上下文整理后生效,本轮不能保证已立即采用。', payload: { reason: 'OFFICIAL_CONTEXT_UPDATE_DEFERRED' } });
|
|
510
533
|
this.checkReporting();
|
|
511
534
|
return method;
|
|
512
535
|
}
|
|
@@ -542,6 +565,7 @@ export class AcpSession {
|
|
|
542
565
|
let rejectAbort;
|
|
543
566
|
const interrupted = new Promise((_resolve, reject) => { rejectAbort = reject; });
|
|
544
567
|
const cancel = () => {
|
|
568
|
+
this.closeDiagnostics.record({ layer: 'acp', initiator: 'acp.prompt-cancel', reason: 'ABORTED', operation: 'session/prompt', action: 'abort' });
|
|
545
569
|
rejectAbort?.(new NeedError('ABORTED', '已请求取消本轮,远端状态需确认', '确认主机取消状态'));
|
|
546
570
|
void this.connection.agent.notify(acp.methods.agent.session.cancel, { sessionId: this.sessionId }).catch(() => {
|
|
547
571
|
this.need({ code: 'CANCEL_UNCONFIRMED', message: '连接中断,无法确认主机已取消本轮', requires: '恢复连接以确认远端状态' });
|
|
@@ -573,7 +597,7 @@ export class AcpSession {
|
|
|
573
597
|
this.turnToken = undefined;
|
|
574
598
|
}
|
|
575
599
|
}
|
|
576
|
-
close() { this.
|
|
600
|
+
close(cause = { layer: 'acp', initiator: 'acp.close', reason: 'ACP_CLOSE_REQUESTED', action: 'close' }, error) { this.closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'acp.close-requested', before: this.closeRequested, after: true }] }); this.closeRequested = true; this.connection?.close(error); }
|
|
577
601
|
}
|
|
578
602
|
export function acpNeed(error) {
|
|
579
603
|
if (error instanceof NeedError)
|
package/dist/broker-runtime.js
CHANGED
|
@@ -6,6 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { CloseDiagnostics, closeCode, flushCloseDiagnostics } from "./close-diagnostics.js";
|
|
9
10
|
import { BrokerTransport, brokerCommand, brokerId, legacyBrokerId, sshIo, controlJson } from "./broker.js";
|
|
10
11
|
import { CODEX_AGENT } from "./agents.js";
|
|
11
12
|
import { protectedOneShotLayout } from "./host-layout.js";
|
|
@@ -63,15 +64,24 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
63
64
|
const contextBoundScope = recoveryKey([...scopeParts, request.contextSha256]);
|
|
64
65
|
// A Codex ACP process can own several immutable native sessions. D18's durable
|
|
65
66
|
// pointer controls their context; the broker still binds every transport identity.
|
|
66
|
-
const scope =
|
|
67
|
+
const scope = recoveryKey([...scopeParts, undefined]);
|
|
68
|
+
const previousContextScope = agent.loginProvider === 'claude' && request.previousContextSha256 ? recoveryKey([...scopeParts, request.previousContextSha256]) : contextBoundScope;
|
|
67
69
|
const existing = request.retainProcess ? retained.get(key) : undefined;
|
|
68
70
|
if (existing) {
|
|
69
71
|
if (existing.scope !== scope)
|
|
70
72
|
throw need('SESSION_CONTEXT_MISMATCH');
|
|
71
73
|
if (existing.runtime.broker?.error)
|
|
72
74
|
throw existing.runtime.broker.error;
|
|
75
|
+
if (request.requireFreshSession) {
|
|
76
|
+
if (existing.runtime.broker?.promptInFlight)
|
|
77
|
+
throw need('INPUT_DELIVERY_UNCONFIRMED');
|
|
78
|
+
await existing.runtime.dispose?.({ layer: 'broker', initiator: 'broker.fresh-session-required', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'dispose' });
|
|
79
|
+
return open(target, project, request);
|
|
80
|
+
}
|
|
73
81
|
existing.update(request);
|
|
74
82
|
existing.runtime.restartNotice = undefined;
|
|
83
|
+
existing.runtime.restartInputIds = undefined;
|
|
84
|
+
existing.runtime.freshSessionConfirmed = undefined;
|
|
75
85
|
existing.runtime.recoveredSession = undefined;
|
|
76
86
|
return existing.runtime;
|
|
77
87
|
}
|
|
@@ -83,7 +93,7 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
83
93
|
if (!journal)
|
|
84
94
|
throw need('RECOVERY_STORAGE_REQUIRED');
|
|
85
95
|
let state = await journal.read();
|
|
86
|
-
const legacyContextScope = !!state && state.scope !== scope &&
|
|
96
|
+
const legacyContextScope = !!state && state.scope !== scope && state.scope === previousContextScope;
|
|
87
97
|
if (state && state.scope !== scope && !legacyContextScope)
|
|
88
98
|
throw need(agent.loginProvider === 'codex' ? 'RECOVERY_CONTEXT_POLICY_UNSUPPORTED' : 'SESSION_CONTEXT_MISMATCH');
|
|
89
99
|
if (legacyContextScope && state?.phase !== 'clean')
|
|
@@ -106,9 +116,11 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
106
116
|
const report = async (code, extra = {}, cleanup = false) => { request.detailChannel?.record({ kind: 'need', severity: 'warning', code, summary: '远端会话恢复状态:' + code, payload: extra }); await request.detailChannel?.flush(); await validate(cleanup); };
|
|
107
117
|
const list = async (cleanup = false) => timing.run('broker-list', async () => { const rows = controlJson(await io.run(command(['list']), cleanup ? undefined : request.signal)); if (!Array.isArray(rows) || rows.some(row => typeof row?.id !== 'string'))
|
|
108
118
|
throw need('BROKER_PROTOCOL_INVALID'); return rows; });
|
|
119
|
+
const detailSink = request.detailChannel;
|
|
120
|
+
const closeDiagnostics = request.closeDiagnostics ?? new CloseDiagnostics(event => detailSink?.record(event));
|
|
109
121
|
let rows = await list();
|
|
110
122
|
const id = brokerId(request.sessionId, employeeScope ? agent.id + '/employee/' + employeeScope : agent.id), oldId = legacyBrokerId(request.sessionId, agent.id);
|
|
111
|
-
let restarted =
|
|
123
|
+
let restarted = !!request.requireFreshSession;
|
|
112
124
|
const attachArgs = (entry, after) => ['attach', entry.id, '--after', String(after), '--framed', ...(modern(entry) ? ['--leased', '--if-instance', entry.instanceId] : [])];
|
|
113
125
|
const connectWithLease = async (transport, entry, cleanup = false) => {
|
|
114
126
|
try {
|
|
@@ -143,13 +155,13 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
143
155
|
}
|
|
144
156
|
};
|
|
145
157
|
const claim = async (entry, cleanup = false) => {
|
|
146
|
-
const probe = new BrokerTransport({ discardOutput: true, onNeed: n => request.detailChannel?.record({ kind: 'need', severity: 'warning', code: n.code, summary: '主机会话连接状态', payload: { brokerId: entry.id, instanceId: entry.instanceId ?? null } }), maxFrameBytes: settings.brokerMaxFrameBytes, initialSequence: entry.lastSequence, instanceId: entry.instanceId, leased: modern(entry), controlMaxBytes: entry.controlMaxBytes, attach: after => io.attach(command(attachArgs(entry, after))) });
|
|
158
|
+
const probe = new BrokerTransport({ observeClose: closeDiagnostics.child().capture(), discardOutput: true, onNeed: n => request.detailChannel?.record({ kind: 'need', severity: 'warning', code: n.code, summary: '主机会话连接状态', payload: { brokerId: entry.id, instanceId: entry.instanceId ?? null } }), maxFrameBytes: settings.brokerMaxFrameBytes, initialSequence: entry.lastSequence, instanceId: entry.instanceId, leased: modern(entry), controlMaxBytes: entry.controlMaxBytes, attach: after => io.attach(command(attachArgs(entry, after))) });
|
|
147
159
|
try {
|
|
148
160
|
await connectWithLease(probe, entry, cleanup);
|
|
149
161
|
}
|
|
150
162
|
catch (error) {
|
|
151
163
|
if (!(entry.state === 'exited' && error instanceof NeedError && error.code === 'AGENT_EXITED')) {
|
|
152
|
-
probe.close();
|
|
164
|
+
probe.close({ layer: 'broker', initiator: 'broker.probe-close', reason: 'BROKER_PROBE_FINISHED', action: 'close' });
|
|
153
165
|
throw error;
|
|
154
166
|
}
|
|
155
167
|
}
|
|
@@ -183,7 +195,7 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
183
195
|
throw need('REMOTE_CLEANUP_UNCONFIRMED');
|
|
184
196
|
}
|
|
185
197
|
finally {
|
|
186
|
-
probe.close();
|
|
198
|
+
probe.close({ layer: 'broker', initiator: 'broker.probe-close', reason: 'BROKER_PROBE_FINISHED', action: 'close' });
|
|
187
199
|
}
|
|
188
200
|
};
|
|
189
201
|
const legacy = employeeScope ? undefined : rows.find(row => row.id === oldId);
|
|
@@ -192,13 +204,13 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
192
204
|
let entry = rows.find(row => row.id === id);
|
|
193
205
|
if (entry)
|
|
194
206
|
validEntry(entry);
|
|
195
|
-
if (legacyContextScope && !entry)
|
|
207
|
+
if (legacyContextScope && !entry && !request.requireFreshSession)
|
|
196
208
|
throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
|
|
197
209
|
if (legacy && !entry)
|
|
198
210
|
restarted = true;
|
|
199
211
|
let transport;
|
|
200
212
|
let resume;
|
|
201
|
-
const build = (entry, after, recovery) => new BrokerTransport({ onNeed: n => request.detailChannel?.record({ kind: 'need', severity: 'warning', code: n.code, summary: '主机会话连接状态', payload: { brokerId: entry.id, instanceId: entry.instanceId } }), maxFrameBytes: settings.brokerMaxFrameBytes, maxMessageBytes: request.acpMaxMessageBytes ?? settings.brokerMaxFrameBytes, initialSequence: after, instanceId: entry.instanceId, leased: true, controlMaxBytes: entry.controlMaxBytes, beforeInput: async () => { await validate(); if (state?.phase !== 'dirty' || state.instanceId !== entry.instanceId) {
|
|
213
|
+
const build = (entry, after, recovery) => new BrokerTransport({ observeClose: closeDiagnostics.capture(), onNeed: n => request.detailChannel?.record({ kind: 'need', severity: 'warning', code: n.code, summary: '主机会话连接状态', payload: { brokerId: entry.id, instanceId: entry.instanceId } }), maxFrameBytes: settings.brokerMaxFrameBytes, maxMessageBytes: request.acpMaxMessageBytes ?? settings.brokerMaxFrameBytes, initialSequence: after, instanceId: entry.instanceId, leased: true, controlMaxBytes: entry.controlMaxBytes, beforeInput: async () => { await validate(); if (state?.phase !== 'dirty' || state.instanceId !== entry.instanceId) {
|
|
202
214
|
const dirty = { version: 1, scope, brokerId: id, instanceId: entry.instanceId, pid: entry.pid, phase: 'dirty' };
|
|
203
215
|
await journal.write(dirty);
|
|
204
216
|
state = dirty;
|
|
@@ -225,6 +237,15 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
225
237
|
if (entry) {
|
|
226
238
|
if (!modern(entry))
|
|
227
239
|
throw need('BROKER_CAPABILITY_REQUIRED');
|
|
240
|
+
if (request.requireFreshSession) {
|
|
241
|
+
if (!state || !['clean', 'ended'].includes(state.phase) || state.instanceId !== entry.instanceId)
|
|
242
|
+
throw need('INPUT_DELIVERY_UNCONFIRMED');
|
|
243
|
+
await stop(entry, 'PRIVATE_RECOVERY_POINTER_ABSENT');
|
|
244
|
+
restarted = true;
|
|
245
|
+
entry = undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (entry) {
|
|
228
249
|
const clean = !employeeScope && state?.phase === 'clean' && state.instanceId === entry.instanceId && state.acp?.sessionId === request.resumeSessionId && state.checkpointId === request.recoveryCheckpointId && state.cursor >= entry.firstSequence - 1 && state.cursor <= entry.lastSequence && entry.state === 'running' && (legacyContextScope ? await request.verifyRecovery?.() === true : await request.verifyRecovery?.() !== false);
|
|
229
250
|
if (legacyContextScope && !clean)
|
|
230
251
|
throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
|
|
@@ -237,14 +258,14 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
237
258
|
await validate();
|
|
238
259
|
if (legacyContextScope) {
|
|
239
260
|
const migrated = { ...state, scope };
|
|
240
|
-
await report('CODEX_RECOVERY_SCOPE_MIGRATED', { oldScope: state.scope, newScope: scope, sessionId: state.acp.sessionId, checkpointId: state.checkpointId, instanceId: entry.instanceId, cursor: state.cursor });
|
|
261
|
+
await report(agent.loginProvider === 'codex' ? 'CODEX_RECOVERY_SCOPE_MIGRATED' : 'CLAUDE_RECOVERY_SCOPE_MIGRATED', { oldScope: state.scope, newScope: scope, sessionId: state.acp.sessionId, checkpointId: state.checkpointId, instanceId: entry.instanceId, cursor: state.cursor });
|
|
241
262
|
await journal.write(migrated);
|
|
242
263
|
state = migrated;
|
|
243
264
|
}
|
|
244
265
|
resume = state.acp;
|
|
245
266
|
}
|
|
246
267
|
catch (error) {
|
|
247
|
-
transport.close();
|
|
268
|
+
transport.close({ layer: 'broker', initiator: 'broker.replay-failure', reason: closeCode(error, 'RECOVERY_UNCONFIRMED'), action: 'close' });
|
|
248
269
|
if (legacyContextScope || error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH', 'ABORTED'].includes(error.code))
|
|
249
270
|
throw error;
|
|
250
271
|
await report('SESSION_REPLAY_UNRECOVERABLE', { brokerId: id, instanceId: entry.instanceId, reason: error instanceof NeedError ? error.code : 'UNKNOWN_FAILURE', recordedCursor: state?.cursor ?? null, firstSequence: entry.firstSequence, lastSequence: entry.lastSequence });
|
|
@@ -274,7 +295,7 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
274
295
|
validEntry(entry);
|
|
275
296
|
transport = build(entry, 0);
|
|
276
297
|
const opening = transport;
|
|
277
|
-
const aborted = () => opening.fail(need('ABORTED'));
|
|
298
|
+
const aborted = () => opening.fail(need('ABORTED'), { layer: 'broker', initiator: 'broker.open-signal', reason: 'ABORTED', action: 'abort' });
|
|
278
299
|
request.signal?.addEventListener('abort', aborted, { once: true });
|
|
279
300
|
try {
|
|
280
301
|
request.signal?.throwIfAborted();
|
|
@@ -284,7 +305,7 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
284
305
|
catch (error) {
|
|
285
306
|
if (!(error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH'].includes(error.code)))
|
|
286
307
|
await stop(entry, 'SESSION_OPEN_FAILED', opening, true, true);
|
|
287
|
-
opening.close();
|
|
308
|
+
opening.close({ layer: 'broker', initiator: 'broker.open-failure', reason: closeCode(error), action: 'close' });
|
|
288
309
|
throw error;
|
|
289
310
|
}
|
|
290
311
|
finally {
|
|
@@ -294,8 +315,8 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
294
315
|
const ownedEntry = entry, ownedTransport = transport;
|
|
295
316
|
let disposed = false;
|
|
296
317
|
let disposal;
|
|
297
|
-
const runtime = { identity: identity(target), cwd: project.path, broker: ownedTransport, acpTransport: ownedTransport,
|
|
298
|
-
...(resume ? { recoveredSession: resume } : {}), ...(restarted ? { restartNotice: '上次会话无法接回,已新建会话;本次从你的这条请求继续。', restartInputIds: request.currentInputIds } : {}),
|
|
318
|
+
const runtime = { closeDiagnostics, identity: identity(target), cwd: project.path, broker: ownedTransport, acpTransport: ownedTransport,
|
|
319
|
+
...(resume ? { recoveredSession: resume } : {}), ...(restarted ? { restartNotice: '上次会话无法接回,已新建会话;本次从你的这条请求继续。', restartInputIds: request.currentInputIds } : {}), ...(request.requireFreshSession ? { freshSessionConfirmed: true } : {}),
|
|
299
320
|
saveCheckpoint: async (checkpointId, acp) => { await validate(); const saved = await ownedTransport.commitCheckpoint(async (cursor) => { await validate(); const clean = { version: 1, phase: 'clean', scope, brokerId: id, instanceId: ownedEntry.instanceId, pid: ownedEntry.pid, cursor, checkpointId, acp }; try {
|
|
300
321
|
await journal.write(clean);
|
|
301
322
|
state = clean;
|
|
@@ -305,24 +326,29 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
305
326
|
throw error;
|
|
306
327
|
} }); if (!saved)
|
|
307
328
|
await report('SESSION_CHECKPOINT_NOT_CLEAN', { brokerId: id, instanceId: ownedEntry.instanceId }); return saved; },
|
|
308
|
-
detach: async () => { ownedTransport.close(); active.delete(key); retained.delete(key); },
|
|
309
|
-
dispose: () => disposal ??= (async () => { if (disposed)
|
|
310
|
-
return; try {
|
|
329
|
+
detach: async (cause = { layer: 'broker', initiator: 'broker.runtime-detach', reason: 'BROKER_DETACH', action: 'detach' }) => { const sink = request.detailChannel; closeDiagnostics.record(cause); ownedTransport.close(cause); active.delete(key); retained.delete(key); await flushCloseDiagnostics(closeDiagnostics, sink ? () => sink.flush() : undefined); },
|
|
330
|
+
dispose: (cause = { layer: 'broker', initiator: 'broker.runtime-dispose', reason: 'BROKER_DISPOSE', action: 'dispose' }) => { const sink = request.detailChannel; closeDiagnostics.record(cause); return disposal ??= (async () => { if (disposed)
|
|
331
|
+
return; let primary; try {
|
|
311
332
|
await stop(ownedEntry, 'SESSION_ENDED', ownedTransport, true);
|
|
312
333
|
await journal.write({ version: 1, phase: 'ended', scope, brokerId: id, instanceId: ownedEntry.instanceId, pid: ownedEntry.pid });
|
|
313
334
|
disposed = true;
|
|
314
335
|
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
primary = { error };
|
|
338
|
+
}
|
|
315
339
|
finally {
|
|
316
|
-
ownedTransport.close();
|
|
340
|
+
ownedTransport.close({ layer: 'broker', initiator: 'broker.dispose-finally', reason: 'BROKER_DISPOSE_CLEANUP', action: 'close' });
|
|
317
341
|
active.delete(key);
|
|
318
342
|
retained.delete(key);
|
|
319
|
-
}
|
|
343
|
+
} await flushCloseDiagnostics(closeDiagnostics, sink ? () => sink.flush() : undefined, primary); })(); },
|
|
344
|
+
};
|
|
320
345
|
if (employeeScope) {
|
|
321
346
|
runtime.saveCheckpoint = undefined;
|
|
322
347
|
runtime.detach = runtime.dispose;
|
|
323
348
|
}
|
|
324
349
|
if (request.retainProcess)
|
|
325
|
-
retained.set(key, { runtime, scope, update: next => {
|
|
350
|
+
retained.set(key, { runtime, scope, update: next => { const sink = next.detailChannel; if (sink)
|
|
351
|
+
closeDiagnostics.bind(event => sink.record(event)); request = next; timing = next.timing ?? new StageTiming(); } });
|
|
326
352
|
return runtime;
|
|
327
353
|
}
|
|
328
354
|
catch (error) {
|