@ai-workbench/remote-agent 0.8.2 → 0.8.5

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/dist/provider.js CHANGED
@@ -2,6 +2,7 @@ import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
2
2
  import { NeedError } from "./remote.js";
3
3
  import { CONNECTION_REQUIRED, TEST_HOST_NOTICE } from "./presentation.js";
4
4
  import { CODEX_AGENT } from "./agents.js";
5
+ import { StageTiming } from "./stage-timing.js";
5
6
  export function requireLoginCoordinator(api) {
6
7
  if (!['getConnectionState', 'getLoginPromptState', 'requestLoginPrompt', 'refreshLoginPrompts'].every(key => typeof api[key] === 'function'))
7
8
  throw new NeedError('CONNECT_API_MISMATCH', '我需要 connect-api 0.6.1 的统一登录询问与连接状态能力', 'L >=0.6.1');
@@ -155,6 +156,7 @@ export function connectProvider(get, options = {}) {
155
156
  const agent = options.agent ?? CODEX_AGENT;
156
157
  const admissions = new WeakMap();
157
158
  const preflight = async (request) => {
159
+ request.timing ??= new StageTiming();
158
160
  request.signal?.throwIfAborted();
159
161
  const api = get();
160
162
  if (!api)
@@ -163,13 +165,16 @@ export function connectProvider(get, options = {}) {
163
165
  requireLoginCoordinator(api);
164
166
  if (request.mode === 'one-shot' && !agent.oneShot)
165
167
  throw new NeedError('MODE_UNSUPPORTED', '此远程模型仅支持会话模式', '使用会话模式');
166
- const before = api.getConnectionState();
167
- const target = await ensureReady(api, agent.loginProvider, request.signal);
168
- validateTarget(target);
169
- request.signal?.throwIfAborted();
170
- if (get() !== api)
171
- throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
172
- const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
168
+ const { target, connection } = await request.timing.run('login-ready', async () => {
169
+ const before = api.getConnectionState();
170
+ const target = await ensureReady(api, agent.loginProvider, request.signal);
171
+ validateTarget(target);
172
+ request.signal?.throwIfAborted();
173
+ if (get() !== api)
174
+ throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
175
+ const connection = await waitForLogin(api, agent.loginProvider, before, get, request.signal);
176
+ return { target, connection };
177
+ });
173
178
  if (request.identity && request.identity !== identity(target))
174
179
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '员工主机与原会话不一致,请新建会话', '原员工主机');
175
180
  admissions.set(request, { api, target, connection });
@@ -180,13 +185,13 @@ export function connectProvider(get, options = {}) {
180
185
  const admitted = admissions.get(request);
181
186
  admissions.delete(request);
182
187
  const { api, target, connection } = admitted;
183
- const stable = () => waitForLogin(api, agent.loginProvider, connection, get, request.signal);
188
+ const stable = () => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, request.signal));
184
189
  await stable();
185
190
  if (!request.sessionId)
186
191
  throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
187
192
  await options.notice?.reconcile(request, api);
188
193
  await stable();
189
- const resolved = unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal));
194
+ const resolved = await request.timing.run('workspace-resolve', async () => unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal)));
190
195
  await stable();
191
196
  if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
192
197
  throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
