@ai-workbench/remote-agent 0.8.1 → 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/README.md +19 -4
- package/dist/acp.d.ts +25 -0
- package/dist/acp.js +159 -17
- package/dist/approval-label.d.ts +6 -0
- package/dist/approval-label.js +45 -0
- package/dist/auth-reason.d.ts +1 -0
- package/dist/auth-reason.js +27 -0
- package/dist/broker-runtime.d.ts +10 -0
- package/dist/broker-runtime.js +302 -0
- package/dist/broker.d.ts +29 -6
- package/dist/broker.js +141 -141
- package/dist/connect-contract.d.ts +2 -1
- package/dist/conversation-details.d.ts +1 -0
- package/dist/conversation-details.js +58 -8
- package/dist/index.d.ts +31 -14
- package/dist/index.js +248 -42
- package/dist/input.d.ts +4 -0
- package/dist/input.js +4 -1
- package/dist/login-diagnostic.d.ts +38 -0
- package/dist/login-diagnostic.js +29 -0
- package/dist/model-catalog.d.ts +55 -0
- package/dist/model-catalog.js +254 -0
- package/dist/model-configuration.d.ts +51 -0
- package/dist/model-configuration.js +212 -0
- package/dist/presentation.js +4 -0
- package/dist/protocol-boundary.d.ts +13 -0
- package/dist/protocol-boundary.js +49 -0
- package/dist/provider.d.ts +21 -3
- package/dist/provider.js +135 -41
- package/dist/readiness.js +7 -5
- package/dist/recovery-checkpoint.d.ts +6 -0
- package/dist/recovery-checkpoint.js +71 -0
- package/dist/recovery.d.ts +34 -0
- package/dist/recovery.js +52 -0
- package/dist/remote-details.d.ts +17 -1
- package/dist/remote-details.js +137 -6
- package/dist/remote.d.ts +3 -0
- package/dist/remote.js +2 -0
- package/dist/rpc-namespace.d.ts +3 -0
- package/dist/rpc-namespace.js +50 -0
- package/dist/stage-timing.d.ts +27 -0
- package/dist/stage-timing.js +39 -0
- package/package.json +4 -4
package/dist/provider.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { loginDiagnostic, loginReason } from "./login-diagnostic.js";
|
|
1
2
|
import { NeedError } from "./remote.js";
|
|
2
3
|
import { CONNECTION_REQUIRED, TEST_HOST_NOTICE } from "./presentation.js";
|
|
3
4
|
import { CODEX_AGENT } from "./agents.js";
|
|
5
|
+
import { StageTiming } from "./stage-timing.js";
|
|
4
6
|
export function requireLoginCoordinator(api) {
|
|
5
7
|
if (!['getConnectionState', 'getLoginPromptState', 'requestLoginPrompt', 'refreshLoginPrompts'].every(key => typeof api[key] === 'function'))
|
|
6
8
|
throw new NeedError('CONNECT_API_MISMATCH', '我需要 connect-api 0.6.1 的统一登录询问与连接状态能力', 'L >=0.6.1');
|
|
@@ -8,6 +10,8 @@ export function requireLoginCoordinator(api) {
|
|
|
8
10
|
export function requireLoginState(state) {
|
|
9
11
|
const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v) && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
|
|
10
12
|
const need = (v) => object(v) && typeof v.code === 'string' && v.code.length > 0 && ['enrollment', 'network', 'ssh', 'auth', 'integration', 'settings', 'projects'].includes(v.layer) && typeof v.message === 'string' && typeof v.requires === 'string' && ['employee', 'administrator', 'ai-administrator', 'plugin'].includes(v.actor) && object(v.action) && typeof v.action.id === 'string' && typeof v.action.label === 'string' && typeof v.action.available === 'boolean';
|
|
13
|
+
if (state.queuedProviders !== undefined && (!Array.isArray(state.queuedProviders) || state.queuedProviders.some(p => p !== 'codex' && p !== 'claude') || new Set(state.queuedProviders).size !== state.queuedProviders.length || state.queuedProviders.some(p => state.checkingProviders?.includes(p))))
|
|
14
|
+
throw new NeedError('CONNECT_API_MISMATCH', '我需要真实的品牌排队与进行中状态', 'L >=0.7.10');
|
|
11
15
|
if (!Array.isArray(state.checkingProviders) || state.checkingProviders.some(p => p !== 'codex' && p !== 'claude') || !object(state.providerNeeds) || Object.entries(state.providerNeeds).some(([p, v]) => !['codex', 'claude'].includes(p) || !need(v)))
|
|
12
16
|
throw new NeedError('CONNECT_API_MISMATCH', '我需要按助手区分的真实登录状态', 'L >=0.6.1');
|
|
13
17
|
}
|
|
@@ -32,15 +36,77 @@ function verifiedLogin(api, brand, expected) {
|
|
|
32
36
|
if (!connection.instanceId || !Number.isSafeInteger(connection.epoch) || connection.epoch < 0 || connection.phase !== 'connected' || expected && (connection.instanceId !== expected.instanceId || connection.epoch !== expected.epoch))
|
|
33
37
|
throw new NeedError('CONNECTION_LOST', 'connection binding changed', 'current employee connection');
|
|
34
38
|
requireLoginState(login);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
throw new NeedError('LOGIN_REQUIRED', '需要登录,请到设置 → 远程主机连接 点登录', '本人登录');
|
|
39
|
+
const reason = loginReason(login, connection, brand);
|
|
40
|
+
if (reason)
|
|
41
|
+
throw Object.assign(new NeedError(reason === 'LOGIN_REQUIRED' ? 'LOGIN_REQUIRED' : 'AUTH_STATUS_INVALID', reason === 'LOGIN_REQUIRED' ? '需要登录,请到设置 → 远程主机连接 点登录' : '无法确认当前品牌的登录状态', 'L核验的本品牌登录状态'), { technical: loginDiagnostic('verified-login', reason, brand, connection, login), ...(typeof login.providerNeeds[brand]?.officialReason === 'string' ? { officialReason: login.providerNeeds[brand].officialReason } : {}) });
|
|
39
42
|
return { ...connection };
|
|
40
43
|
}
|
|
44
|
+
/** Wait only for L's explicit in-progress evidence. No timer, polling, refresh or authorization. */
|
|
45
|
+
async function waitForLogin(api, brand, expected, get, signal) {
|
|
46
|
+
const check = () => {
|
|
47
|
+
signal?.throwIfAborted();
|
|
48
|
+
if (get() !== api)
|
|
49
|
+
throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
|
|
50
|
+
const connection = api.getConnectionState(), state = api.getLoginPromptState();
|
|
51
|
+
requireLoginState(state);
|
|
52
|
+
if (connection.phase !== 'connected' || connection.instanceId !== expected.instanceId || connection.epoch !== expected.epoch)
|
|
53
|
+
return verifiedLogin(api, brand, expected);
|
|
54
|
+
const pending = state.instanceId === connection.instanceId && state.phase !== 'blocked' && state.phase !== 'disposed' && (state.queuedProviders?.includes(brand) || state.checkingProviders.includes(brand) || state.phase === 'authorizing' && state.provider === brand);
|
|
55
|
+
return pending ? undefined : verifiedLogin(api, brand, expected);
|
|
56
|
+
};
|
|
57
|
+
const ready = check();
|
|
58
|
+
if (ready)
|
|
59
|
+
return ready;
|
|
60
|
+
if (typeof api.onLoginPromptState !== 'function' || typeof api.onConnectionState !== 'function')
|
|
61
|
+
throw new NeedError('CONNECT_API_MISMATCH', '我需要登录检查状态订阅能力', 'L onLoginPromptState / onConnectionState');
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
let ended = false;
|
|
64
|
+
const disposers = [];
|
|
65
|
+
const clear = () => { ended = true; signal?.removeEventListener('abort', abort); for (const off of disposers)
|
|
66
|
+
off(); };
|
|
67
|
+
const changed = () => { if (ended)
|
|
68
|
+
return; try {
|
|
69
|
+
const value = check();
|
|
70
|
+
if (value) {
|
|
71
|
+
clear();
|
|
72
|
+
resolve(value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
clear();
|
|
77
|
+
reject(error);
|
|
78
|
+
} };
|
|
79
|
+
const abort = () => { if (ended)
|
|
80
|
+
return; clear(); reject(new NeedError('ABORTED', 'consumer wait cancelled', 'employee action')); };
|
|
81
|
+
const register = (off) => { if (ended)
|
|
82
|
+
off();
|
|
83
|
+
else
|
|
84
|
+
disposers.push(off); };
|
|
85
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
86
|
+
try {
|
|
87
|
+
if (signal?.aborted)
|
|
88
|
+
abort();
|
|
89
|
+
if (!ended)
|
|
90
|
+
register(api.onLoginPromptState(changed));
|
|
91
|
+
if (!ended)
|
|
92
|
+
register(api.onConnectionState(changed));
|
|
93
|
+
changed();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
clear();
|
|
97
|
+
reject(error);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function ensureReady(api, brand, signal) {
|
|
102
|
+
const result = await awaitL(() => api.ensureReady(brand), signal);
|
|
103
|
+
if (!result.ok)
|
|
104
|
+
throw Object.assign(new NeedError(result.error.code, result.error.message, result.error.requires), { technical: loginDiagnostic('ensure-ready', 'L_ENSURE_READY_FAILED', brand, api.getConnectionState(), api.getLoginPromptState(), result.error.officialReason), ...(typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {}) });
|
|
105
|
+
return result.value;
|
|
106
|
+
}
|
|
41
107
|
export function unwrap(result) {
|
|
42
108
|
if (!result.ok)
|
|
43
|
-
throw new NeedError(result.error.code, result.error.message, result.error.requires);
|
|
109
|
+
throw Object.assign(new NeedError(result.error.code, result.error.message, result.error.requires), typeof result.error.officialReason === 'string' ? { officialReason: result.error.officialReason } : {});
|
|
44
110
|
return result.value;
|
|
45
111
|
}
|
|
46
112
|
export function validateTarget(t) {
|
|
@@ -76,12 +142,12 @@ export async function offerLogin(connect, request, currentApi, agent = CODEX_AGE
|
|
|
76
142
|
requireNativeWorkspace(connect);
|
|
77
143
|
request.signal?.throwIfAborted();
|
|
78
144
|
const before = connect.getConnectionState();
|
|
79
|
-
const target =
|
|
145
|
+
const target = await ensureReady(connect, agent.loginProvider, request.signal);
|
|
80
146
|
validateTarget(target);
|
|
81
147
|
request.signal?.throwIfAborted();
|
|
82
148
|
if (currentApi && currentApi() !== connect)
|
|
83
149
|
throw new NeedError('CONNECTION_LOST', '远程主机连接组件已更换', '当前员工连接');
|
|
84
|
-
|
|
150
|
+
await waitForLogin(connect, agent.loginProvider, before, currentApi ?? (() => connect), request.signal);
|
|
85
151
|
}
|
|
86
152
|
export function sameTarget(a, b) {
|
|
87
153
|
return identity(a) === identity(b) && a.projectRoot === b.projectRoot && a.knownHostsFile === b.knownHostsFile && a.proxyCommand === b.proxyCommand && a.session.command === b.session.command && a.session.root === b.session.root && a.session.settingsPath === b.session.settingsPath;
|
|
@@ -90,6 +156,7 @@ export function connectProvider(get, options = {}) {
|
|
|
90
156
|
const agent = options.agent ?? CODEX_AGENT;
|
|
91
157
|
const admissions = new WeakMap();
|
|
92
158
|
const preflight = async (request) => {
|
|
159
|
+
request.timing ??= new StageTiming();
|
|
93
160
|
request.signal?.throwIfAborted();
|
|
94
161
|
const api = get();
|
|
95
162
|
if (!api)
|
|
@@ -98,13 +165,16 @@ export function connectProvider(get, options = {}) {
|
|
|
98
165
|
requireLoginCoordinator(api);
|
|
99
166
|
if (request.mode === 'one-shot' && !agent.oneShot)
|
|
100
167
|
throw new NeedError('MODE_UNSUPPORTED', '此远程模型仅支持会话模式', '使用会话模式');
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
});
|
|
108
178
|
if (request.identity && request.identity !== identity(target))
|
|
109
179
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '员工主机与原会话不一致,请新建会话', '原员工主机');
|
|
110
180
|
admissions.set(request, { api, target, connection });
|
|
@@ -115,15 +185,14 @@ export function connectProvider(get, options = {}) {
|
|
|
115
185
|
const admitted = admissions.get(request);
|
|
116
186
|
admissions.delete(request);
|
|
117
187
|
const { api, target, connection } = admitted;
|
|
118
|
-
const stable = () =>
|
|
119
|
-
|
|
120
|
-
stable();
|
|
188
|
+
const stable = () => request.timing.run('connection-confirm', () => waitForLogin(api, agent.loginProvider, connection, get, request.signal));
|
|
189
|
+
await stable();
|
|
121
190
|
if (!request.sessionId)
|
|
122
191
|
throw new NeedError('PROJECT_BINDING_REQUIRED', '我需要当前会话和已选择的工作区,请使用原生工作区入口', 'DSH会话与原生工作区');
|
|
123
192
|
await options.notice?.reconcile(request, api);
|
|
124
|
-
stable();
|
|
125
|
-
const resolved = unwrap(await awaitL(() => api.resolveSessionProject(request.sessionId, { provider: agent.loginProvider, purpose: 'user-turn' }), request.signal));
|
|
126
|
-
stable();
|
|
193
|
+
await stable();
|
|
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)));
|
|
195
|
+
await stable();
|
|
127
196
|
if (!resolved || resolved.instanceId !== connection.instanceId || resolved.epoch !== connection.epoch)
|
|
128
197
|
throw new NeedError('CONNECTION_LOST', '工作区解析回复已失效', '当前连接代次');
|
|
129
198
|
validateTarget(resolved.target);
|
|
@@ -132,32 +201,57 @@ export function connectProvider(get, options = {}) {
|
|
|
132
201
|
throw new NeedError('PROJECT_BINDING_INVALID', '工作区绑定与当前会话或可信主机不一致', 'L核验的原生工作区绑定');
|
|
133
202
|
if (request.cwd && request.cwd !== project.path || request.bindingId && request.bindingId !== resolved.bindingId || request.workspaceId && request.workspaceId !== resolved.workspaceId)
|
|
134
203
|
throw new NeedError('SESSION_CONTEXT_MISMATCH', '原会话的工作区绑定已改变,请新建会话', '原工作区绑定');
|
|
135
|
-
const details = await options.details?.(request, api, connection, target);
|
|
136
|
-
stable();
|
|
137
|
-
let workspaceNotice = undefined;
|
|
138
|
-
if (resolved.notice) {
|
|
139
|
-
if (!options.notice)
|
|
140
|
-
throw new NeedError('DEPENDENCY_REQUIRED', '我需要官方会话持久记录来确认工作区说明', '官方会话与持久化服务');
|
|
141
|
-
workspaceNotice = await options.notice.deliver(resolved.notice, resolved, request, api);
|
|
142
|
-
stable();
|
|
143
|
-
}
|
|
144
|
-
if (!options.open)
|
|
145
|
-
throw new NeedError('RUNTIME_REQUIRED', '我需要主机运行适配器', '主机broker适配');
|
|
146
|
-
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));
|
|
147
205
|
try {
|
|
148
|
-
|
|
206
|
+
if (details)
|
|
207
|
+
request.timing.bind(detail => details.record(detail));
|
|
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;
|
|
149
244
|
}
|
|
150
245
|
catch (error) {
|
|
151
|
-
|
|
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
|
+
}
|
|
152
253
|
throw error;
|
|
153
254
|
}
|
|
154
|
-
// The transport can outlive a turn; presentation belongs only to this invocation.
|
|
155
|
-
runtime.workspaceNotice = workspaceNotice || undefined;
|
|
156
|
-
runtime.details = details ?? runtime.details;
|
|
157
|
-
runtime.binding = { bindingId: resolved.bindingId, workspaceId: resolved.workspaceId, revision: resolved.bindingRevision };
|
|
158
|
-
runtime.connection = { ...connection };
|
|
159
|
-
runtime.loginRequired = () => offerLogin(api, request, get, agent);
|
|
160
|
-
return runtime;
|
|
161
255
|
};
|
|
162
256
|
resolve.preflight = preflight;
|
|
163
257
|
return resolve;
|
package/dist/readiness.js
CHANGED
|
@@ -32,6 +32,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
32
32
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
33
|
done = true;
|
|
34
34
|
};
|
|
35
|
+
import { loginDiagnostic } from "./login-diagnostic.js";
|
|
35
36
|
import { randomUUID } from 'node:crypto';
|
|
36
37
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
37
38
|
import { unwrap, awaitL, requireLoginCoordinator, requireLoginState } from "./provider.js";
|
|
@@ -56,18 +57,19 @@ function snapshot(api, agent = CODEX_AGENT) {
|
|
|
56
57
|
if (connection.phase !== 'connected')
|
|
57
58
|
return denied(connection.need?.code ?? (connection.phase === 'disposed' ? 'DISPOSED' : 'CONNECTION_LOST'));
|
|
58
59
|
const state = api.getLoginPromptState();
|
|
60
|
+
const diagnostic = (reason) => ({ label: '登录检查快照', value: JSON.stringify(loginDiagnostic('readiness', reason, brand, connection, state)) });
|
|
59
61
|
if (!connection.instanceId || state.instanceId !== connection.instanceId)
|
|
60
|
-
return denied('AUTH_STATUS_INVALID');
|
|
62
|
+
return denied('AUTH_STATUS_INVALID', [diagnostic('LOGIN_INSTANCE_MISMATCH')]);
|
|
61
63
|
requireLoginState(state);
|
|
62
64
|
const technical = details({ hostContext: state.technicalDetails?.hostContext });
|
|
63
65
|
if (state.phase === 'blocked' || state.phase === 'disposed')
|
|
64
|
-
return denied(state.technicalDetails?.code ?? 'AUTH_STATUS_INVALID', technical);
|
|
65
|
-
if (state.checkingProviders.includes(brand) || state.phase === 'authorizing' && state.provider === brand)
|
|
66
|
+
return denied(state.technicalDetails?.code ?? 'AUTH_STATUS_INVALID', [...technical, diagnostic(state.phase === 'disposed' ? 'LOGIN_PHASE_DISPOSED' : 'LOGIN_PHASE_BLOCKED')]);
|
|
67
|
+
if (state.queuedProviders?.includes(brand) || state.checkingProviders.includes(brand) || state.phase === 'authorizing' && state.provider === brand)
|
|
66
68
|
return { phase: 'checking', loginStatus: 'unknown', title: `正在检查 ${agent.name.zh} 登录状态`, message: '远程主机连接正在检查登录状态。', technical };
|
|
67
69
|
if (state.providerNeeds[brand])
|
|
68
|
-
return denied(state.providerNeeds[brand].code, technical);
|
|
70
|
+
return denied(state.providerNeeds[brand].code, [...technical, diagnostic('LOGIN_PROVIDER_NEED')]);
|
|
69
71
|
if (typeof state.logins?.[brand] !== 'boolean')
|
|
70
|
-
return denied('AUTH_STATUS_INVALID', technical);
|
|
72
|
+
return denied('AUTH_STATUS_INVALID', [...technical, diagnostic('LOGIN_VALUE_INVALID')]);
|
|
71
73
|
return state.logins[brand] ? { phase: 'ready', loginStatus: 'logged-in', title: `${agent.name.zh}已登录`, message: '本次远程主机连接和主机登录检查已完成。', technical } : { phase: 'login-required', loginStatus: 'not-logged-in', title: `${agent.name.zh} 尚未登录`, message: '需要登录,请到设置 → 远程主机连接 点登录', technical };
|
|
72
74
|
}
|
|
73
75
|
export function createReadiness(get, agent = CODEX_AGENT) {
|
|
@@ -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;
|
package/dist/recovery.js
ADDED
|
@@ -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
|
+
}
|
package/dist/remote-details.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type DetailObservation } from './conversation-details.ts';
|
|
2
|
+
import type { LoginDiagnostic } from './login-diagnostic.ts';
|
|
2
3
|
import type { ConnectApi as LConnectApi, ConnectionState } from '@ai-workbench/connect/api';
|
|
3
4
|
export type { RemoteConversationDetail as RemoteDetail } from '@ai-workbench/connect/api';
|
|
4
5
|
type Connection = Pick<ConnectionState, 'instanceId' | 'epoch' | 'phase'>;
|
|
5
|
-
export type RemoteDetailsApi = Pick<LConnectApi, 'getConnectionState'> & Partial<Pick<LConnectApi, 'getContractInfo' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails'>>;
|
|
6
|
+
export type RemoteDetailsApi = Pick<LConnectApi, 'getConnectionState'> & Partial<Pick<LConnectApi, 'getContractInfo' | 'appendRemoteConversationDetail' | 'getRemoteConversationDetailReceipt' | 'listRemoteConversationDetails' | 'appendLoginDiagnostic' | 'getLoginDiagnosticReceipt' | 'listLoginDiagnostics'>>;
|
|
6
7
|
export interface DetailChannel {
|
|
7
8
|
record(observation: DetailObservation): void;
|
|
8
9
|
beginTurn(ids: {
|
|
@@ -18,9 +19,24 @@ export interface DetailRequest {
|
|
|
18
19
|
identity: string;
|
|
19
20
|
connection: Connection;
|
|
20
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>;
|
|
21
25
|
export declare function createRemoteDetails(options: {
|
|
22
26
|
profileDir: string;
|
|
23
27
|
getApi: () => RemoteDetailsApi | undefined;
|
|
24
28
|
}): {
|
|
29
|
+
recordLoginFailure(request: {
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
code: string;
|
|
32
|
+
diagnostic: LoginDiagnostic;
|
|
33
|
+
context?: {
|
|
34
|
+
turn: string | number | null;
|
|
35
|
+
step: string | number | null;
|
|
36
|
+
};
|
|
37
|
+
}): Promise<{
|
|
38
|
+
eventId: string;
|
|
39
|
+
recorded: true;
|
|
40
|
+
}>;
|
|
25
41
|
open(request: DetailRequest): Promise<DetailChannel>;
|
|
26
42
|
};
|