@ai-workbench/remote-agent 0.8.23 → 0.8.25
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 +9 -5
- package/dist/acp.js +1 -1
- package/dist/connect-contract.d.ts +1 -1
- package/dist/employee-mcp.js +1 -1
- package/dist/host-workspace.d.ts +10 -15
- package/dist/host-workspace.js +26 -109
- package/dist/index.d.ts +2 -2
- package/dist/index.js +42 -21
- package/dist/preflight-diagnostic.js +1 -1
- package/dist/presentation.d.ts +0 -1
- package/dist/presentation.js +1 -1
- package/dist/provider.d.ts +9 -15
- package/dist/provider.js +43 -94
- package/dist/session-source.d.ts +18 -0
- package/dist/session-source.js +90 -0
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.8.
|
|
1
|
+
# AI Workbench 远程助手 · @ai-workbench/remote-agent@0.8.25 · 插件 → 添加插件
|
|
2
2
|
|
|
3
3
|
通过官方 DSH 插件入口使用 Claude(远程主机)与 Codex(远程主机)。登录和工作区来源由“远程主机连接”组件提供;员工在原生工作区和模型菜单中选择文件夹与型号。
|
|
4
4
|
|
|
5
|
-
文件和命令在所选的本机或主机目录执行,订阅凭据留在主机。插件不改写 DSH
|
|
5
|
+
文件和命令在所选的本机或主机目录执行,订阅凭据留在主机。插件不改写 DSH 的默认模型,不为员工创建同名主机目录,已有主机会话只提取一次原有主机来源并保存。
|
|
6
|
+
|
|
7
|
+
需要远程主机连接组件 0.7.29 或更新的 0.7.x,以及联网助手 0.4.9。主机来源保存于官方 storage.domain 的会话元数据表,每轮仅由连接组件检查一次主机目录,不检查本机占位目录。
|
|
6
8
|
|
|
7
9
|
## 会话行为
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
每轮启动一个新助手进程,按原生会话编号恢复上下文;不接回旧进程或重放上一轮输入。主机会话始终使用创建时保存的主机和目录;工作区之后的选择只影响新会话。
|
|
10
12
|
|
|
11
13
|
断线会结束本轮;操作可能已部分执行,请先检查文件。恢复连接后可在原会话继续。旧进程清理和技术详情记录尽力而为,不阻止下一轮。原生本机会话恢复的兼容性及实际设备结果见本轮交付证据,未验证项不作为已通过。
|
|
12
14
|
|
|
@@ -17,7 +19,7 @@ DSH 启动和打开模型菜单时读取一次主机型号列表。发送时不
|
|
|
17
19
|
- 员工选本机文件夹,就在员工电脑上执行;通道失败不会改在主机运行。
|
|
18
20
|
- 订阅凭据留在主机,不放进命令参数、日志或技术详情。
|
|
19
21
|
- 跟随 DSH 当前权限:完全权限不问、工作区内修改先问、仅可查看只读。需要审批时使用官方卡片。
|
|
20
|
-
-
|
|
22
|
+
- 一条会话对应一个文件来源;主机来源不会随工作区后续选择改变,本机来源继续沿用原生绑定。
|
|
21
23
|
|
|
22
24
|
Codex 员工端只使用随包提供的原生组件,通道等待本次组件的启动标记后才完成开通,不逐通道执行摘要或版本校验。当前员工端支持范围以发布验证记录为准,不让员工手填执行器路径。
|
|
23
25
|
|
|
@@ -40,6 +42,8 @@ Codex 员工端只使用随包提供的原生组件,通道等待本次组件
|
|
|
40
42
|
|
|
41
43
|
技术详情只写官方 profile 下本地私有日志,并由既有技术详情界面展开显示。写入失败不覆盖原始错误、不等待远端投递回执。每个运行环境只记录首次关闭原因;首先观察到的原因不自动等同跨层根因。
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
工程验证和独立审查见 `docs/evidence/2026-10-25-X-host-source-v2/`;AUTH profile 夹具补齐与最终 main 全套复验见其中的 `auth-profile-return.md`。固定包摘要及真实设备验收状态以项目 X 回执为准。插件不修改 DSH 核心,不添加启动器或自定义 profile。
|
|
44
46
|
|
|
45
47
|
远程助手参数页只显示官方 ConfigForms 当前值,不提供保存、放弃或恢复继承值;修改请使用 DSH 官方 Config“打开配置文件”入口。
|
|
48
|
+
|
|
49
|
+
模型目录在官方启动读取时获取,并在连接变为 connected 时经官方注册刷新;官方菜单打开或重试读取缓存是 DSH 官方限制,本插件不改官方菜单或核心。真正不支持的推理档位由 DSH 预检,其英文提示保留为观察项。
|
package/dist/acp.js
CHANGED
|
@@ -484,7 +484,7 @@ export class AcpSession {
|
|
|
484
484
|
signal?.addEventListener('abort', abort, { once: true });
|
|
485
485
|
try {
|
|
486
486
|
const hello = await this.stage('acp-initialize', () => this.connection.agent.request(acp.methods.agent.initialize, {
|
|
487
|
-
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.
|
|
487
|
+
protocolVersion: acp.PROTOCOL_VERSION, clientInfo: { name: '@ai-workbench/remote-agent', version: '0.8.24' },
|
|
488
488
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false, session: { notices: {} }, ...(this.employeeMeta ? { _meta: { awEmployeeExecution: { version: 1, closeReason: 1 } } } : {}) },
|
|
489
489
|
}, { cancellationSignal: signal }));
|
|
490
490
|
if (hello.protocolVersion !== acp.PROTOCOL_VERSION)
|
|
@@ -20,5 +20,5 @@ export type SessionProjectResolution = HostSessionProjectTarget;
|
|
|
20
20
|
export interface NativeWorkspaceApi {
|
|
21
21
|
getContractInfo(): ContractInfo;
|
|
22
22
|
ensureReady(provider: LoginProvider): Promise<ConnectResult<ConnectTarget>>;
|
|
23
|
-
resolveSessionProject(sessionId: string, use: WorkspaceUse, location
|
|
23
|
+
resolveSessionProject(sessionId: string, use: WorkspaceUse, location: HostWorkspaceResolveContext): Promise<ConnectResult<SessionProjectResolution>>;
|
|
24
24
|
}
|
package/dist/employee-mcp.js
CHANGED
|
@@ -152,7 +152,7 @@ export async function openEmployeeMcp(options) {
|
|
|
152
152
|
return tools.call(request.params.name, request.params.arguments ?? {}, signal, event => { if (sink)
|
|
153
153
|
observeUsing(event, sink); });
|
|
154
154
|
});
|
|
155
|
-
mcp.onerror = () => { observeConnection?.({ layer: 'relay', initiator: 'mcp.server-error', reason: 'EMPLOYEE_MCP_PROTOCOL_OR_TRANSPORT_ERROR', action: 'failure' }); observe({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_MCP_UNKNOWN', summary: '
|
|
155
|
+
mcp.onerror = () => { observeConnection?.({ layer: 'relay', initiator: 'mcp.server-error', reason: 'EMPLOYEE_MCP_PROTOCOL_OR_TRANSPORT_ERROR', action: 'failure' }); observe({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_MCP_UNKNOWN', summary: '本机工具连接中断,在途结果未知,没有重放;恢复后可在原会话继续。' }); };
|
|
156
156
|
mcp.onclose = () => { observeConnection?.({ layer: 'relay', initiator: 'mcp.client-closed', reason: 'MCP_CLIENT_CLOSED', action: 'close' }); clients.delete(mcp); transports.delete(transport); };
|
|
157
157
|
void mcp.connect(transport).catch(error => { observeConnection?.({ layer: 'relay', initiator: 'mcp.connect-failure', reason: closeCode(error, 'MCP_CONNECT_FAILED'), action: 'close' }); socket.destroy(); });
|
|
158
158
|
});
|
package/dist/host-workspace.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** Current official membership and explicit host selection; never infer source from a pathname. */
|
|
12
|
-
export declare function hostWorkspaceGuard(get: () => ConnectApi | undefined, api: ConnectApi & NativeWorkspaceApi, connection: ConnectionState, target: ConnectTarget, request: RuntimeRequest, workspace: ((request: RuntimeRequest) => Promise<{
|
|
13
|
-
id: string;
|
|
14
|
-
path: string;
|
|
15
|
-
}>) | undefined, provider: LoginProvider): Promise<HostWorkspaceGuard | undefined>;
|
|
1
|
+
import type { HostWorkspaceResolveContext, LoginProvider, NativeWorkspaceApi } from './connect-contract.ts';
|
|
2
|
+
import type { ConnectApi, RuntimeRequest } from './provider.ts';
|
|
3
|
+
export type SessionSource = Readonly<HostWorkspaceResolveContext> | Readonly<{
|
|
4
|
+
source: 'local';
|
|
5
|
+
}>;
|
|
6
|
+
export declare const absoluteHostPath: (path: unknown) => path is string;
|
|
7
|
+
/** Read only L's public source projection. Old host status/binding/physical fields have no meaning here. */
|
|
8
|
+
export declare function workspaceSessionSource(api: ConnectApi, workspaceId: string, request?: RuntimeRequest): SessionSource | undefined;
|
|
9
|
+
/** One L v2 resolution per user turn. L owns host/path checks and the current SSH target. */
|
|
10
|
+
export declare function resolveHostWorkspace(api: ConnectApi & NativeWorkspaceApi, request: RuntimeRequest, provider: LoginProvider): Promise<import("@ai-workbench/connect/api").HostSessionProjectTarget>;
|
package/dist/host-workspace.js
CHANGED
|
@@ -1,114 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { awaitL, sameTarget, unwrap } from "./provider.js";
|
|
3
|
-
import { connectNeedError } from "./connect-need.js";
|
|
1
|
+
import { awaitL, unwrap } from "./provider.js";
|
|
4
2
|
import { NeedError } from "./remote.js";
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
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关系');
|
|
3
|
+
export const absoluteHostPath = (path) => typeof path === 'string' && path.startsWith('/') && !path.includes('\0');
|
|
4
|
+
const unavailable = () => new NeedError('WORKSPACE_LOCATION_REQUIRED', '我需要这条会话原来选择的文件来源,请在远程主机连接中查看工作区文件来源。', '会话的主机名称和绝对目录');
|
|
5
|
+
/** Read only L's public source projection. Old host status/binding/physical fields have no meaning here. */
|
|
6
|
+
export function workspaceSessionSource(api, workspaceId, request) {
|
|
7
|
+
const rows = api.getWorkspaceLocationState?.().items.filter(row => row.workspaceId === workspaceId) ?? [];
|
|
8
|
+
if (rows.length !== 1)
|
|
22
9
|
return;
|
|
10
|
+
const row = rows[0];
|
|
11
|
+
if (row.source === 'local') {
|
|
12
|
+
if (request?.cwd && request.cwd !== row.localPath)
|
|
13
|
+
throw unavailable();
|
|
14
|
+
return Object.freeze({ source: 'local' });
|
|
23
15
|
}
|
|
24
|
-
|
|
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)
|
|
16
|
+
if (row.source !== 'host')
|
|
28
17
|
return;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
const cause = revoked.signal.reason;
|
|
101
|
-
revoke(error);
|
|
102
|
-
if (cause instanceof NeedError)
|
|
103
|
-
throw cause;
|
|
104
|
-
}
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
dispose();
|
|
112
|
-
throw error;
|
|
113
|
-
}
|
|
18
|
+
const host = row.host?.host, path = request?.cwd ?? row.remote?.path;
|
|
19
|
+
if (typeof host !== 'string' || !host || !absoluteHostPath(path))
|
|
20
|
+
throw unavailable();
|
|
21
|
+
return Object.freeze({ source: 'host', host, path });
|
|
22
|
+
}
|
|
23
|
+
/** One L v2 resolution per user turn. L owns host/path checks and the current SSH target. */
|
|
24
|
+
export async function resolveHostWorkspace(api, request, provider) {
|
|
25
|
+
const source = request.workspaceSource;
|
|
26
|
+
if (source?.source !== 'host')
|
|
27
|
+
throw unavailable();
|
|
28
|
+
if ((api.getContractInfo().hostWorkspaceResolution ?? 0) < 2)
|
|
29
|
+
throw new NeedError('DEPENDENCY_REQUIRED', '我需要更新远程主机连接组件后再使用主机目录;本轮未执行,原话保留。', 'L >=0.7.29 / hostWorkspaceResolution >=2');
|
|
30
|
+
return unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider, purpose: 'user-turn' }, { ...source }), request.signal));
|
|
114
31
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -74,8 +74,8 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
74
74
|
listModels(provider: string): Promise<(LlmResolvedModelInfo & {
|
|
75
75
|
catalog: HostModel;
|
|
76
76
|
})[]>;
|
|
77
|
-
resolveModel(provider: string, id: string,
|
|
78
|
-
prepareCall(provider: string, id: string,
|
|
77
|
+
resolveModel(provider: string, id: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
78
|
+
prepareCall(provider: string, id: string, signal?: AbortSignal): Promise<{
|
|
79
79
|
model: LlmResolvedModelInfo;
|
|
80
80
|
stream: (options: GenerateOptions) => AsyncGenerator<StreamChunk, any, any>;
|
|
81
81
|
}>;
|
package/dist/index.js
CHANGED
|
@@ -22,12 +22,13 @@ import { ModelPresentation } from "./model-presentation.js";
|
|
|
22
22
|
import { EMPLOYEE_EXECUTION_NOTICE } from "./employee-presentation.js";
|
|
23
23
|
import { ModelConfigurationError } from "./model-configuration.js";
|
|
24
24
|
import { officialEmployeeWorkspace } from "./employee-workspace.js";
|
|
25
|
+
import { createSessionSources } from "./session-source.js";
|
|
25
26
|
import { resolveEmployeeNative } from "./employee-native.js";
|
|
26
27
|
import { createClaudeLocalProvider } from "./claude-local.js";
|
|
27
28
|
import { readLocalUserTurn } from "./local-user-turn.js";
|
|
28
29
|
import { readEmployeeWorkspace } from "./employee-workspace.js";
|
|
29
30
|
import { createPreflightDiagnostics } from "./preflight-diagnostic.js";
|
|
30
|
-
import { installModelInputRetention } from "./model-input-retention.js";
|
|
31
|
+
import { installModelInputRetention, markModelPreparationFailure } from "./model-input-retention.js";
|
|
31
32
|
import { dshPermissionSource, permissionMode } from "./permissions.js";
|
|
32
33
|
export { readDshPermissions } from "./permissions.js";
|
|
33
34
|
export const name = 'ai-workbench-remote-agent';
|
|
@@ -121,8 +122,33 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
121
122
|
finally {
|
|
122
123
|
catalog.release();
|
|
123
124
|
} }
|
|
124
|
-
async resolveModel(provider, id,
|
|
125
|
-
|
|
125
|
+
async resolveModel(provider, id, signal) {
|
|
126
|
+
const displayed = this.modelPresentation.resolve(provider, id);
|
|
127
|
+
if (displayed)
|
|
128
|
+
return displayed;
|
|
129
|
+
const active = signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
|
|
130
|
+
const catalog = await this.catalog(active);
|
|
131
|
+
try {
|
|
132
|
+
active.throwIfAborted();
|
|
133
|
+
this.modelPresentation.remember(catalog);
|
|
134
|
+
const resolved = this.modelPresentation.resolve(provider, id);
|
|
135
|
+
if (!resolved)
|
|
136
|
+
throw new LlmError('所选模型当前不可用,请打开模型菜单重新选择。', 'MODEL_CATALOG_UNAVAILABLE');
|
|
137
|
+
return resolved;
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
catalog.release();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async prepareCall(provider, id, signal) {
|
|
144
|
+
try {
|
|
145
|
+
return { model: await this.resolveModel(provider, id, signal), stream: (options) => this.stream(options) };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
markModelPreparationFailure(provider, error, signal);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
126
152
|
providerRetryPolicy() { return { mode: 'normal', maxRetries: 0, retryableCodes: [], initialDelayMs: 0, maxDelayMs: 0, jitterRatio: 0 }; }
|
|
127
153
|
cleanupRuntime(runtime, cause = { layer: 'adapter', initiator: 'adapter.cleanup-runtime', reason: 'RUNTIME_CLEANUP', action: 'dispose' }) {
|
|
128
154
|
runtime.closeDiagnostics?.record(cause);
|
|
@@ -253,7 +279,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
253
279
|
const missingPointer = !!last && !previous;
|
|
254
280
|
live = undefined;
|
|
255
281
|
const configuration = JSON.stringify([config.acpMaxMessageBytes, config.brokerMaxFrameBytes, config.brokerControlMaxBytes, config.employeeShellKillGraceMs]);
|
|
256
|
-
Object.assign(request, { acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, sessionId: options.sessionId, mode, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
|
|
282
|
+
Object.assign(request, { acpMaxMessageBytes: config.acpMaxMessageBytes, resumeSessionId: previous?.sessionId, sessionId: options.sessionId, mode, hostSource: previous?.hostSource, cwd: previous?.cwd, identity: previous?.identity, bindingId: previous?.bindingId, workspaceId: previous?.workspaceId, agent: this.agent, signal, brokerSettings: Object.freeze({ brokerMaxFrameBytes: config.brokerMaxFrameBytes, brokerControlMaxBytes: config.brokerControlMaxBytes }) });
|
|
257
283
|
request.previousContextSha256 = previous?.contextSha256;
|
|
258
284
|
const after = options.messages.findLastIndex(message => message.role === 'assistant');
|
|
259
285
|
const tail = options.messages.slice(after + 1);
|
|
@@ -298,13 +324,7 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
298
324
|
request.signal = modelSignal;
|
|
299
325
|
modelSignal.throwIfAborted();
|
|
300
326
|
}
|
|
301
|
-
if (runtime.
|
|
302
|
-
modelSignal = AbortSignal.any([modelSignal, runtime.contextSignal]);
|
|
303
|
-
options.signal = modelSignal;
|
|
304
|
-
request.signal = modelSignal;
|
|
305
|
-
modelSignal.throwIfAborted();
|
|
306
|
-
}
|
|
307
|
-
if (previous && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
|
|
327
|
+
if (previous && !runtime.hostSource && (previous.identity !== runtime.identity || previous.cwd !== runtime.cwd || previous.bindingId && previous.bindingId !== runtime.binding?.bindingId || previous.workspaceId && previous.workspaceId !== runtime.binding?.workspaceId))
|
|
308
328
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的员工主机或工作区已改变,请新建会话', '原会话的可信目标和工作区');
|
|
309
329
|
input = projectHostWorkspaceInput(input, runtime.hostWorkspace);
|
|
310
330
|
if (runtime.hostWorkspace && mode === 'session' && Buffer.byteLength(JSON.stringify({ prompt: input.prompt, systemPrompt: input.systemPrompt }), 'utf8') > config.acpMaxMessageBytes)
|
|
@@ -506,17 +526,11 @@ export class CodexRemoteAdapter extends LlmAdapter {
|
|
|
506
526
|
}
|
|
507
527
|
}
|
|
508
528
|
yield* mapper.flush();
|
|
509
|
-
const pointer = { ...(modelConfiguration ? { modelConfiguration } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
|
|
529
|
+
const pointer = { ...(runtime.hostSource ? { hostSource: runtime.hostSource } : {}), ...(modelConfiguration ? { modelConfiguration } : {}), version: 2, agent: this.agent.id, contextSha256, ...(mode === 'session' ? { usageSnapshot: mapper.usageSnapshot } : {}), ...(options.sessionId ? { dshSessionId: options.sessionId } : {}), identity: runtime.identity, cwd: runtime.cwd, sessionId: id, ...(runtime.binding ? { bindingId: runtime.binding.bindingId, workspaceId: runtime.binding.workspaceId } : {}) };
|
|
510
530
|
succeeded = true;
|
|
511
531
|
yield { type: 'finish', reason: { kind: stop === 'max_tokens' || stop === 'max_turn_requests' ? 'max-tokens' : 'stop' }, replayState: { response: pointer } };
|
|
512
532
|
}
|
|
513
533
|
catch (error) {
|
|
514
|
-
// ACP cancellation must retain the source revocation that caused it.
|
|
515
|
-
// User cancellation and unrelated signals keep their existing semantics.
|
|
516
|
-
const sourceReason = runtime?.contextSignal?.aborted ? runtime.contextSignal.reason : undefined;
|
|
517
|
-
if (error instanceof NeedError && error.code === 'ABORTED' && sourceReason instanceof NeedError) {
|
|
518
|
-
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') });
|
|
519
|
-
}
|
|
520
534
|
turnFailure = error;
|
|
521
535
|
runtime?.closeDiagnostics?.record({ layer: 'adapter', initiator: 'adapter.turn-failure', reason: closeCode(error, signal.aborted ? 'ABORTED' : 'TURN_FAILED'), action: 'failure' });
|
|
522
536
|
recordInformation();
|
|
@@ -589,6 +603,7 @@ export function apply(ctx, live) {
|
|
|
589
603
|
return manager;
|
|
590
604
|
};
|
|
591
605
|
ctx.provide('aiWorkbenchRemoteDiagnostics', { read: (query) => managerForProfile().read(query) });
|
|
606
|
+
const sessionSources = createSessionSources(ctx, get);
|
|
592
607
|
const preflightDiagnostics = createPreflightDiagnostics({ ctx, getApi: get, manager: managerForProfile });
|
|
593
608
|
const diagnostics = new Map();
|
|
594
609
|
const diagnostic = (scope, provider, message) => {
|
|
@@ -641,7 +656,7 @@ export function apply(ctx, live) {
|
|
|
641
656
|
const sessions = services.get('sessions');
|
|
642
657
|
return officialEmployeeWorkspace(request.sessionId, registry, sessions?.get(request.sessionId)?.header);
|
|
643
658
|
};
|
|
644
|
-
const provider = connectProvider(get, { agent, workspace, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
659
|
+
const provider = connectProvider(get, { agent, workspace, source: sessionSources, ...(agent.loginProvider === 'codex' ? { employee: {
|
|
645
660
|
getNetwork: () => ctx.get('aiWorkbenchNetwork'),
|
|
646
661
|
workspace, native: () => resolveEmployeeNative(),
|
|
647
662
|
} } : {}), details, open, local });
|
|
@@ -665,12 +680,18 @@ export function apply(ctx, live) {
|
|
|
665
680
|
}
|
|
666
681
|
return managerForProfile().recordLoginFailure({ sessionId, code, diagnostic, context: { turn, step } });
|
|
667
682
|
}, modelDirectory, preflightDiagnostics, dshPermissionSource(ctx));
|
|
668
|
-
for (const agent of AGENTS)
|
|
669
|
-
void adapter.listModels(agent.id).catch(() => ctx.logger('remote-agent').warn('模型列表暂不可用,打开菜单时可重新读取'));
|
|
670
683
|
const ids = AGENTS.map(agent => agent.id);
|
|
671
684
|
const entries = () => AGENTS.map(agent => ({ provider: agent.id, displayName: agent.name[language], settingsNs: actualSettingsNamespace ?? settingsNamespace, settingsPath: [] }));
|
|
672
685
|
const directory = ctx.llm.registerConfigurableProviders(entries());
|
|
673
686
|
const registration = ctx.llm.registerAdapter(ids, adapter);
|
|
687
|
+
let connection;
|
|
688
|
+
ctx.on('ai-workbench/connection-state', state => {
|
|
689
|
+
const previous = connection;
|
|
690
|
+
connection = state;
|
|
691
|
+
// Official replacement invalidates the browser's shared catalog and loads it.
|
|
692
|
+
if (state.phase === 'connected' && (previous?.phase !== 'connected' || previous.instanceId !== state.instanceId || previous.epoch !== state.epoch))
|
|
693
|
+
registration.replace(ids);
|
|
694
|
+
});
|
|
674
695
|
installModelInputRetention(ctx, { isOurProvider: provider => ids.includes(provider), diagnosticForAttempt: position => preflightDiagnostics.forPreparation(position.sessionId, position), preflightDiagnostic: (provider, message) => diagnostic('preflight', provider, message), diagnostic: (provider, message) => diagnostic('input', provider, message) });
|
|
675
696
|
const cleanup = async (id) => { try {
|
|
676
697
|
await adapter.detachSession(id);
|
|
@@ -49,7 +49,7 @@ export function createPreflightDiagnostics(options) {
|
|
|
49
49
|
const rows = state?.items.filter(row => row.workspaceId === workspace.id) ?? [];
|
|
50
50
|
if (state?.phase === 'ready' && state.instanceId === connection?.instanceId && state.epoch === connection.epoch && rows.length === 1) {
|
|
51
51
|
const row = rows[0];
|
|
52
|
-
source = { workspaceId: row.workspaceId, bindingId: row.bindingId, source: row.source === 'local' || row.source === 'host' ? row.source : null, status: row.status };
|
|
52
|
+
source = { workspaceId: row.workspaceId, bindingId: row.bindingId, source: row.source === 'local' || row.source === 'host' ? row.source : null, status: row.source === 'host' ? null : row.status ?? null };
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
package/dist/presentation.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Functional wording from connect-api appendix D1-D3 (L0.5.1 candidate). */
|
|
2
2
|
export declare const REMOTE_CODEX_NAME = "Codex\uFF08\u8FDC\u7A0B\u4E3B\u673A\uFF09";
|
|
3
3
|
export declare const CONNECTION_REQUIRED = "\u8FDC\u7A0B\u4E3B\u673A\u8FDE\u63A5\u7EC4\u4EF6\u6CA1\u6709\u88C5\u597D\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002";
|
|
4
|
-
export declare const TEST_HOST_NOTICE = "\u8FDC\u7A0B\u4E3B\u673A\u6B63\u5728\u4F7F\u7528\u6D4B\u8BD5\u914D\u7F6E\uFF0C\u9047\u5230\u95EE\u9898\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u3002";
|
|
5
4
|
/** Employee failures use business language; diagnostic codes remain separately available. */
|
|
6
5
|
export declare function employeeFailure(code: string, brand?: 'codex' | 'claude'): string;
|
package/dist/presentation.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** Functional wording from connect-api appendix D1-D3 (L0.5.1 candidate). */
|
|
2
2
|
export const REMOTE_CODEX_NAME = 'Codex(远程主机)';
|
|
3
3
|
export const CONNECTION_REQUIRED = '远程主机连接组件没有装好,请联系管理员。';
|
|
4
|
-
export const TEST_HOST_NOTICE = '远程主机正在使用测试配置,遇到问题请联系管理员。';
|
|
5
4
|
/** Employee failures use business language; diagnostic codes remain separately available. */
|
|
6
5
|
export function employeeFailure(code, brand = 'codex') {
|
|
7
6
|
const messages = {
|
|
@@ -13,6 +12,7 @@ export function employeeFailure(code, brand = 'codex') {
|
|
|
13
12
|
PERMISSION_POLICY_CHANGED: '当前权限已变化或无法核对,这次派发已停止;没有自动重发,你的原话仍保留,在途结果请查看技术详情。',
|
|
14
13
|
APPROVAL_STALE: '连接或权限已变化,原审批已失效;没有自动重发,在途结果请查看技术详情。',
|
|
15
14
|
PROJECT_BINDING_INVALID: '文件来源已改,请新建会话继续;原会话历史仍可查看。',
|
|
15
|
+
WORKSPACE_SOURCE_STORAGE_FAILED: '这条会话的文件来源未能保存,本轮工作区准备失败;请恢复后在原会话重试。',
|
|
16
16
|
WORKSPACE_LOCATION_REQUIRED: '我需要你确认这个工作区使用哪一个文件夹,本轮未执行;你的原话仍保留。',
|
|
17
17
|
WORKSPACE_LOCATION_CONFLICT: '工作区来源选择已更新,本次确认不能覆盖新的选择;本轮未执行,你的原话仍保留。',
|
|
18
18
|
LOCAL_USER_TURN_CHANGED: '本次输入已变化,本轮已停止,请确认对话后重新发送。',
|
package/dist/provider.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SessionSource } from './host-workspace.ts';
|
|
1
2
|
import type { CloseCause, CloseDiagnostics } from './close-diagnostics.ts';
|
|
2
3
|
import type { AgentApp } from '@agentclientprotocol/sdk';
|
|
3
4
|
import type { AgentApp as ExtendedAgentApp } from '@agentclientprotocol/sdk/unstable';
|
|
@@ -27,7 +28,9 @@ export interface Runtime {
|
|
|
27
28
|
identity: string;
|
|
28
29
|
cwd: string;
|
|
29
30
|
hostWorkspace?: Readonly<HostWorkspacePresentation>;
|
|
30
|
-
|
|
31
|
+
hostSource?: Extract<SessionSource, {
|
|
32
|
+
source: 'host';
|
|
33
|
+
}>;
|
|
31
34
|
execution?: EmployeeExecution;
|
|
32
35
|
acpTransport?: {
|
|
33
36
|
writable: WritableStream<Uint8Array>;
|
|
@@ -61,6 +64,10 @@ export interface Runtime {
|
|
|
61
64
|
closeDiagnostics?: CloseDiagnostics;
|
|
62
65
|
}
|
|
63
66
|
export interface RuntimeRequest extends Invocation {
|
|
67
|
+
workspaceSource?: SessionSource;
|
|
68
|
+
hostSource?: Extract<SessionSource, {
|
|
69
|
+
source: 'host';
|
|
70
|
+
}>;
|
|
64
71
|
closeDiagnostics?: CloseDiagnostics;
|
|
65
72
|
previousContextSha256?: string;
|
|
66
73
|
preflightFailureRecorded?: boolean;
|
|
@@ -89,18 +96,6 @@ export type RuntimeProvider = ((request: RuntimeRequest) => Promise<Runtime>) &
|
|
|
89
96
|
preflight?: (request: RuntimeRequest) => Promise<void>;
|
|
90
97
|
};
|
|
91
98
|
export interface ConnectTarget {
|
|
92
|
-
hostContext?: {
|
|
93
|
-
enforcement: 'production' | 'test';
|
|
94
|
-
checkExitCode: 0 | 1;
|
|
95
|
-
failedChecks: {
|
|
96
|
-
id: string;
|
|
97
|
-
status: string;
|
|
98
|
-
}[];
|
|
99
|
-
unverifiedChecks: {
|
|
100
|
-
id: string;
|
|
101
|
-
status: string;
|
|
102
|
-
}[];
|
|
103
|
-
};
|
|
104
99
|
host: string;
|
|
105
100
|
username: string;
|
|
106
101
|
port: number;
|
|
@@ -165,8 +160,6 @@ declare module '@deepseek-ai/cordis' {
|
|
|
165
160
|
export declare function awaitL<T>(operation: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
166
161
|
export declare function unwrap<T>(result: Result<T>): T;
|
|
167
162
|
export declare function validateTarget(t: ConnectTarget): void;
|
|
168
|
-
/** Only the declared public check identifiers/statuses, never provider evidence. */
|
|
169
|
-
export declare function hostContextNotice(t: ConnectTarget): string | undefined;
|
|
170
163
|
export declare function identity(t: ConnectTarget): string;
|
|
171
164
|
/** Reject draft.3 and legacy runtime rather than inventing workspace bindings. */
|
|
172
165
|
export declare function requireNativeWorkspace(api: ConnectApi): asserts api is ConnectApi & NativeWorkspaceApi;
|
|
@@ -183,6 +176,7 @@ export declare function connectProvider(get: () => ConnectApi | undefined, optio
|
|
|
183
176
|
id: string;
|
|
184
177
|
path: string;
|
|
185
178
|
}>;
|
|
179
|
+
source?: (request: RuntimeRequest, api: ConnectApi) => Promise<SessionSource | undefined>;
|
|
186
180
|
employee?: EmployeeProviderOptions;
|
|
187
181
|
local?: LocalRuntimeProvider;
|
|
188
182
|
details?: (request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<DetailChannel>;
|
package/dist/provider.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveHostWorkspace, workspaceSessionSource } from "./host-workspace.js";
|
|
2
2
|
import { connectNeedError } from "./connect-need.js";
|
|
3
3
|
import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
|
|
4
4
|
import { NeedError } from "./remote.js";
|
|
5
|
-
import { CONNECTION_REQUIRED
|
|
5
|
+
import { CONNECTION_REQUIRED } from "./presentation.js";
|
|
6
6
|
import { CODEX_AGENT } from "./agents.js";
|
|
7
7
|
export function requireLoginCoordinator(api) {
|
|
8
8
|
if (!['getConnectionState', 'getLoginPromptState', 'requestLoginPrompt', 'refreshLoginPrompts'].every(key => typeof api[key] === 'function'))
|
|
@@ -118,19 +118,6 @@ export function validateTarget(t) {
|
|
|
118
118
|
|| ![t.projectRoot, t.session.command, t.session.root, t.session.settingsPath].every(p => typeof p === 'string' && p.startsWith('/'))) {
|
|
119
119
|
throw new NeedError('CONNECT_API_MISMATCH', '我需要 connect-api 0.3.0 的可信主机、项目和 session 能力;旧目标不能继续使用', 'L 提供的完整 0.3.0 目标');
|
|
120
120
|
}
|
|
121
|
-
hostContextNotice(t);
|
|
122
|
-
}
|
|
123
|
-
/** Only the declared public check identifiers/statuses, never provider evidence. */
|
|
124
|
-
export function hostContextNotice(t) {
|
|
125
|
-
const h = t.hostContext;
|
|
126
|
-
if (h === undefined)
|
|
127
|
-
return;
|
|
128
|
-
const checks = (items) => Array.isArray(items) && items.every(item => item && typeof item.id === 'string' && typeof item.status === 'string');
|
|
129
|
-
if (!h || (h.enforcement !== 'test' && h.enforcement !== 'production') || (h.checkExitCode !== 0 && h.checkExitCode !== 1) || !checks(h.failedChecks) || !checks(h.unverifiedChecks))
|
|
130
|
-
throw new NeedError('CONNECT_API_MISMATCH', '主机检查背景无效,不能假定已通过生产检查', 'L 提供的有效 hostContext');
|
|
131
|
-
if (h.enforcement !== 'test')
|
|
132
|
-
return;
|
|
133
|
-
return TEST_HOST_NOTICE;
|
|
134
121
|
}
|
|
135
122
|
export function identity(t) { return `${t.username}@${t.host}:${t.port}`; }
|
|
136
123
|
/** Reject draft.3 and legacy runtime rather than inventing workspace bindings. */
|
|
@@ -158,30 +145,6 @@ export function sameTarget(a, b) {
|
|
|
158
145
|
}
|
|
159
146
|
export function connectProvider(get, options = {}) {
|
|
160
147
|
const agent = options.agent ?? CODEX_AGENT;
|
|
161
|
-
const hostScopes = new WeakMap();
|
|
162
|
-
const hostMethods = new WeakMap();
|
|
163
|
-
const retainHostScope = (runtime, scope) => {
|
|
164
|
-
hostScopes.get(runtime)?.dispose();
|
|
165
|
-
hostScopes.set(runtime, scope);
|
|
166
|
-
runtime.contextSignal = scope.revocation;
|
|
167
|
-
if (hostMethods.has(runtime))
|
|
168
|
-
return;
|
|
169
|
-
const original = { dispose: runtime.dispose, detach: runtime.detach };
|
|
170
|
-
hostMethods.set(runtime, original);
|
|
171
|
-
const release = () => { hostScopes.get(runtime)?.dispose(); hostScopes.delete(runtime); };
|
|
172
|
-
runtime.dispose = async (cause) => { try {
|
|
173
|
-
await original.dispose?.(cause);
|
|
174
|
-
}
|
|
175
|
-
finally {
|
|
176
|
-
release();
|
|
177
|
-
} };
|
|
178
|
-
runtime.detach = async (cause) => { try {
|
|
179
|
-
await original.detach?.(cause);
|
|
180
|
-
}
|
|
181
|
-
finally {
|
|
182
|
-
release();
|
|
183
|
-
} };
|
|
184
|
-
};
|
|
185
148
|
// Keep the legacy source SDK wrapper's strip-only import graph free of broker runtime syntax.
|
|
186
149
|
// Cache the initialization promise so concurrent sessions share this provider's binding lifetimes.
|
|
187
150
|
let employee;
|
|
@@ -195,7 +158,13 @@ export function connectProvider(get, options = {}) {
|
|
|
195
158
|
requireLoginCoordinator(api);
|
|
196
159
|
let sourceStage = 'input-acceptance';
|
|
197
160
|
try {
|
|
198
|
-
|
|
161
|
+
request.workspaceSource = options.source ? await options.source(request, api) : request.hostSource;
|
|
162
|
+
if (!options.source && !request.workspaceSource && options.workspace && request.sessionId && api.getContractInfo().workspaceLocations === 1) {
|
|
163
|
+
const workspace = await options.workspace(request);
|
|
164
|
+
request.workspaceSource = workspaceSessionSource(api, workspace.id, request);
|
|
165
|
+
}
|
|
166
|
+
const host = request.workspaceSource?.source === 'host';
|
|
167
|
+
if (options.employee && !host) {
|
|
199
168
|
const module = await import("./employee-provider.js");
|
|
200
169
|
request.localContext = await module.inspectEmployeeWorkspace(get, options.employee, agent, api, request);
|
|
201
170
|
}
|
|
@@ -211,15 +180,16 @@ export function connectProvider(get, options = {}) {
|
|
|
211
180
|
const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
|
|
212
181
|
return { target, connection };
|
|
213
182
|
})();
|
|
214
|
-
if (request.identity && request.identity !== identity(target))
|
|
183
|
+
if (request.workspaceSource?.source !== 'host' && request.identity && request.identity !== identity(target))
|
|
215
184
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '员工主机与原会话不一致,请新建会话', '原员工主机');
|
|
216
185
|
request.localAdmissionCheck?.();
|
|
217
186
|
admissions.set(request, { api, target, connection });
|
|
218
187
|
sourceStage = 'local-admission';
|
|
219
|
-
const locations = api.getContractInfo().workspaceLocations === 1 ? api.getWorkspaceLocationState?.() : undefined;
|
|
220
|
-
if (locations && (locations.phase === 'failed' || locations.phase === 'disposed') && locations.instanceId === connection.instanceId && locations.epoch === connection.epoch && locations.need)
|
|
188
|
+
const locations = !host && api.getContractInfo().workspaceLocations === 1 ? api.getWorkspaceLocationState?.() : undefined;
|
|
189
|
+
if (!host && locations && (locations.phase === 'failed' || locations.phase === 'disposed') && locations.instanceId === connection.instanceId && locations.epoch === connection.epoch && locations.need)
|
|
221
190
|
throw connectNeedError(locations.need);
|
|
222
|
-
|
|
191
|
+
if (!host)
|
|
192
|
+
await options.local?.admit?.(request, api, connection, target);
|
|
223
193
|
}
|
|
224
194
|
catch (error) {
|
|
225
195
|
admissions.delete(request);
|
|
@@ -238,59 +208,38 @@ export function connectProvider(get, options = {}) {
|
|
|
238
208
|
const { api, target, connection } = admitted;
|
|
239
209
|
if (!request.sessionId)
|
|
240
210
|
throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
|
|
241
|
-
const host =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return local;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (!host && agent.loginProvider === 'claude' && options.local) {
|
|
252
|
-
const runtime = await options.local(request, api, connection, target);
|
|
253
|
-
if (runtime)
|
|
254
|
-
return runtime;
|
|
255
|
-
}
|
|
256
|
-
// A local source must have been handled by its employee provider above.
|
|
257
|
-
// Host execution requires an explicit host location; no name-based resolver.
|
|
258
|
-
if (!host)
|
|
259
|
-
throw new NeedError('WORKSPACE_LOCATION_REQUIRED', '请选择文件夹来源;改变来源后请新建会话。', '已选择的本机或主机文件夹');
|
|
260
|
-
const resolved = await host.resolve();
|
|
261
|
-
const project = resolved.project;
|
|
262
|
-
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
263
|
-
throw new NeedError(host ? 'PROJECT_BINDING_INVALID' : 'SESSION_CONTEXT_MISMATCH', host ? '文件来源已改,请新建会话继续;原会话历史仍可查看。' : '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
264
|
-
const details = await options.details?.(request, api, connection, target);
|
|
265
|
-
try {
|
|
266
|
-
if (!options.open)
|
|
267
|
-
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
268
|
-
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 });
|
|
269
|
-
try {
|
|
270
|
-
host?.observeRuntime(runtime);
|
|
271
|
-
}
|
|
272
|
-
catch (error) {
|
|
273
|
-
void runtime.dispose?.().catch(() => console.warn('HOST_CLEANUP_FAILED'));
|
|
274
|
-
throw error;
|
|
275
|
-
}
|
|
276
|
-
if (host)
|
|
277
|
-
retainHostScope(runtime, host);
|
|
278
|
-
runtime.hostWorkspace = host && 'directory' in resolved ? Object.freeze({ cwd: resolved.directory.path }) : undefined;
|
|
279
|
-
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
280
|
-
runtime.details = details ?? runtime.details;
|
|
281
|
-
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
282
|
-
runtime.connection = { ...connection };
|
|
283
|
-
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
284
|
-
return runtime;
|
|
285
|
-
}
|
|
286
|
-
catch (error) {
|
|
287
|
-
throw error;
|
|
211
|
+
const host = request.workspaceSource?.source === 'host';
|
|
212
|
+
if (!host && options.employee) {
|
|
213
|
+
employee ??= import("./employee-provider.js").then(module => module.createEmployeeProvider(get, options, agent));
|
|
214
|
+
const local = await (await employee)({ api, target, connection, request });
|
|
215
|
+
if (local) {
|
|
216
|
+
local.loginRequired = () => offerLogin(api, request, get, agent);
|
|
217
|
+
return local;
|
|
288
218
|
}
|
|
289
219
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
220
|
+
if (!host && agent.loginProvider === 'claude' && options.local) {
|
|
221
|
+
const runtime = await options.local(request, api, connection, target);
|
|
222
|
+
if (runtime)
|
|
223
|
+
return runtime;
|
|
293
224
|
}
|
|
225
|
+
// A local source must have been handled by its employee provider above.
|
|
226
|
+
// Host execution requires an explicit host location; no name-based resolver.
|
|
227
|
+
if (!host)
|
|
228
|
+
throw new NeedError('WORKSPACE_LOCATION_REQUIRED', '请选择文件夹来源;改变来源后请新建会话。', '已选择的本机或主机文件夹');
|
|
229
|
+
const resolved = await resolveHostWorkspace(api, request, agent.loginProvider);
|
|
230
|
+
const project = resolved.project;
|
|
231
|
+
const details = await options.details?.(request, api, connection, target);
|
|
232
|
+
if (!options.open)
|
|
233
|
+
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
234
|
+
const runtime = await options.open(resolved.target, project, { ...request, agent, bindingId: undefined, workspaceId: undefined, bindingRevision: undefined, detailChannel: details });
|
|
235
|
+
runtime.hostSource = Object.freeze({ ...request.workspaceSource });
|
|
236
|
+
runtime.hostWorkspace = Object.freeze({ cwd: project.path });
|
|
237
|
+
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
238
|
+
runtime.details = details ?? runtime.details;
|
|
239
|
+
runtime.binding = undefined;
|
|
240
|
+
runtime.connection = { ...connection };
|
|
241
|
+
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
242
|
+
return runtime;
|
|
294
243
|
};
|
|
295
244
|
resolve.preflight = preflight;
|
|
296
245
|
return resolve;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type SessionSource } from './host-workspace.ts';
|
|
3
|
+
import type { ConnectApi, RuntimeRequest } from './provider.ts';
|
|
4
|
+
export declare const sessionSourceSpec: {
|
|
5
|
+
name: string;
|
|
6
|
+
version: number;
|
|
7
|
+
tables: {
|
|
8
|
+
sessions: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<string, {
|
|
9
|
+
source: "host";
|
|
10
|
+
host: string;
|
|
11
|
+
path: string;
|
|
12
|
+
} | {
|
|
13
|
+
source: "local";
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/** Official domain metadata, keyed by native session ID. No L files, local path probes or host identity hashes. */
|
|
18
|
+
export declare function createSessionSources(ctx: Context, getApi: () => ConnectApi | undefined): (request: RuntimeRequest, api: ConnectApi) => Promise<SessionSource | undefined>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
|
|
2
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { absoluteHostPath, workspaceSessionSource } from "./host-workspace.js";
|
|
5
|
+
import { NeedError } from "./remote.js";
|
|
6
|
+
const sourceSchema = z.discriminatedUnion('source', [
|
|
7
|
+
z.object({ source: z.literal('host'), host: z.string().min(1), path: z.string().refine(absoluteHostPath) }).strict(),
|
|
8
|
+
z.object({ source: z.literal('local') }).strict(),
|
|
9
|
+
]);
|
|
10
|
+
export const sessionSourceSpec = defineDomain({ name: 'ai_workbench_remote_sources', version: 1, tables: { sessions: domainTable(sourceSchema) } });
|
|
11
|
+
const storageNeed = () => new NeedError('WORKSPACE_SOURCE_STORAGE_FAILED', '这条会话的文件来源未能保存,本轮工作区准备失败;请恢复后在原会话重试。', '官方会话来源存储');
|
|
12
|
+
/** Official domain metadata, keyed by native session ID. No L files, local path probes or host identity hashes. */
|
|
13
|
+
export function createSessionSources(ctx, getApi) {
|
|
14
|
+
let domain;
|
|
15
|
+
const pending = new Map();
|
|
16
|
+
const captured = new WeakMap();
|
|
17
|
+
const open = () => {
|
|
18
|
+
if (!domain) {
|
|
19
|
+
const facility = ctx.get('storageDomain');
|
|
20
|
+
if (!facility)
|
|
21
|
+
throw storageNeed();
|
|
22
|
+
domain = facility.open(sessionSourceSpec).catch(error => { domain = undefined; throw error; });
|
|
23
|
+
}
|
|
24
|
+
return domain;
|
|
25
|
+
};
|
|
26
|
+
ctx.effect(() => async () => { if (domain)
|
|
27
|
+
await (await domain).close(); });
|
|
28
|
+
function candidate(session, api, request) {
|
|
29
|
+
if (request?.hostSource)
|
|
30
|
+
return sourceSchema.parse(request.hostSource);
|
|
31
|
+
const registry = ctx.get('workspaceRegistry');
|
|
32
|
+
// Creation can precede workspace membership publication. The official creation cwd identifies its workspace.
|
|
33
|
+
const rows = registry?.list().filter(row => request?.workspaceId ? row.id === request.workspaceId : row.sessionIds.includes(session.id) || row.path === session.header.cwd) ?? [];
|
|
34
|
+
if (rows.length !== 1)
|
|
35
|
+
return;
|
|
36
|
+
return workspaceSessionSource(api, rows[0].id, request);
|
|
37
|
+
}
|
|
38
|
+
function save(session, api, request) {
|
|
39
|
+
const id = session.id, existing = pending.get(id);
|
|
40
|
+
if (existing)
|
|
41
|
+
return existing;
|
|
42
|
+
const work = async () => {
|
|
43
|
+
const table = (await open()).table('sessions'), stored = table.get(id);
|
|
44
|
+
if (stored) {
|
|
45
|
+
const source = Object.freeze({ ...stored });
|
|
46
|
+
captured.set(session, source);
|
|
47
|
+
return source;
|
|
48
|
+
}
|
|
49
|
+
const chosen = captured.get(session) ?? candidate(session, api, request);
|
|
50
|
+
if (!chosen)
|
|
51
|
+
return;
|
|
52
|
+
captured.set(session, chosen);
|
|
53
|
+
await table.put(id, chosen);
|
|
54
|
+
return chosen;
|
|
55
|
+
};
|
|
56
|
+
const result = work().catch(error => { if (error instanceof NeedError)
|
|
57
|
+
throw error; throw Object.assign(storageNeed(), { cause: error }); });
|
|
58
|
+
pending.set(id, result);
|
|
59
|
+
void result.finally(() => pending.delete(id)).catch(() => { });
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
ctx.on('session/created', session => {
|
|
63
|
+
// Restored sessions migrate from their recorded replay/workspace at first turn.
|
|
64
|
+
if (session.firstLiveSeq !== 0)
|
|
65
|
+
return;
|
|
66
|
+
const api = getApi();
|
|
67
|
+
if (!api)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
const source = candidate(session, api);
|
|
71
|
+
if (source)
|
|
72
|
+
captured.set(session, source);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
ctx.logger('remote-agent').warn('创建时文件来源暂不可读;发送时将重试');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!captured.has(session))
|
|
79
|
+
return;
|
|
80
|
+
void save(session, api).catch(() => ctx.logger('remote-agent').warn('会话文件来源暂未保存;发送时将重试'));
|
|
81
|
+
});
|
|
82
|
+
return async (request, api) => {
|
|
83
|
+
if (!request.sessionId || api.getContractInfo?.().workspaceLocations !== 1 && !request.hostSource)
|
|
84
|
+
return;
|
|
85
|
+
const session = ctx.get('sessions')?.get(SessionId(request.sessionId));
|
|
86
|
+
if (!session)
|
|
87
|
+
throw storageNeed();
|
|
88
|
+
return save(session, api, request);
|
|
89
|
+
};
|
|
90
|
+
}
|
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.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"zod": "4.6.7"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.
|
|
39
|
+
"@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.29.tgz",
|
|
40
40
|
"@ai-workbench/network": "file:test/fixtures/ai-workbench-network-0.4.9.tgz",
|
|
41
41
|
"@deepseek-ai/cordis": "4.0.4",
|
|
42
42
|
"@deepseek-ai/cordis-plugin-group": "1.0.4",
|
|
@@ -63,10 +63,11 @@
|
|
|
63
63
|
"react-dom": "18.3.1",
|
|
64
64
|
"typescript": "5.9.3",
|
|
65
65
|
"zustand": "4.4.7",
|
|
66
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2"
|
|
66
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2"
|
|
67
68
|
},
|
|
68
69
|
"peerDependencies": {
|
|
69
|
-
"@ai-workbench/connect": "~0.7.
|
|
70
|
+
"@ai-workbench/connect": "~0.7.29",
|
|
70
71
|
"@ai-workbench/network": "~0.4.9",
|
|
71
72
|
"@deepseek-ai/cordis": "4.0.4",
|
|
72
73
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
@@ -76,7 +77,8 @@
|
|
|
76
77
|
"@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
|
|
77
78
|
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
|
|
78
79
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
79
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2"
|
|
80
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2"
|
|
80
82
|
},
|
|
81
83
|
"dsh": {
|
|
82
84
|
"bundle": {
|