@@ -196,32 +201,57 @@ export function connectProvider(get, options = {}) {
196
201
  throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
197
202
  if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
198
203
  throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
199
- const details = await options.details?.(request, api, connection, target);
200
- await stable();
201
- let workspaceNotice = undefined;
202
- if (resolved.notice) {
203
- if (!options.notice)
204
- throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
205
- workspaceNotice = await options.notice.deliver(resolved.notice, resolved, request, api);
206
- await stable();
207
- }
208
- if (!options.open)
209
- throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
210
- const runtime = await options.open(resolved.target, project, { ...request, agent });
204
+ const details = await request.timing.run('details-open', async () => options.details?.(request, api, connection, target));
211
205
  try {
206
+ if (details)
207
+ request.timing.bind(detail => details.record(detail));
212
208
  await stable();
209
+ let workspaceNotice = undefined;
210
+ if (resolved.notice) {
211
+ if (!options.notice)
212
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
213
+ workspaceNotice = await request.timing.run('workspace-notice', () => options.notice.deliver(resolved.notice, resolved, request, api));
214
+ await stable();
215
+ }
216
+ if (!options.open)
217
+ throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
218
+ const observedBinding = api.getWorkspaceBindingState?.();
219
+ const validateContext = async (ignoreCancellation = false) => {
220
+ const check = () => waitForLogin(api, agent.loginProvider, connection, get, ignoreCancellation ? undefined : request.signal);
221
+ await check();
222
+ if (observedBinding) {
223
+ const current = api.getWorkspaceBindingState();
224
+ if (current.instanceId !== observedBinding.instanceId || !current.items.some(item => item.bindingId === resolved.bindingId && item.workspaceId === resolved.workspaceId && item.status === 'ready'))
225
+ throw new NeedError('SESSION_CONTEXT_MISMATCH', '当前工作区绑定已改变,原会话操作未执行', '原生会话原绑定');
226
+ }
227
+ await check();
228
+ };
229
+ const runtime = await options.open(resolved.target, project, { ...request, agent, bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, bindingRevision: resolved.bindingRevision, detailChannel: details, validateContext });
230
+ try {
231
+ await stable();
232
+ }
233
+ catch (error) {
234
+ await runtime.dispose?.();
235
+ throw error;
236
+ }
237
+ // The transport can outlive a turn; presentation belongs only to this invocation.
238
+ runtime.workspaceNotice = workspaceNotice || undefined;
239
+ runtime.details = details ?? runtime.details;
240
+ runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
241
+ runtime.connection = { ...connection };
242
+ runtime.loginRequired = () => offerLogin(api, request, get, agent);
243
+ return runtime;
213
244
  }
214
245
  catch (error) {
215
- await runtime.dispose?.();
246
+ try {
247
+ await details?.flush();
248
+ }
249
+ catch (diagnostic) {
250
+ const code = (value) => value instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(value.code) ? value.code : 'UNKNOWN_FAILURE';
251
+ throw new NeedError('CONVERSATION_DETAILS_FAILED', '技术详情未能保存;本轮已停止', `原错误代码 ${code(error)};诊断错误代码 ${code(diagnostic)}`);
252
+ }
216
253
  throw error;
217
254
  }
218
- // The transport can outlive a turn; presentation belongs only to this invocation.
219
- runtime.workspaceNotice = workspaceNotice || undefined;
220
- runtime.details = details ?? runtime.details;
221
- runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
222
- runtime.connection = { ...connection };
223
- runtime.loginRequired = () => offerLogin(api, request, get, agent);
224
- return runtime;
225
255
  };
226
256
  resolve.preflight = preflight;
227
257
  return resolve;
