@ai-workbench/remote-agent 0.0.0-stage → 0.8.1

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.
Files changed (46) hide show
  1. package/README.md +58 -2
  2. package/cordis.patch.yml +3 -0
  3. package/dist/acp.d.ts +54 -0
  4. package/dist/acp.js +341 -0
  5. package/dist/agents.d.ts +15 -0
  6. package/dist/agents.js +4 -0
  7. package/dist/broker.d.ts +67 -0
  8. package/dist/broker.js +397 -0
  9. package/dist/client.js +36 -0
  10. package/dist/connect-contract.d.ts +26 -0
  11. package/dist/connect-contract.js +1 -0
  12. package/dist/conversation-details.d.ts +47 -0
  13. package/dist/conversation-details.js +319 -0
  14. package/dist/host-layout.d.ts +8 -0
  15. package/dist/host-layout.js +9 -0
  16. package/dist/index.d.ts +114 -0
  17. package/dist/index.js +360 -0
  18. package/dist/input.d.ts +12 -0
  19. package/dist/input.js +49 -0
  20. package/dist/interaction.d.ts +19 -0
  21. package/dist/interaction.js +1 -0
  22. package/dist/job.d.ts +17 -0
  23. package/dist/job.js +57 -0
  24. package/dist/mapping.d.ts +46 -0
  25. package/dist/mapping.js +230 -0
  26. package/dist/oneshot.d.ts +13 -0
  27. package/dist/oneshot.js +78 -0
  28. package/dist/presentation.d.ts +6 -0
  29. package/dist/presentation.js +45 -0
  30. package/dist/provider-locale.d.ts +19 -0
  31. package/dist/provider-locale.js +73 -0
  32. package/dist/provider.d.ts +144 -0
  33. package/dist/provider.js +164 -0
  34. package/dist/readiness.d.ts +28 -0
  35. package/dist/readiness.js +192 -0
  36. package/dist/remote-details.d.ts +26 -0
  37. package/dist/remote-details.js +250 -0
  38. package/dist/remote.d.ts +34 -0
  39. package/dist/remote.js +51 -0
  40. package/dist/sdk-ssh-wrapper.d.ts +2 -0
  41. package/dist/sdk-ssh-wrapper.js +34 -0
  42. package/dist/workspace-notice.d.ts +47 -0
  43. package/dist/workspace-notice.js +273 -0
  44. package/locale/en.json +10 -0
  45. package/locale/zh.json +10 -0
  46. package/package.json +97 -4