@@ -0,0 +1,6 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { RuntimeRequest } from './provider.ts';
3
+ /** A step's accepted batch follows step/start in the official event contract. */
4
+ export declare function currentInputIds(ctx: Context, request: RuntimeRequest): Promise<readonly string[]>;
5
+ /** Read only the official persistence API after its awaited durability checkpoint. */
6
+ export declare function verifyRecoveryCheckpoint(ctx: Context, request: RuntimeRequest, provider: string): Promise<boolean>;
@@ -0,0 +1,71 @@
1
+ import { SessionId } from '@deepseek-ai/dsh-session';
2
+ import { BlockAssembler, expandAssistantStream } from '@deepseek-ai/dsh-llm';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ /** A step's accepted batch follows step/start in the official event contract. */
5
+ export async function currentInputIds(ctx, request) {
6
+ if (!request.sessionId)
7
+ return [];
8
+ const id = SessionId(request.sessionId), store = ctx.get('sessions'), session = store?.get(id);
9
+ const persistence = ctx.get('sessionPersistence');
10
+ if (!session || !store || !persistence || !await store.flush(session))
11
+ return [];
12
+ const handle = await persistence.open(id, 'read');
13
+ try {
14
+ if (handle.id !== id || handle.header?.id !== id || handle.access !== 'read')
15
+ return [];
16
+ const { events } = await handle.read();
17
+ if (!Array.isArray(events))
18
+ return [];
19
+ let step;
20
+ let ids = [];
21
+ for (const event of events) {
22
+ if (event.type === 'step/start') {
23
+ step = event;
24
+ ids = [];
25
+ }
26
+ else if (event.type === 'turn/start' || event.type === 'step/end' || event.type === 'turn/end') {
27
+ step = undefined;
28
+ ids = [];
29
+ }
30
+ else if (event.type === 'user/message' && step)
31
+ ids.push(event.data.id);
32
+ }
33
+ return ids;
34
+ }
35
+ finally {
36
+ await handle.close();
37
+ }
38
+ }
39
+ /** Read only the official persistence API after its awaited durability checkpoint. */
40
+ export async function verifyRecoveryCheckpoint(ctx, request, provider) {
41
+ if (!request.sessionId || !request.recoveryCheckpointId || !request.resumeSessionId)
42
+ return false;
43
+ const id = SessionId(request.sessionId), store = ctx.get('sessions'), session = store?.get(id);
44
+ const persistence = ctx.get('sessionPersistence');
45
+ if (!session || typeof store?.flush !== 'function' || typeof persistence?.open !== 'function')
46
+ return false;
47
+ if (!await store.flush(session))
48
+ return false;
49
+ const handle = await persistence.open(id, 'read');
50
+ try {
51
+ if (handle?.id !== id || handle.header?.id !== id || handle.access !== 'read')
52
+ return false;
53
+ const { events } = await handle.read();
54
+ if (!Array.isArray(events))
55
+ return false;
56
+ const final = events.findLast(e => e.type === 'assistant/message' && e.surfaceOp === 'append');
57
+ const live = session.deriveMessages().findLast(m => m.role === 'assistant');
58
+ if (final?.type !== 'assistant/message' || final.data.interrupted || !live || live.id !== final.data.message.id || !isDeepStrictEqual(live, final.data.message))
59
+ return false;
60
+ const message = final.data.message, pointer = message.source.replayState?.response;
61
+ if (message.source.provider !== provider || pointer?.agent !== provider || pointer.dshSessionId !== id || pointer.sessionId !== request.resumeSessionId || pointer.recoveryCheckpointId !== request.recoveryCheckpointId)
62
+ return false;
63
+ const assembled = new BlockAssembler();
64
+ for (const part of expandAssistantStream(final.data.stream))
65
+ assembled.push(part.chunk);
66
+ return ['stop', 'max-tokens'].includes(assembled.finish.kind) && isDeepStrictEqual(assembled.replayState, message.source.replayState) && isDeepStrictEqual(assembled.blocks(), message.content);
67
+ }
68
+ finally {
69
+ await handle.close();
70
+ }
71
+ }
@@ -0,0 +1,34 @@
1
+ import type { AgentCapabilities, SessionConfigOption } from '@agentclientprotocol/sdk/unstable';
2
+ export interface AcpRecovery {
3
+ sessionId: string;
4
+ capabilities: AgentCapabilities;
5
+ modelConfiguration?: {
6
+ configOptions: SessionConfigOption[];
7
+ resolvedEvidence?: {
8
+ modelValue: string;
9
+ resolvedModel: string;
10
+ };
11
+ };
12
+ configUpdates?: SessionConfigOption[][];
13
+ }
14
+ export interface RecoveryState {
15
+ version: 1;
16
+ phase: 'dirty' | 'clean' | 'ended';
17
+ scope: string;
18
+ brokerId: string;
19
+ instanceId?: string;
20
+ pid?: number;
21
+ cursor?: number;
22
+ checkpointId?: string;
23
+ acp?: AcpRecovery;
24
+ }
25
+ export interface RecoveryJournal {
26
+ read(): Promise<RecoveryState | undefined>;
27
+ write(state: RecoveryState): Promise<void>;
28
+ }
29
+ export declare function validateRecovery(value: unknown): RecoveryState;
30
+ /** Only negotiated booleans/known empty capability markers, never agent metadata. */
31
+ export declare function recoveryCapabilities(c: AgentCapabilities): AgentCapabilities;
32
+ export declare function recoveryKey(value: unknown): string;
33
+ /** History is immutable; current is atomically replaced only after fsync. No expiry. */
34
+ export declare function recoveryJournal(profileDir: string, key: string): RecoveryJournal;
@@ -0,0 +1,52 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { open, rename } from 'node:fs/promises';
4
+ import { constants } from 'node:fs';
5
+ import { privateDirectory, durableWrite, readJournal } from "./remote-details.js";
6
+ import { NeedError } from "./remote.js";
7
+ const invalid = () => new NeedError('RECOVERY_STORAGE_INVALID', '会话恢复记录无效,本轮尚未发送', '有效的私有会话恢复记录');
8
+ export function validateRecovery(value) {
9
+ const s = value;
10
+ if (!s || s.version !== 1 || !['dirty', 'clean', 'ended'].includes(s.phase) || !/^[a-f0-9]{64}$/.test(s.scope) || !/^r[a-f0-9]{23}$/.test(s.brokerId))
11
+ throw invalid();
12
+ if (s.instanceId !== undefined && !/^[a-f0-9-]{36}$/.test(s.instanceId) || s.pid !== undefined && (!Number.isSafeInteger(s.pid) || s.pid < 1))
13
+ throw invalid();
14
+ if (s.phase === 'clean' && (!s.instanceId || !Number.isSafeInteger(s.cursor) || s.cursor < 0 || typeof s.checkpointId !== 'string' || !s.checkpointId || !s.acp || typeof s.acp.sessionId !== 'string' || !s.acp.sessionId || !s.acp.capabilities || typeof s.acp.capabilities !== 'object' || Array.isArray(s.acp.capabilities)))
15
+ throw invalid();
16
+ return structuredClone(s);
17
+ }
18
+ /** Only negotiated booleans/known empty capability markers, never agent metadata. */
19
+ export function recoveryCapabilities(c) {
20
+ return { loadSession: c.loadSession === true, promptCapabilities: { image: c.promptCapabilities?.image === true, audio: c.promptCapabilities?.audio === true, embeddedContext: c.promptCapabilities?.embeddedContext === true }, sessionCapabilities: { ...(c.sessionCapabilities?.resume ? { resume: {} } : {}), ...(c.sessionCapabilities?.systemPrompt ? { systemPrompt: {} } : {}) } };
21
+ }
22
+ export function recoveryKey(value) { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
23
+ /** History is immutable; current is atomically replaced only after fsync. No expiry. */
24
+ export function recoveryJournal(profileDir, key) {
25
+ if (!/^[a-f0-9]{64}$/.test(key))
26
+ throw invalid();
27
+ const parent = join(profileDir, 'ai-workbench-remote-agent'), dir = join(parent, 'recovery', key), current = join(dir, 'current.json');
28
+ let tail = Promise.resolve();
29
+ const prepare = async () => { await privateDirectory(parent); await privateDirectory(join(parent, 'recovery')); await privateDirectory(dir); };
30
+ return {
31
+ async read() { await tail; await prepare(); try {
32
+ return validateRecovery(await readJournal(current));
33
+ }
34
+ catch (error) {
35
+ if (error?.code === 'ENOENT')
36
+ return;
37
+ throw error;
38
+ } },
39
+ write(state) { const value = validateRecovery(state); const task = tail.then(async () => { await prepare(); const id = randomUUID(); await durableWrite(join(dir, id + '.json'), value); const temp = join(dir, id + '.current'); const file = await open(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); try {
40
+ await file.writeFile(JSON.stringify(value) + '\n');
41
+ await file.sync();
42
+ }
43
+ finally {
44
+ await file.close();
45
+ } await rename(temp, current); const directory = await open(dir, 'r'); try {
46
+ await directory.sync();
47
+ }
48
+ finally {
49
+ await directory.close();
50
+ } }); tail = task; return task; },
51
+ };
52
+ }
@@ -19,6 +19,9 @@ export interface DetailRequest {
19
19
  identity: string;
20
20
  connection: Connection;
21
21
  }
22
+ export declare function privateDirectory(path: string): Promise<void>;
23
+ export declare function durableWrite(path: string, value: unknown): Promise<void>;
24
+ export declare function readJournal(path: string): Promise<unknown>;
22
25
  export declare function createRemoteDetails(options: {
23
26
  profileDir: string;
24
27
  getApi: () => RemoteDetailsApi | undefined;
@@ -23,7 +23,7 @@ function kind(observation) {
23
23
  return observation.severity === 'action-required' ? 'action-required' : 'unknown';
24
24
  return { extension: 'extensions', tool: 'tools', need: 'action-required', notice: 'extensions', session: 'extensions' }[observation.kind] ?? observation.kind;
25
25
  }
26
- async function privateDirectory(path) {
26
+ export async function privateDirectory(path) {
27
27
  if (!isAbsolute(path))
28
28
  throw failure('REMOTE_DETAIL_STORAGE_FAILED');
29
29
  // Existing parent/profile ownership is the host's responsibility; reject symlinks
@@ -57,7 +57,7 @@ async function privateDirectory(path) {
57
57
  if ((info.mode & 0o077) !== 0 || (process.getuid && info.uid !== process.getuid()))
58
58
  throw failure('REMOTE_DETAIL_STORAGE_FAILED');
59
59
  }
60
- async function durableWrite(path, value) {
60
+ export async function durableWrite(path, value) {
61
61
  const file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
62
62
  try {
63
63
  await file.writeFile(JSON.stringify(value) + '\n');
@@ -74,7 +74,7 @@ async function durableWrite(path, value) {
74
74
  await parent.close();
75
75
  }
76
76
  }
77
- async function readJournal(path) {
77
+ export async function readJournal(path) {
78
78
  const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
79
79
  try {
80
80
  const info = await file.stat();
@@ -0,0 +1,3 @@
1
+ import type { Stream } from '@agentclientprotocol/sdk/unstable';
2
+ /** Public Stream only. Server request/approval IDs are never changed. */
3
+ export declare function isolateRpcIds(stream: Stream): Stream;
@@ -0,0 +1,50 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { NeedError } from "./remote.js";
3
+ /** Public Stream only. Server request/approval IDs are never changed. */
4
+ export function isolateRpcIds(stream) {
5
+ const prefix = randomUUID() + '/';
6
+ const pending = new Map();
7
+ const readable = stream.readable.pipeThrough(new TransformStream({ transform(message, controller) {
8
+ const raw = message;
9
+ if (!raw.method && Object.hasOwn(raw, 'id')) {
10
+ if (typeof raw.id !== 'string' || !pending.has(raw.id))
11
+ throw new NeedError('ACP_FOREIGN_RESPONSE', '收到不属于当前客户端的协议响应', '原会话的完整客户端状态');
12
+ const id = pending.get(raw.id);
13
+ pending.delete(raw.id);
14
+ controller.enqueue({ ...raw, id });
15
+ }
16
+ else
17
+ controller.enqueue(message);
18
+ } }));
19
+ const writable = new WritableStream({ async write(message) {
20
+ const raw = message;
21
+ let outgoing = raw;
22
+ if (typeof raw.method === 'string' && Object.hasOwn(raw, 'id')) {
23
+ const id = prefix + JSON.stringify(raw.id);
24
+ pending.set(id, raw.id);
25
+ outgoing = { ...raw, id };
26
+ }
27
+ else if (raw.method === '$/cancel_request' && raw.params && typeof raw.params === 'object') {
28
+ const params = raw.params;
29
+ outgoing = { ...raw, params: { ...params, requestId: prefix + JSON.stringify(params.requestId) } };
30
+ }
31
+ const writer = stream.writable.getWriter();
32
+ try {
33
+ await writer.write(outgoing);
34
+ }
35
+ finally {
36
+ writer.releaseLock();
37
+ }
38
+ }, async close() { const writer = stream.writable.getWriter(); try {
39
+ await writer.close();
40
+ }
41
+ finally {
42
+ writer.releaseLock();
43
+ } }, async abort(reason) { const writer = stream.writable.getWriter(); try {
44
+ await writer.abort(reason);
45
+ }
46
+ finally {
47
+ writer.releaseLock();
48
+ } } });
49
+ return { readable, writable };
50
+ }
@@ -0,0 +1,27 @@
1
+ import type { DetailObservation } from './conversation-details.ts';
2
+ export declare const STAGES: Readonly<{
3
+ readonly 'login-ready': "登录就绪检查";
4
+ readonly 'connection-confirm': "核对当前连接状态";
5
+ readonly 'input-prepare': "准备本轮输入";
6
+ readonly 'workspace-resolve': "解析工作区";
7
+ readonly 'details-open': "打开技术详情";
8
+ readonly 'details-flush': "等待技术详情持久确认";
9
+ readonly 'workspace-notice': "记录工作区说明";
10
+ readonly 'broker-list': "查询主机会话";
11
+ readonly 'broker-start': "启动主机会话";
12
+ readonly 'broker-attach': "连接主机会话";
13
+ readonly 'acp-initialize': "初始化远程模型";
14
+ readonly 'acp-session-new': "新建模型会话";
15
+ readonly 'acp-session-load': "加载模型会话";
16
+ readonly 'acp-session-resume': "恢复模型会话";
17
+ readonly 'acp-prompt': "模型处理本轮(含等待审批)";
18
+ }>;
19
+ export type Stage = keyof typeof STAGES;
20
+ /** Passive client observations; no thresholds, retries, or remote-clock claims. */
21
+ export declare class StageTiming {
22
+ private readonly started;
23
+ private pending;
24
+ private sink?;
25
+ bind(sink: (detail: DetailObservation) => void): void;
26
+ run<T>(phase: Stage, operation: () => Promise<T>): Promise<T>;
27
+ }
@@ -0,0 +1,39 @@
1
+ export const STAGES = Object.freeze({
2
+ 'login-ready': '登录就绪检查', 'connection-confirm': '核对当前连接状态', 'input-prepare': '准备本轮输入', 'workspace-resolve': '解析工作区',
3
+ 'details-open': '打开技术详情', 'details-flush': '等待技术详情持久确认', 'workspace-notice': '记录工作区说明',
4
+ 'broker-list': '查询主机会话', 'broker-start': '启动主机会话', 'broker-attach': '连接主机会话',
5
+ 'acp-initialize': '初始化远程模型', 'acp-session-new': '新建模型会话',
6
+ 'acp-session-load': '加载模型会话', 'acp-session-resume': '恢复模型会话',
7
+ 'acp-prompt': '模型处理本轮(含等待审批)',
8
+ });
9
+ /** Passive client observations; no thresholds, retries, or remote-clock claims. */
10
+ export class StageTiming {
11
+ started = performance.now();
12
+ pending = [];
13
+ sink;
14
+ bind(sink) {
15
+ this.sink = sink;
16
+ const pending = this.pending;
17
+ this.pending = [];
18
+ for (const detail of pending)
19
+ sink(detail);
20
+ }
21
+ async run(phase, operation) {
22
+ const started = performance.now();
23
+ let outcome = 'failed';
24
+ try {
25
+ const result = await operation();
26
+ outcome = 'ok';
27
+ return result;
28
+ }
29
+ finally {
30
+ const ended = performance.now();
31
+ const durationMs = ended - started, elapsedMs = ended - this.started;
32
+ const detail = { kind: 'session', severity: outcome === 'ok' ? 'info' : 'warning', code: 'REMOTE_STAGE_TIMING', summary: STAGES[phase] + ':' + durationMs.toFixed(1) + '毫秒', payload: { phase, durationMs, elapsedMs, outcome, clock: 'client-monotonic' } };
33
+ if (this.sink)
34
+ this.sink(detail);
35
+ else
36
+ this.pending.push(detail);
37
+ }
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-workbench/remote-agent",
3
- "version": "0.8.2",
3
+ "version": "0.8.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,7 +19,7 @@
19
19
  "cordis.patch.yml"
20
20
  ],
21
21
  "scripts": {
22
- "test": "npm run build && node --test lib/test/*.test.js",
22
+ "test": "npm run build && node --test --test-concurrency=1 lib/test/*.test.js",
23
23
  "typecheck": "node node_modules/typescript/bin/tsc --noEmit",
24
24
  "build": "node node_modules/typescript/bin/tsc && node node_modules/typescript/bin/tsc -p tsconfig.build.json && chmod 755 lib/src/sdk-ssh-wrapper.js dist/sdk-ssh-wrapper.js && node tools/build-client.mjs",
25
25
  "probe:host": "node tools/probe-host.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.11.tgz",
39
+ "@ai-workbench/connect": "file:test/fixtures/ai-workbench-connect-0.7.13.tgz",
40
40
  "@deepseek-ai/cordis": "4.0.4",
41
41
  "@deepseek-ai/cordis-plugin-group": "1.0.4",
42
42
  "@deepseek-ai/cordis-plugin-include": "1.0.9",
@@ -56,7 +56,7 @@
56
56
  "@deepseek-ai/dsh-attachment": "0.2.0-rc.2"
57
57
  },
58
58
  "peerDependencies": {
59
- "@ai-workbench/connect": "~0.7.11",
59
+ "@ai-workbench/connect": "~0.7.13",
60
60
  "@deepseek-ai/cordis": "4.0.4",
61
61
  "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
62
62
  "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",