package/dist/broker.js ADDED
@@ -0,0 +1,397 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { spawn } from 'node:child_process';
10
+ import { createHash } from 'node:crypto';
11
+ import { protectedOneShotLayout } from "./host-layout.js";
12
+ import { NeedError, shellQuote } from "./remote.js";
13
+ import { CODEX_AGENT } from "./agents.js";
14
+ import { identity, unwrap, validateTarget, sameTarget } from "./provider.js";
15
+ /** One byte stream / SDK app, multiple SSH attachments. Input is never replayed. */
16
+ export class BrokerTransport {
17
+ readable;
18
+ writable;
19
+ options;
20
+ output;
21
+ attachment;
22
+ cursor = 0;
23
+ epoch = 0;
24
+ attached = false;
25
+ ended = false;
26
+ joining;
27
+ lease = new AbortController();
28
+ listeners = new Set();
29
+ permits = new Map();
30
+ approvalSent = false;
31
+ unconfirmed = new Set();
32
+ failure;
33
+ constructor(options) {
34
+ if (!Number.isSafeInteger(options.maxFrameBytes) || options.maxFrameBytes < 1)
35
+ throw new NeedError('SETTINGS_REQUIRED', '我需要 brokerMaxFrameBytes', '管理员确认 brokerMaxFrameBytes');
36
+ this.options = options;
37
+ this.lease.abort();
38
+ this.readable = new ReadableStream({ start: c => { this.output = c; }, pull: () => { this.attachment?.stdout.resume(); }, cancel: () => { this.close(); } });
39
+ this.writable = new WritableStream({ write: bytes => this.write(bytes), close: () => this.close(), abort: () => this.close() });
40
+ }
41
+ get online() { return this.attached && !this.ended; }
42
+ get generation() { return this.epoch; }
43
+ get signal() { return this.lease.signal; }
44
+ get lastSequence() { return this.cursor; }
45
+ get error() { return this.failure; }
46
+ authorizeResponse(id, generation) { this.permits.set(JSON.stringify(id), generation); }
47
+ /** Called only by the ACP protocol layer after a matching result or current-turn output. */
48
+ acknowledge(method) { this.unconfirmed.delete(method); }
49
+ /** A matching terminal prompt response settles all approval replies in that turn. */
50
+ completePrompt() { this.unconfirmed.delete('session/prompt'); this.approvalSent = false; for (const key of this.unconfirmed)
51
+ if (key.startsWith('response:'))
52
+ this.unconfirmed.delete(key); }
53
+ onChange(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
54
+ changed() { for (const fn of this.listeners)
55
+ fn(); }
56
+ notice(code, message) { this.options.onNeed?.({ code, message, requires: '恢复原员工主机连接或显式结束本轮' }); }
57
+ fail(error) {
58
+ if (this.ended)
59
+ return;
60
+ this.failure = error;
61
+ this.ended = true;
62
+ this.attached = false;
63
+ this.lease.abort();
64
+ this.notice(error.code, error.message);
65
+ this.changed();
66
+ this.attachment?.kill('SIGTERM');
67
+ try {
68
+ this.output.error(error);
69
+ }
70
+ catch { /* Reader already cancelled. */ }
71
+ }
72
+ close() {
73
+ if (this.ended)
74
+ return;
75
+ this.ended = true;
76
+ this.attached = false;
77
+ this.lease.abort();
78
+ this.changed();
79
+ this.attachment?.kill('SIGTERM');
80
+ try {
81
+ this.output.close();
82
+ }
83
+ catch { /* Reader already cancelled. */ }
84
+ }
85
+ async waitOnline(signal) {
86
+ signal?.throwIfAborted();
87
+ if (this.online)
88
+ return;
89
+ if (this.ended)
90
+ throw this.failure ?? new NeedError('SESSION_ENDED', '远端会话已结束', '重新建立会话');
91
+ await new Promise((resolve, reject) => {
92
+ const done = () => { off(); signal?.removeEventListener('abort', abort); };
93
+ const changed = () => { if (this.online) {
94
+ done();
95
+ resolve();
96
+ }
97
+ else if (this.ended) {
98
+ done();
99
+ reject(this.failure ?? new NeedError('SESSION_ENDED', '远端会话已结束', '重新建立会话'));
100
+ } };
101
+ const abort = () => { done(); reject(new NeedError('ABORTED', '本轮已取消', '确认主机状态')); };
102
+ const off = this.onChange(changed);
103
+ signal?.addEventListener('abort', abort, { once: true });
104
+ changed();
105
+ });
106
+ }
107
+ connect() {
108
+ if (this.online)
109
+ return Promise.resolve();
110
+ if (this.joining)
111
+ return this.joining;
112
+ if (this.ended)
113
+ return Promise.reject(this.failure ?? new NeedError('SESSION_ENDED', '远端会话已结束', '重新建立会话'));
114
+ this.joining = this.attach().finally(() => { this.joining = undefined; });
115
+ return this.joining;
116
+ }
117
+ async attach() {
118
+ const child = await this.options.attach(this.cursor);
119
+ if (this.ended) {
120
+ child.kill('SIGTERM');
121
+ throw new NeedError('SESSION_ENDED', '本轮已结束', '重新连接');
122
+ }
123
+ this.attachment = child;
124
+ this.epoch++;
125
+ const generation = this.epoch;
126
+ child.stderr.resume();
127
+ child.stdin.on('error', () => { });
128
+ let partial = Buffer.alloc(0);
129
+ let ready = false;
130
+ const connected = Promise.withResolvers();
131
+ // A local end suppresses detached(), so settle the handshake independently.
132
+ const off = this.onChange(() => { if (this.ended)
133
+ connected.reject(this.failure ?? new NeedError('SESSION_ENDED', '远端会话已结束', '重新建立会话')); });
134
+ const fatal = (error) => { connected.reject(error); this.fail(error); };
135
+ const frame = (f) => {
136
+ if (f.type === 'ready') {
137
+ if (ready)
138
+ throw new NeedError('BROKER_PROTOCOL_INVALID', 'broker 重复 ready', '有效的 broker 帧');
139
+ ready = true;
140
+ this.attached = true;
141
+ this.lease = new AbortController();
142
+ this.changed();
143
+ connected.resolve();
144
+ return;
145
+ }
146
+ if (f.type === 'error')
147
+ throw new NeedError(typeof f.code === 'string' ? f.code : 'BROKER_FAILED', '主机会话无法续接(' + (typeof f.code === 'string' ? f.code : '未知错误') + ');没有重放输入或代答审批', '恢复或显式结束原会话');
148
+ if (f.type === 'exit') {
149
+ connected.reject(new NeedError('AGENT_EXITED', '主机 agent 已退出', '重新建立会话'));
150
+ this.fail(new NeedError('AGENT_EXITED', '主机 agent 已退出,退出状态不代表本轮完成', '确认主机状态'));
151
+ return;
152
+ }
153
+ if (f.type !== 'data' || !Number.isSafeInteger(f.seq) || f.seq < 1 || (f.channel !== 'stdout' && f.channel !== 'stderr') || typeof f.data !== 'string' || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(f.data))
154
+ throw new NeedError('BROKER_PROTOCOL_INVALID', 'broker 输出帧无效', '有效的序号/通道/base64 数据');
155
+ const seq = f.seq;
156
+ if (seq <= this.cursor)
157
+ return;
158
+ if (seq !== this.cursor + 1)
159
+ throw new NeedError('REPLAY_GAP', '主机输出存在缺口,不能安全继续审批', '显式结束或恢复原会话');
160
+ if (f.channel === 'stdout')
161
+ this.output.enqueue(Buffer.from(f.data, 'base64'));
162
+ this.cursor = seq;
163
+ if ((this.output.desiredSize ?? 0) <= 0)
164
+ child.stdout.pause();
165
+ };
166
+ child.stdout.on('data', (bytes) => {
167
+ if (this.ended || generation !== this.epoch)
168
+ return;
169
+ try {
170
+ partial = Buffer.concat([partial, bytes]);
171
+ let newline;
172
+ while ((newline = partial.indexOf(10)) >= 0) {
173
+ if (newline > this.options.maxFrameBytes)
174
+ throw new NeedError('BROKER_FRAME_TOO_LARGE', 'broker 输出超出具名帧大小设置', '管理员调整 brokerMaxFrameBytes');
175
+ const line = partial.subarray(0, newline);
176
+ partial = partial.subarray(newline + 1);
177
+ const parsed = JSON.parse(line.toString('utf8'));
178
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object')
179
+ throw Error('frame');
180
+ frame(parsed);
181
+ if (this.ended)
182
+ return;
183
+ }
184
+ if (partial.length > this.options.maxFrameBytes)
185
+ throw new NeedError('BROKER_FRAME_TOO_LARGE', 'broker 输出超出具名帧大小设置', '管理员调整 brokerMaxFrameBytes');
186
+ }
187
+ catch (error) {
188
+ fatal(error instanceof NeedError ? error : new NeedError('BROKER_PROTOCOL_INVALID', 'broker 输出帧无法解析', '有效的 broker 输出'));
189
+ }
190
+ });
191
+ const detached = () => {
192
+ if (generation !== this.epoch || this.ended)
193
+ return;
194
+ this.attached = false;
195
+ this.lease.abort();
196
+ this.changed();
197
+ if (!ready) {
198
+ fatal(new NeedError('BROKER_ATTACH_FAILED', '主机会话附着失败;根因未查明', '有效的原员工 SSH/broker 连接'));
199
+ return;
200
+ }
201
+ // A matching remote receipt can precede the local stdin write callback.
202
+ // The protocol ledger, rather than that callback's timing, owns delivery.
203
+ if (this.approvalSent || this.unconfirmed.size) {
204
+ fatal(new NeedError('INPUT_DELIVERY_UNCONFIRMED', '输入或审批应答尚无协议收件证明时连接中断;不会重发或再次批准', '显式结束原进程并检查项目状态'));
205
+ return;
206
+ }
207
+ this.notice('CONNECTION_LOST', '连接中断,主机可能正在等待你的审批;重连前不发送审批');
208
+ void this.options.onDisconnect?.().catch(error => this.fail(error instanceof NeedError ? error : new NeedError('RECONNECT_FAILED', '续接失败,根因未查明', '恢复原连接')));
209
+ };
210
+ child.once('error', detached);
211
+ child.once('close', detached);
212
+ return connected.promise.finally(off);
213
+ }
214
+ async write(bytes) {
215
+ if (!this.online || !this.attachment)
216
+ throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '连接不在线,输入没有排队或重放', '恢复连接并确认原会话状态');
217
+ let response;
218
+ try {
219
+ response = JSON.parse(Buffer.from(bytes).toString('utf8'));
220
+ }
221
+ catch {
222
+ throw new NeedError('ACP_INPUT_INVALID', 'ACP 输出不是一条完整 JSON 消息', '有效的 ACP SDK 字节输出');
223
+ }
224
+ const selected = response.result?.outcome?.outcome === 'selected';
225
+ if (selected) {
226
+ const key = JSON.stringify(response.id);
227
+ const permit = this.permits.get(key);
228
+ this.permits.delete(key);
229
+ if (permit !== this.epoch)
230
+ throw new NeedError('APPROVAL_STALE', '连接已改变,旧审批未发送', '重连后的本次审批');
231
+ this.approvalSent = true;
232
+ }
233
+ this.unconfirmed.add(response.method ?? 'response:' + JSON.stringify(response.id));
234
+ const child = this.attachment;
235
+ await new Promise((resolve, reject) => {
236
+ const closed = () => { cleanup(); reject(new NeedError('INPUT_DELIVERY_UNCONFIRMED', '输入送达未确认,未重发', '确认原进程状态')); };
237
+ const cleanup = () => { child.removeListener('close', closed); };
238
+ child.once('close', closed);
239
+ child.stdin.write(bytes, error => { cleanup(); error ? reject(new NeedError('INPUT_DELIVERY_UNCONFIRMED', '输入送达失败,未重发', '确认原进程状态')) : resolve(); });
240
+ });
241
+ }
242
+ }
243
+ export function controlNeed(stderr) {
244
+ let code = 'BROKER_CONTROL_FAILED';
245
+ try {
246
+ const result = JSON.parse(stderr);
247
+ if (typeof result?.code === 'string' && /^[A-Z][A-Z0-9_]{0,63}$/.test(result.code))
248
+ code = result.code;
249
+ }
250
+ catch { /* Only the declared JSON error code is public, never raw stderr. */ }
251
+ return new NeedError(code, code === 'EACCES' ? '我需要管理员让该员工可读、不可写主机会话配置;当前路径访问被拒绝' : `主机会话控制失败(${code});根因${code === 'BROKER_CONTROL_FAILED' ? '未查明' : '见错误代码'},没有重试或新建替代进程`, code === 'EACCES' ? '员工可读且员工不可写的 session.settingsPath' : '恢复原员工 broker 连接并确认会话状态');
252
+ }
253
+ export function sshIo(target, argv, maxControlBytes) {
254
+ const base = argv(target);
255
+ return { attach: async (command) => spawn('ssh', [...base, command], { stdio: ['pipe', 'pipe', 'pipe'] }), run: (command, signal) => new Promise((resolve, reject) => {
256
+ const child = spawn('ssh', ['-n', ...base, command], { stdio: ['ignore', 'pipe', 'pipe'] });
257
+ let bytes = Buffer.alloc(0);
258
+ let diagnostics = Buffer.alloc(0);
259
+ let failure;
260
+ const abort = () => { failure = new NeedError('ABORTED', '主机会话操作已取消,需确认进程状态', '确认主机状态'); child.kill('SIGTERM'); };
261
+ if (signal?.aborted)
262
+ abort();
263
+ else
264
+ signal?.addEventListener('abort', abort, { once: true });
265
+ child.stdout.on('data', (b) => { if (failure)
266
+ return; bytes = Buffer.concat([bytes, b]); if (bytes.length > maxControlBytes) {
267
+ failure = new NeedError('BROKER_CONTROL_TOO_LARGE', 'broker 控制响应超过具名大小设置', '管理员调整 brokerControlMaxBytes');
268
+ child.kill('SIGTERM');
269
+ } });
270
+ child.stderr.on('data', (b) => { if (failure)
271
+ return; diagnostics = Buffer.concat([diagnostics, b]); if (diagnostics.length > maxControlBytes) {
272
+ failure = new NeedError('BROKER_CONTROL_TOO_LARGE', 'broker 错误响应超过具名大小设置', '管理员调整 brokerControlMaxBytes');
273
+ child.kill('SIGTERM');
274
+ } });
275
+ child.on('error', () => { failure = new NeedError('SSH_FAILED', 'SSH 启动失败,根因未查明', '有效的 SSH 执行程序'); });
276
+ child.on('close', code => { signal?.removeEventListener('abort', abort); failure ? reject(failure) : code === 0 ? resolve(bytes.toString('utf8')) : reject(controlNeed(diagnostics.toString('utf8'))); });
277
+ }) };
278
+ }
279
+ export function brokerCommand(t, args) { return `AW_SESSION_ROOT=${shellQuote(t.session.root)} ${shellQuote(t.session.command)} ${args.map(shellQuote).join(' ')}`; }
280
+ export function brokerId(sessionId, agentId = CODEX_AGENT.id) { return (agentId === CODEX_AGENT.id ? 'x' : 'a') + createHash('sha256').update(agentId === CODEX_AGENT.id ? sessionId : agentId + '/' + sessionId).digest('hex').slice(0, 23); }
281
+ function controlJson(text) {
282
+ try {
283
+ return JSON.parse(text);
284
+ }
285
+ catch {
286
+ throw new NeedError('BROKER_PROTOCOL_INVALID', 'broker 控制响应不是有效 JSON,未复制原始输出', '有效的 broker 控制响应');
287
+ }
288
+ }
289
+ export function brokerFactory(get, _ask, configuration, provided) {
290
+ const active = new Set();
291
+ const retained = new Map();
292
+ const open = async (target, project, request) => {
293
+ const settings = request.brokerSettings ?? (typeof configuration === 'function' ? configuration() : configuration);
294
+ if (!request.sessionId)
295
+ throw new NeedError('SESSION_REQUIRED', '我需要当前 DSH 会话标识', 'DSH sessionId');
296
+ if (request.mode === 'one-shot') {
297
+ return { identity: identity(target), cwd: project.path, oneShot: { target, cwd: project.path, ...protectedOneShotLayout(target) } };
298
+ }
299
+ if (![settings.brokerMaxFrameBytes, settings.brokerControlMaxBytes].every(n => Number.isSafeInteger(n) && n > 0))
300
+ throw new NeedError('SETTINGS_REQUIRED', '我需要管理员确认 broker 帧和控制响应大小设置', 'brokerMaxFrameBytes / brokerControlMaxBytes(无默认)');
301
+ let argv = provided?.argv;
302
+ if (!provided?.io && !argv) {
303
+ try {
304
+ const moduleName = '@ai-workbench/connect';
305
+ const supplied = await import(__rewriteRelativeImportExtension(moduleName));
306
+ if (typeof supplied.sshArgs !== 'function')
307
+ throw Error('missing sshArgs');
308
+ argv = supplied.sshArgs;
309
+ }
310
+ catch {
311
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要 connect 0.3.0 包导出的 sshArgs 来执行可信 SSH', '@ai-workbench/connect 0.3.0');
312
+ }
313
+ }
314
+ const ioFor = (t) => provided?.io?.(t) ?? sshIo(t, argv, settings.brokerControlMaxBytes);
315
+ const agent = request.agent ?? CODEX_AGENT;
316
+ let io = ioFor(target);
317
+ const id = brokerId(request.sessionId, agent.id);
318
+ const command = (args) => brokerCommand(target, args);
319
+ const list = controlJson(await io.run(command(['list']), request.signal));
320
+ if (!Array.isArray(list) || list.some(s => typeof s?.id !== 'string'))
321
+ throw new NeedError('BROKER_PROTOCOL_INVALID', '主机会话列表无效', '有效的 broker 列表');
322
+ if (list.some(s => s.id === id)) {
323
+ throw new NeedError('ACP_CLIENT_LOST', '我需要管理员确认并结束遗留的主机会话;当前保留原进程,没有代答旧审批', '管理员核对并明确结束对应主机会话');
324
+ }
325
+ const started = controlJson(await io.run(command(['start', id, '--cwd', project.path, '--settings', target.session.settingsPath, '--', agent.command]), request.signal));
326
+ if (started?.protocolVersion !== 1 || started.id !== id || !Number.isSafeInteger(started.pid) || started.pid < 1 || typeof started.socket !== 'string')
327
+ throw new NeedError('BROKER_START_UNCONFIRMED', 'broker 未返回匹配的启动确认,需确认遗留会话;未自动新建替代进程', `确认主机会话 ${id} 的状态`);
328
+ let transport;
329
+ transport = new BrokerTransport({ maxFrameBytes: settings.brokerMaxFrameBytes, attach: after => io.attach(command(['attach', id, '--after', String(after), '--framed'])), onDisconnect: async () => {
330
+ const connect = get();
331
+ if (!connect)
332
+ throw new NeedError('DEPENDENCY_REQUIRED', '我需要连接插件恢复员工连接', 'aiWorkbenchConnect');
333
+ const next = unwrap(await connect.getSshTarget());
334
+ validateTarget(next);
335
+ if (!sameTarget(next, target))
336
+ throw new NeedError('SESSION_CONTEXT_MISMATCH', '重连目标与原会话不一致', '原员工主机、可信SSH参数和broker');
337
+ io = ioFor(next);
338
+ await transport.connect();
339
+ } });
340
+ const aborted = () => transport.fail(new NeedError('ABORTED', '本轮已取消,正在结束主机进程', '确认主机状态'));
341
+ request.signal?.addEventListener('abort', aborted, { once: true });
342
+ if (request.signal?.aborted)
343
+ aborted();
344
+ const dispose = async () => { request.signal?.removeEventListener('abort', aborted); transport.close(); try {
345
+ await io.run(command(['kill', id]));
346
+ }
347
+ catch {
348
+ throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '无法确认原主机会话已结束;不会自动新建替代进程', '恢复连接并结束对应主机会话');
349
+ } };
350
+ try {
351
+ await transport.connect();
352
+ request.signal?.throwIfAborted();
353
+ if (request.retainProcess)
354
+ request.signal?.removeEventListener('abort', aborted);
355
+ }
356
+ catch (error) {
357
+ await dispose();
358
+ throw error;
359
+ }
360
+ return { identity: identity(target), cwd: project.path, broker: transport, acpTransport: transport, dispose };
361
+ };
362
+ return async (target, project, request) => {
363
+ const key = (request.agent ?? CODEX_AGENT).id + '/' + identity(target) + '/' + request.sessionId;
364
+ const existing = request.retainProcess ? retained.get(key) : undefined;
365
+ if (existing) {
366
+ const settings = request.brokerSettings ?? (typeof configuration === 'function' ? configuration() : configuration);
367
+ if (existing.cwd !== project.path || existing.settings.brokerMaxFrameBytes !== settings.brokerMaxFrameBytes || existing.settings.brokerControlMaxBytes !== settings.brokerControlMaxBytes || !sameTarget(existing.target, target))
368
+ throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话工作区、可信SSH参数或主机运行设置已改变,请新建会话', '原会话的可信主机、工作区与运行设置');
369
+ if (existing.runtime.broker?.error)
370
+ throw existing.runtime.broker.error;
371
+ return existing.runtime;
372
+ }
373
+ if (active.has(key))
374
+ throw new NeedError('TURN_BUSY', '当前 DSH 会话已有远端运行,不能当作遗留进程结束', '等待当前运行结束');
375
+ active.add(key);
376
+ try {
377
+ const runtime = await open(target, project, request);
378
+ const dispose = runtime.dispose;
379
+ let disposed = false;
380
+ runtime.dispose = async () => { if (disposed)
381
+ return; disposed = true; try {
382
+ await dispose?.();
383
+ }
384
+ finally {
385
+ active.delete(key);
386
+ retained.delete(key);
387
+ } };
388
+ if (request.retainProcess && request.mode === 'session')
389
+ retained.set(key, { runtime, cwd: project.path, settings: { ...(request.brokerSettings ?? (typeof configuration === 'function' ? configuration() : configuration)) }, target });
390
+ return runtime;
391
+ }
392
+ catch (error) {
393
+ active.delete(key);
394
+ throw error;
395
+ }
396
+ };
397
+ }
package/dist/client.js ADDED
@@ -0,0 +1,36 @@
1
+ /* Official locale and provider-registration updates only; no custom UI or refresh. */
2
+ window.__ModuleLoader__.load({id:'@ai-workbench/remote-agent',factory(){
3
+ const messages={"en":{"administratorNote":"Administrators: use Settings → Open configuration file to edit the existing plugin entry. See README for the six fields and approved defaults.","providerName":"Codex (Remote Host)"},"zh":{"administratorNote":"管理员:从设置使用“打开配置文件”,修改已有插件条目;六个字段和已批准默认值见 README。","providerName":"Codex(远程主机)"}};
4
+ return{inject:['locale','connection'],apply(ctx){
5
+ ctx.effect(()=>ctx.locale.register('ai-workbench-codex',messages),'Codex official translations');
6
+ ctx.effect(()=>{
7
+ let disposed=false,connected=false,previous,generation=0,request=new AbortController(),queue=Promise.resolve();
8
+ const sync=()=>{
9
+ if(disposed||!connected)return;
10
+ const active=ctx.locale.getSnapshot().active;
11
+ const language=active.split('-')[0].toLowerCase()==='zh'?'zh':'en';
12
+ if(language===previous)return;
13
+ previous=language;
14
+ const mine=generation,signal=request.signal;
15
+ queue=queue.then(async()=>{
16
+ if(disposed||signal.aborted||mine!==generation)return;
17
+ const result=await ctx.connection.rpc.call('/api','awCodexPresentation/setLocale',{args:{language}},signal);
18
+ signal.throwIfAborted();
19
+ if(result?.ok!==true||result.value?.language!==language||result.value?.name!==messages[language].providerName){
20
+ throw Object.assign(new Error('Provider-name locale sync was not confirmed'),{code:result?.ok===false&&typeof result.error?.code==='string'?result.error.code:'LOCALE_SYNC_INVALID'});
21
+ }
22
+ }).catch(error=>{
23
+ if(!disposed&&!signal.aborted&&mine===generation)ctx.logger.warn('Codex provider-name locale update failed',typeof error?.code==='string'?error.code:'LOCALE_SYNC_FAILED');
24
+ });
25
+ };
26
+ const link=()=>{
27
+ const next=ctx.connection.state.getSnapshot()==='connected';
28
+ if(next===connected)return;
29
+ connected=next;generation++;previous=undefined;request.abort();request=new AbortController();
30
+ if(connected)sync();
31
+ };
32
+ const offLocale=ctx.locale.subscribe(sync),offConnection=ctx.connection.state.subscribe(link);link();
33
+ return()=>{disposed=true;offLocale();offConnection();request.abort();};
34
+ },'Codex provider names follow the official locale');
35
+ }};
36
+ }});
@@ -0,0 +1,26 @@
1
+ import type { ConnectTarget } from './provider.ts';
2
+ import type { ContractInfo, Provider, WorkspaceUse, WorkspaceNoticeReceipt as LWorkspaceNoticeReceipt } from '@ai-workbench/connect/api';
3
+ /** 正式L0.7公开类型;消费子集不表示已安装或已通过真实产品门。 */
4
+ export type { WorkspaceNotice, SessionProjectTarget } from '@ai-workbench/connect/api';
5
+ import type { SessionProjectTarget } from '@ai-workbench/connect/api';
6
+ export type LoginProvider = Provider;
7
+ export type WorkspaceNoticeReceipt = Extract<LWorkspaceNoticeReceipt, {
8
+ surface: 'provider';
9
+ }>;
10
+ export type ConnectResult<T> = {
11
+ ok: true;
12
+ value: T;
13
+ } | {
14
+ ok: false;
15
+ error: {
16
+ code: string;
17
+ message: string;
18
+ requires: string;
19
+ };
20
+ };
21
+ export interface NativeWorkspaceApi {
22
+ getContractInfo(): ContractInfo;
23
+ ensureReady(provider: LoginProvider): Promise<ConnectResult<ConnectTarget>>;
24
+ resolveSessionProject(sessionId: string, use: WorkspaceUse): Promise<ConnectResult<SessionProjectTarget>>;
25
+ acknowledgeWorkspaceNotice(receipt: WorkspaceNoticeReceipt): Promise<ConnectResult<void>>;
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
2
+ [key: string]: JsonValue;
3
+ };
4
+ export interface DetailScope {
5
+ instanceId: string;
6
+ epoch: number;
7
+ sessionId: string;
8
+ provider: 'claude' | 'codex';
9
+ turn: number;
10
+ step: number;
11
+ }
12
+ export interface DetailObservation {
13
+ kind: 'extension' | 'commands' | 'tool' | 'usage' | 'notice' | 'need' | 'session';
14
+ severity: 'info' | 'warning' | 'action-required';
15
+ code: string;
16
+ summary: string;
17
+ method?: string;
18
+ payload?: unknown;
19
+ }
20
+ export type SanitizedObservation = Omit<DetailObservation, 'payload'> & {
21
+ payload: JsonValue;
22
+ };
23
+ export interface DetailRecord extends Omit<DetailObservation, 'payload'>, DetailScope {
24
+ version: 1;
25
+ eventId: string;
26
+ sequence: number;
27
+ payload: JsonValue;
28
+ }
29
+ export type DetailSink = {
30
+ append(record: Readonly<DetailRecord>): Promise<void>;
31
+ };
32
+ /** Only objects created by this module are trusted as redaction markers. */
33
+ export declare function isSanitizedRedaction(value: unknown): boolean;
34
+ /** Sanitize an internal observation without attaching any L wire semantics. */
35
+ export declare function sanitizeObservation(observation: DetailObservation, protectModelMaterial?: boolean): Readonly<SanitizedObservation>;
36
+ /** Internal diagnostic recorder; a sink is not an assertion that an L wire API exists. */
37
+ export declare class ConversationDetails {
38
+ private readonly scope;
39
+ private readonly sink;
40
+ private readonly records;
41
+ private tail;
42
+ private firstFailure;
43
+ constructor(scope: DetailScope, sink: DetailSink);
44
+ record(observation: DetailObservation): void;
45
+ flush(): Promise<void>;
46
+ snapshot(): readonly DetailRecord[];
47
+ }