@ai-workbench/remote-agent 0.8.12 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/dist/acp.d.ts +12 -7
- package/dist/acp.js +65 -41
- package/dist/broker-runtime.js +46 -20
- package/dist/broker.d.ts +4 -2
- package/dist/broker.js +18 -9
- package/dist/claude-local.d.ts +2 -1
- package/dist/claude-local.js +61 -34
- package/dist/close-diagnostics.d.ts +56 -0
- package/dist/close-diagnostics.js +104 -0
- package/dist/conversation-details.js +67 -9
- package/dist/employee-endpoint.d.ts +3 -1
- package/dist/employee-endpoint.js +8 -6
- package/dist/employee-exec.d.ts +7 -4
- package/dist/employee-exec.js +39 -29
- package/dist/employee-mcp.d.ts +6 -3
- package/dist/employee-mcp.js +75 -39
- package/dist/employee-provider.js +58 -36
- package/dist/employee-shell.d.ts +2 -0
- package/dist/employee-shell.js +17 -5
- package/dist/employee-ssh.d.ts +2 -0
- package/dist/employee-ssh.js +6 -4
- package/dist/employee-tools.d.ts +3 -1
- package/dist/employee-tools.js +6 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +133 -44
- package/dist/local-user-turn.js +2 -1
- package/dist/model-catalog.d.ts +9 -1
- package/dist/model-catalog.js +63 -5
- package/dist/model-configuration.d.ts +14 -1
- package/dist/model-configuration.js +122 -89
- package/dist/presentation.js +3 -2
- package/dist/provider.d.ts +8 -2
- package/dist/recovery.d.ts +4 -0
- package/dist/rpc-namespace.d.ts +2 -2
- package/dist/rpc-namespace.js +2 -1
- package/dist/session-header.d.ts +4 -0
- package/dist/session-header.js +12 -0
- package/package.json +3 -3
|
@@ -6,6 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { CloseDiagnostics, closeCode, closeErrorCodes } from "./close-diagnostics.js";
|
|
9
10
|
import { createHash } from 'node:crypto';
|
|
10
11
|
import { admitEmployeeLocation } from "./employee-location.js";
|
|
11
12
|
import { EmployeeExecution } from "./employee-exec.js";
|
|
@@ -104,7 +105,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
104
105
|
prior = live.get(sessionId);
|
|
105
106
|
}
|
|
106
107
|
if (prior && (prior.current.api !== api || info?.workspaceLocations !== 1 || info?.localWorkspaceContext !== 1)) {
|
|
107
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
108
|
+
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
108
109
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
109
110
|
}
|
|
110
111
|
if (info?.workspaceLocations !== 1 && info?.localWorkspaceContext !== 1)
|
|
@@ -121,12 +122,12 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
121
122
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
122
123
|
if (row.source !== 'local') {
|
|
123
124
|
if (prior)
|
|
124
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
125
|
+
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
125
126
|
if (request.localAcceptance)
|
|
126
127
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
|
-
let details;
|
|
130
|
+
let details, closeDiagnostics;
|
|
130
131
|
try {
|
|
131
132
|
const acceptance = request.localAcceptance;
|
|
132
133
|
if (!acceptance || acceptance.instanceId !== connection.instanceId || acceptance.epoch !== connection.epoch || acceptance.sessionId !== sessionId || acceptance.provider !== agent.loginProvider || acceptance.workspaceId !== workspace.id || acceptance.bindingId !== row.bindingId)
|
|
@@ -134,6 +135,10 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
134
135
|
details = await options.details?.(request, api, connection, target);
|
|
135
136
|
if (details)
|
|
136
137
|
request.timing?.bind(detail => details.record(detail));
|
|
138
|
+
const sink = details;
|
|
139
|
+
closeDiagnostics = prior?.runtime.closeDiagnostics ?? new CloseDiagnostics(event => sink?.record(event));
|
|
140
|
+
closeDiagnostics.bind(event => sink?.record(event));
|
|
141
|
+
request.closeDiagnostics = closeDiagnostics;
|
|
137
142
|
const unsupported = request.mode !== 'session' || agent.loginProvider !== 'codex' || !!request.recoveryCheckpointId || !!request.resumeSessionId && !prior;
|
|
138
143
|
if (unsupported)
|
|
139
144
|
throw fail('EMPLOYEE_OPERATION_UNSUPPORTED');
|
|
@@ -145,7 +150,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
145
150
|
if (!network || typeof network.prepareEmployeeEndpoint !== 'function' || typeof network.onState !== 'function')
|
|
146
151
|
throw fail('DEPENDENCY_REQUIRED');
|
|
147
152
|
if (prior && prior.network !== network) {
|
|
148
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
153
|
+
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
149
154
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
150
155
|
}
|
|
151
156
|
const admitted = await admitEmployeeLocation({ api: api, network, workspace, connection, expectedLocal: { instanceId: acceptance.instanceId, epoch: acceptance.epoch, workspaceId: acceptance.workspaceId, bindingId: acceptance.bindingId, directory: { ...acceptance.directory } }, settingsPath: target.session.settingsPath, signal: request.signal });
|
|
@@ -167,7 +172,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
167
172
|
native: { path: native.path, sha256: native.sha256 },
|
|
168
173
|
});
|
|
169
174
|
if (prior && (prior.scope !== scope || prior.ended))
|
|
170
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
175
|
+
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
171
176
|
let entry = prior && prior.scope === scope && !prior.ended ? prior : undefined;
|
|
172
177
|
let candidate = entry;
|
|
173
178
|
const base = async (signal) => {
|
|
@@ -177,7 +182,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
177
182
|
if (candidate?.ended)
|
|
178
183
|
throw candidate.reason ?? fail('EMPLOYEE_EXEC_CLOSED');
|
|
179
184
|
await (candidate?.current ?? input).stable(signal);
|
|
180
|
-
await admitted.validate(signal);
|
|
185
|
+
await closeDiagnostics.run('endpoint.prepare', () => admitted.validate(signal));
|
|
181
186
|
if (get() !== api || employee.getNetwork() !== network)
|
|
182
187
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
183
188
|
await (candidate?.current ?? input).stable(signal);
|
|
@@ -193,25 +198,26 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
193
198
|
}
|
|
194
199
|
const io = employee.io ?? sshIo(target, argv, request.acpMaxMessageBytes);
|
|
195
200
|
const scopedIo = { run: (command, signal) => io.run(command, signal), attach: async (command) => {
|
|
201
|
+
const observe = closeDiagnostics.capture();
|
|
196
202
|
const child = await io.attach(command);
|
|
197
|
-
child.once('close', () => { if (candidate && !candidate.ended)
|
|
198
|
-
void candidate.dispose(fail('EMPLOYEE_EXEC_UNKNOWN')).catch(() => { }); });
|
|
203
|
+
child.once('close', (code, signal) => { observe({ layer: 'relay', initiator: 'ssh.process.close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'exit', exitCode: code, signal }); if (candidate && !candidate.ended)
|
|
204
|
+
void candidate.dispose(fail('EMPLOYEE_EXEC_UNKNOWN'), { layer: 'runtime', initiator: 'provider.child-close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'dispose' }).catch(() => { }); });
|
|
199
205
|
return child;
|
|
200
206
|
} };
|
|
201
|
-
const ssh = { target, endpoint: admitted.endpoint, employeeBinary: native.path, binarySha256: native.sha256, acpMaxMessageBytes: request.acpMaxMessageBytes, argv: argv ?? (() => []), validate: base, io: scopedIo };
|
|
207
|
+
const ssh = { closeDiagnostics, target, endpoint: admitted.endpoint, employeeBinary: native.path, binarySha256: native.sha256, acpMaxMessageBytes: request.acpMaxMessageBytes, argv: argv ?? (() => []), validate: base, io: scopedIo };
|
|
202
208
|
const validate = async (signal) => { try {
|
|
203
209
|
await validateEmployeeSshTarget(ssh, signal);
|
|
204
210
|
}
|
|
205
211
|
catch (error) {
|
|
206
212
|
if (candidate && errorCode(error) !== 'ABORTED')
|
|
207
|
-
await candidate.dispose(asNeed(error));
|
|
213
|
+
await candidate.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.validation-failure', reason: closeCode(error), action: 'invalidate' });
|
|
208
214
|
throw error;
|
|
209
215
|
} };
|
|
210
216
|
await validate(request.signal ?? new AbortController().signal);
|
|
211
217
|
if (!options.open)
|
|
212
218
|
throw fail('RUNTIME_REQUIRED');
|
|
213
219
|
// Host cwd is only the protected agent bootstrap/config directory. Native tools use admitted.cwd.
|
|
214
|
-
const host = await options.open(target, { name: 'employee', path: target.projectRoot }, { ...request, agent, employeeScope: scope, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, bindingRevision: state.revision, detailChannel: details, validateContext: async (ignore = false) => {
|
|
220
|
+
const host = await options.open(target, { name: 'employee', path: target.projectRoot }, { ...request, agent, employeeScope: scope, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, bindingRevision: state.revision, closeDiagnostics, detailChannel: details, validateContext: async (ignore = false) => {
|
|
215
221
|
// Disposal addresses only the already-owned host broker instance through its frozen IO/CAS.
|
|
216
222
|
// A withdrawn local source must prevent dispatch, but cannot forbid that instance's exact kill.
|
|
217
223
|
if (ignore)
|
|
@@ -226,49 +232,65 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
226
232
|
await validate(request.signal ?? new AbortController().signal);
|
|
227
233
|
}
|
|
228
234
|
catch (error) {
|
|
229
|
-
await entry.dispose(asNeed(error));
|
|
235
|
+
await entry.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.final-validation', reason: closeCode(error), action: 'dispose' });
|
|
230
236
|
throw error;
|
|
231
237
|
}
|
|
232
238
|
entry.runtime.details = details;
|
|
233
239
|
entry.runtime.connection = { ...connection };
|
|
234
240
|
entry.runtime.binding = { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision };
|
|
241
|
+
entry.runtime.restartNotice = host.restartNotice;
|
|
242
|
+
entry.runtime.restartInputIds = host.restartInputIds;
|
|
243
|
+
entry.runtime.freshSessionConfirmed = host.freshSessionConfirmed;
|
|
235
244
|
return entry.runtime;
|
|
236
245
|
}
|
|
237
246
|
const disposers = [];
|
|
238
|
-
const execution = new EmployeeExecution({ bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
|
|
239
|
-
const runtime = { ...host, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined,
|
|
240
|
-
entry = { scope, runtime, current: input, details, execution, network, ended: false, dispose: reason => {
|
|
247
|
+
const execution = new EmployeeExecution({ closeDiagnostics, bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
|
|
248
|
+
const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes }, employeePresentation: '文件与命令在本机选定目录运行,Git 使用本机仓库;模型、登录和全局助手要求仍在远程主机。本机自定义的附加工具、自动动作、插件及断开后的会话恢复暂不支持。子任务被系统回收后能否继续,尚未验证。' };
|
|
249
|
+
entry = { scope, runtime, current: input, details, execution, network, ended: false, dispose: (reason, cause = { layer: 'runtime', initiator: 'provider.dispose', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', action: 'dispose' }) => {
|
|
250
|
+
closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'provider.ended', before: entry.ended, after: true }] });
|
|
241
251
|
if (entry.closing)
|
|
242
252
|
return entry.closing;
|
|
243
253
|
entry.ended = true;
|
|
244
254
|
entry.reason = reason;
|
|
245
|
-
execution.invalidate(reason ?? fail('EMPLOYEE_EXEC_CLOSED'));
|
|
255
|
+
execution.invalidate(reason ?? fail('EMPLOYEE_EXEC_CLOSED'), { layer: 'executor', initiator: 'provider.invalidate-executor', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', action: 'invalidate' });
|
|
246
256
|
for (const off of disposers)
|
|
247
257
|
off();
|
|
248
258
|
host.broker?.fail(reason ?? fail('EMPLOYEE_EXEC_CLOSED'));
|
|
249
259
|
entry.closing = (async () => {
|
|
250
|
-
|
|
260
|
+
const cleanup = [], reporting = [];
|
|
251
261
|
try {
|
|
252
|
-
await execution.dispose();
|
|
262
|
+
await execution.dispose({ layer: 'executor', initiator: 'provider.cleanup-executor', reason: 'RUNTIME_CLEANUP', action: 'dispose' });
|
|
253
263
|
}
|
|
254
264
|
catch (error) {
|
|
255
|
-
cleanup
|
|
265
|
+
cleanup.push(...closeErrorCodes(error, 'EXECUTOR_CLEANUP_FAILED'));
|
|
256
266
|
}
|
|
257
267
|
try {
|
|
258
|
-
await host.dispose?.();
|
|
268
|
+
await host.dispose?.({ layer: 'broker', initiator: 'provider.cleanup-host', reason: 'RUNTIME_CLEANUP', action: 'dispose' });
|
|
259
269
|
}
|
|
260
270
|
catch (error) {
|
|
261
|
-
cleanup
|
|
271
|
+
cleanup.push(...closeErrorCodes(error, 'HOST_CLEANUP_FAILED'));
|
|
272
|
+
}
|
|
273
|
+
const firstReason = String(closeDiagnostics.first?.reason ?? reason?.code ?? 'EMPLOYEE_EXEC_CLOSED');
|
|
274
|
+
try {
|
|
275
|
+
entry.details?.record({ kind: 'need', severity: 'warning', code: cleanup.length ? 'REMOTE_CLEANUP_UNCONFIRMED' : 'EMPLOYEE_EXEC_UNKNOWN', summary: '员工执行通道已关闭,命令结果 UNKNOWN', payload: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, result: 'UNKNOWN', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', firstReason, cleanup: cleanup.length ? 'unconfirmed' : 'closed', cleanupCodes: cleanup } });
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
reporting.push(closeCode(error, 'CLOSE_RECORD_FAILED'));
|
|
262
279
|
}
|
|
263
280
|
try {
|
|
264
|
-
entry.details?.record({ kind: 'need', severity: 'warning', code: cleanup ? 'REMOTE_CLEANUP_UNCONFIRMED' : 'EMPLOYEE_EXEC_UNKNOWN', summary: '员工执行通道已关闭,命令结果 UNKNOWN', payload: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, result: 'UNKNOWN', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', cleanup: cleanup ? 'unconfirmed' : 'closed' } });
|
|
265
281
|
await entry.details?.flush();
|
|
266
282
|
}
|
|
267
|
-
catch {
|
|
268
|
-
|
|
283
|
+
catch (error) {
|
|
284
|
+
reporting.push(...closeErrorCodes(error, 'CLOSE_FLUSH_FAILED'));
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
closeDiagnostics.assertRecorded();
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
reporting.push(closeDiagnostics.recordingFailureCode ?? closeCode(error, 'CLOSE_RECORD_FAILED'));
|
|
269
291
|
}
|
|
270
|
-
if (cleanup)
|
|
271
|
-
throw
|
|
292
|
+
if (cleanup.length || reporting.length)
|
|
293
|
+
throw Object.assign(new NeedError(reporting.length ? 'CONVERSATION_DETAILS_FAILED' : 'REMOTE_CLEANUP_UNCONFIRMED', '执行连接已关闭;首次原因 ' + firstReason + ',清理或诊断保存未确认。', '首次关闭 ' + firstReason + ';清理 ' + (cleanup.join(',') || 'closed') + ';保存 ' + (reporting.join(',') || 'recorded')), { cause: new AggregateError([firstReason, ...cleanup, ...reporting].map(code => new NeedError(code, '关闭收尾代码 ' + code, '技术详情中的固定代码')), 'EMPLOYEE_CLOSE_AND_DIAGNOSTIC_FAILURES') });
|
|
272
294
|
if (live.get(sessionId) === entry)
|
|
273
295
|
live.delete(sessionId);
|
|
274
296
|
})();
|
|
@@ -279,35 +301,35 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
279
301
|
try {
|
|
280
302
|
const result = await execution.handle(params, signal);
|
|
281
303
|
if (result.closed)
|
|
282
|
-
await entry.dispose();
|
|
304
|
+
await entry.dispose(undefined, { layer: 'runtime', initiator: 'provider.rpc-closed-result', reason: 'EMPLOYEE_EXEC_CLOSED', operation: typeof params === 'object' && params && 'operation' in params && typeof params.operation === 'string' ? params.operation : 'unknown', action: 'dispose' });
|
|
283
305
|
return { ...result };
|
|
284
306
|
}
|
|
285
307
|
catch (error) {
|
|
286
308
|
// EmployeeExecution reserves ABORTED for an unconsumed read wait. Lost responses are UNKNOWN.
|
|
287
309
|
if (!(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
|
|
288
|
-
await entry.dispose(asNeed(error));
|
|
310
|
+
await entry.dispose(asNeed(error), { layer: 'runtime', initiator: 'provider.rpc-failure', reason: closeCode(error), action: 'dispose' });
|
|
289
311
|
throw error;
|
|
290
312
|
}
|
|
291
313
|
};
|
|
292
|
-
runtime.dispose =
|
|
293
|
-
runtime.detach = runtime.
|
|
294
|
-
const revoke = (reason) => { void entry.dispose(reason).catch(() => { }); };
|
|
314
|
+
runtime.dispose = cause => entry.dispose(undefined, cause ?? { layer: 'runtime', initiator: 'runtime.dispose', reason: 'RUNTIME_DISPOSE', action: 'dispose' });
|
|
315
|
+
runtime.detach = cause => entry.dispose(undefined, cause ?? { layer: 'runtime', initiator: 'runtime.detach', reason: 'RUNTIME_DETACH', action: 'detach' });
|
|
316
|
+
const revoke = (reason, initiator, changes) => { void entry.dispose(reason, { layer: 'endpoint', initiator, reason: reason.code, action: 'invalidate', changes }).catch(() => { }); };
|
|
295
317
|
disposers.push(api.onWorkspaceLocationState(next => {
|
|
296
318
|
const matches = next.items.filter(item => item.workspaceId === admitted.workspaceId);
|
|
297
319
|
if (get() !== api || next.phase !== 'ready' || next.instanceId !== connection.instanceId || next.epoch !== connection.epoch || matches.length !== 1 || matches[0].bindingId !== admitted.bindingId || matches[0].source !== 'local' || matches[0].status !== 'selected')
|
|
298
|
-
revoke(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
320
|
+
revoke(fail('EMPLOYEE_CONTEXT_CHANGED'), 'endpoint.workspace-change', [{ field: 'workspace.binding-id', before: admitted.bindingId, after: matches[0]?.bindingId ?? null }, { field: 'workspace.source', before: 'local', after: matches[0]?.source ?? null }, { field: 'workspace.status', before: 'selected', after: matches[0]?.status ?? null }, { field: 'workspace.match-count', before: 1, after: matches.length }]);
|
|
299
321
|
}), api.onConnectionState(next => { if (get() !== api || next.phase !== 'connected' || next.instanceId !== connection.instanceId || next.epoch !== connection.epoch)
|
|
300
|
-
revoke(fail('CONNECTION_LOST')); }), network.onState(event => { if (event.kind === 'lost' || employee.getNetwork() !== network)
|
|
301
|
-
revoke(fail('CONNECTION_LOST')); }));
|
|
322
|
+
revoke(fail('CONNECTION_LOST'), 'endpoint.connection-change', [{ field: 'connection.instance-id', before: connection.instanceId, after: next.instanceId }, { field: 'connection.epoch', before: connection.epoch, after: next.epoch }, { field: 'connection.phase', before: 'connected', after: next.phase }]); }), network.onState(event => { if (event.kind === 'lost' || employee.getNetwork() !== network)
|
|
323
|
+
revoke(fail('CONNECTION_LOST'), 'endpoint.network-loss', [{ field: 'network.phase', before: null, after: event.state.phase }]); }));
|
|
302
324
|
if (host.broker)
|
|
303
325
|
disposers.push(host.broker.onChange(() => { if (!host.broker.online)
|
|
304
|
-
revoke(fail('EMPLOYEE_EXEC_UNKNOWN')); }));
|
|
326
|
+
revoke(fail('EMPLOYEE_EXEC_UNKNOWN'), 'endpoint.broker-offline'); }));
|
|
305
327
|
live.set(sessionId, entry);
|
|
306
328
|
try {
|
|
307
329
|
await validate(request.signal ?? new AbortController().signal);
|
|
308
330
|
}
|
|
309
331
|
catch (error) {
|
|
310
|
-
await entry.dispose(asNeed(error));
|
|
332
|
+
await entry.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.final-validation', reason: closeCode(error), action: 'dispose' });
|
|
311
333
|
throw error;
|
|
312
334
|
}
|
|
313
335
|
return runtime;
|
package/dist/employee-shell.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CloseDiagnostics } from './close-diagnostics.ts';
|
|
1
2
|
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
3
|
import type { EmployeeEndpointTarget } from './employee-endpoint.ts';
|
|
3
4
|
export interface EmployeeShellResult {
|
|
@@ -13,6 +14,7 @@ export type EmployeeShell = (request: {
|
|
|
13
14
|
signal: AbortSignal;
|
|
14
15
|
}) => Promise<EmployeeShellResult>;
|
|
15
16
|
interface Options {
|
|
17
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
16
18
|
target: EmployeeEndpointTarget;
|
|
17
19
|
maxBytes: number;
|
|
18
20
|
killGraceMs: number;
|
package/dist/employee-shell.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeCode } from "./close-diagnostics.js";
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { EMPLOYEE_TARGET_PYTHON, EMPLOYEE_PYTHON_ERRORS } from "./employee-relay.js";
|
|
3
4
|
import { NeedError } from "./remote.js";
|
|
@@ -15,11 +16,13 @@ const unknown = (code) => new NeedError(code, '本机命令结果未知;没有
|
|
|
15
16
|
export function createEmployeeShell(options) {
|
|
16
17
|
return async ({ command, timeout, signal }) => {
|
|
17
18
|
signal.throwIfAborted();
|
|
19
|
+
const diagnostics = options.closeDiagnostics?.child();
|
|
18
20
|
const executionId = randomUUID(), target = options.target;
|
|
19
21
|
const child = await options.attach({ command: '/usr/bin/python3', args: ['-c', SHELL_PYTHON, JSON.stringify({ endpoint: target.endpoint, bridge: target.bridge, directory: target.local.directory, maxBytes: options.maxBytes, executionId })] });
|
|
20
22
|
return new Promise((yes, no) => {
|
|
21
23
|
let settled = false, bytes = 0, stdout = [], stderr = [], reason, grace, timer;
|
|
22
24
|
const finish = (error, code) => {
|
|
25
|
+
diagnostics?.record({ layer: 'executor', initiator: 'shell.finish', reason: closeCode(error, 'SHELL_FINISHED'), action: 'close', exitCode: code ?? null, changes: [{ field: 'shell.settled', before: settled, after: true }] });
|
|
23
26
|
if (settled)
|
|
24
27
|
return;
|
|
25
28
|
settled = true;
|
|
@@ -62,11 +65,20 @@ export function createEmployeeShell(options) {
|
|
|
62
65
|
no(unknown('EMPLOYEE_EXIT_UNKNOWN'));
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
68
|
+
try {
|
|
69
|
+
diagnostics?.assertRecorded();
|
|
70
|
+
}
|
|
71
|
+
catch (failure) {
|
|
72
|
+
no(failure);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
65
75
|
yes({ stdout: out, stderr: err.slice(header.length, start) + err.slice(start + started.length), stderrSource: 'combined-ssh-and-command', exitCode: code, signal: null });
|
|
66
76
|
};
|
|
67
|
-
const stop = (code) => { if (settled || reason)
|
|
68
|
-
return; reason = unknown(code); child.kill('SIGTERM'); grace = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
|
|
69
|
-
|
|
77
|
+
const stop = (code) => { diagnostics?.record({ layer: 'executor', initiator: 'shell.stop', reason: code, action: 'terminate', changes: [{ field: 'shell.stop-code', before: reason?.code ?? null, after: settled || reason ? reason?.code ?? null : code }] }); if (settled || reason)
|
|
78
|
+
return; reason = unknown(code); child.kill('SIGTERM'); grace = setTimeout(() => { if (child.exitCode === null && child.signalCode === null) {
|
|
79
|
+
diagnostics?.record({ layer: 'executor', initiator: 'shell.kill-grace', reason: reason?.code ?? 'SHELL_CLEANUP', action: 'terminate', signal: 'SIGKILL' });
|
|
80
|
+
child.kill('SIGKILL');
|
|
81
|
+
} finish(reason); }, options.killGraceMs); };
|
|
70
82
|
const abort = () => stop('EMPLOYEE_CANCELLED_UNKNOWN');
|
|
71
83
|
const collect = (destination, chunk) => { bytes += chunk.length; if (bytes > options.maxBytes) {
|
|
72
84
|
stop('EMPLOYEE_OUTPUT_TOO_LARGE_UNKNOWN');
|
|
@@ -75,8 +87,8 @@ export function createEmployeeShell(options) {
|
|
|
75
87
|
child.stdout.on('data', (b) => collect(stdout, b));
|
|
76
88
|
child.stderr.on('data', (b) => collect(stderr, b));
|
|
77
89
|
child.stdin.on('error', () => stop('EMPLOYEE_INPUT_UNKNOWN'));
|
|
78
|
-
child.once('error', () => { finish(unknown('EMPLOYEE_TRANSPORT_UNKNOWN')); });
|
|
79
|
-
child.once('close', code => finish(reason, code));
|
|
90
|
+
child.once('error', () => { diagnostics?.record({ layer: 'relay', initiator: 'shell.process-error', reason: 'EMPLOYEE_TRANSPORT_UNKNOWN', action: 'failure' }); finish(unknown('EMPLOYEE_TRANSPORT_UNKNOWN')); });
|
|
91
|
+
child.once('close', (code, exitSignal) => { diagnostics?.record({ layer: 'relay', initiator: 'shell.process-close', reason: reason?.code ?? 'PROCESS_EXIT', action: 'exit', exitCode: code, signal: exitSignal }); finish(reason, code); });
|
|
80
92
|
signal.addEventListener('abort', abort, { once: true });
|
|
81
93
|
if (signal.aborted)
|
|
82
94
|
abort();
|
package/dist/employee-ssh.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CloseDiagnostics } from './close-diagnostics.ts';
|
|
1
2
|
import type { EmployeeEndpointTarget } from '@ai-workbench/network/api';
|
|
2
3
|
import { type BrokerIo, type SshArgv } from './broker.ts';
|
|
3
4
|
import type { ConnectTarget } from './provider.ts';
|
|
@@ -5,6 +6,7 @@ import { type EmployeeExecutionTransport } from './employee-exec.ts';
|
|
|
5
6
|
export interface EmployeeSshOptions {
|
|
6
7
|
target: ConnectTarget;
|
|
7
8
|
endpoint: EmployeeEndpointTarget;
|
|
9
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
8
10
|
/** Automatically discovered package binary path on this same employee device. */
|
|
9
11
|
employeeBinary: string;
|
|
10
12
|
binarySha256: string;
|
package/dist/employee-ssh.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeCode } from "./close-diagnostics.js";
|
|
1
2
|
import { sshIo } from "./broker.js";
|
|
2
3
|
import { createEmployeeProcessTransport } from "./employee-exec.js";
|
|
3
4
|
import { NeedError, shellQuote } from "./remote.js";
|
|
@@ -92,7 +93,8 @@ async function currentTarget(options, signal) {
|
|
|
92
93
|
await gate();
|
|
93
94
|
const b = endpoint.bridge, e = endpoint.endpoint;
|
|
94
95
|
const command = [b.command, 'target', '--settings', b.settingsPath, '--device', e.deviceId, '--if-instance', b.bridgeInstanceId, '--if-generation', String(b.generation), '--if-endpoint', e.endpointInstanceId, '--if-context', e.executionContextId].map(shellQuote).join(' ');
|
|
95
|
-
const
|
|
96
|
+
const run = () => awaitSignal(io.run(command, signal), signal);
|
|
97
|
+
const raw = await (options.closeDiagnostics ? options.closeDiagnostics.run('endpoint.target', run) : run());
|
|
96
98
|
const args = readTarget(raw, endpoint, options.acpMaxMessageBytes);
|
|
97
99
|
await gate();
|
|
98
100
|
return { io, args, gate };
|
|
@@ -107,9 +109,9 @@ export async function createEmployeeSshTransport(options, signal = new AbortCont
|
|
|
107
109
|
checkSignal(signal);
|
|
108
110
|
// EmployeeExecution races the user's open wait; this factory retains any late child cleanup.
|
|
109
111
|
const child = await io.attach('exec ' + ['/usr/bin/ssh', ...args, remote].map(shellQuote).join(' '));
|
|
110
|
-
const transport = createEmployeeProcessTransport(child, options.acpMaxMessageBytes, ['AW_EMPLOYEE_BINARY_UNAVAILABLE', 'AW_EMPLOYEE_BINARY_HASH_FAILED', 'AW_EMPLOYEE_BINARY_HASH_MISMATCH', 'AW_EMPLOYEE_BINARY_VERSION_FAILED', 'AW_EMPLOYEE_BINARY_VERSION_MISMATCH']);
|
|
112
|
+
const transport = createEmployeeProcessTransport(child, options.acpMaxMessageBytes, ['AW_EMPLOYEE_BINARY_UNAVAILABLE', 'AW_EMPLOYEE_BINARY_HASH_FAILED', 'AW_EMPLOYEE_BINARY_HASH_MISMATCH', 'AW_EMPLOYEE_BINARY_VERSION_FAILED', 'AW_EMPLOYEE_BINARY_VERSION_MISMATCH'], options.closeDiagnostics?.capture());
|
|
111
113
|
if (signal.aborted) {
|
|
112
|
-
await transport.close();
|
|
114
|
+
await transport.close({ layer: 'endpoint', initiator: 'ssh.open-abort', reason: 'ABORTED', operation: 'open', action: 'abort' });
|
|
113
115
|
throw fail('ABORTED');
|
|
114
116
|
}
|
|
115
117
|
try {
|
|
@@ -117,7 +119,7 @@ export async function createEmployeeSshTransport(options, signal = new AbortCont
|
|
|
117
119
|
return transport;
|
|
118
120
|
}
|
|
119
121
|
catch (error) {
|
|
120
|
-
await transport.close();
|
|
122
|
+
await transport.close({ layer: 'endpoint', initiator: 'ssh.post-open-validation', reason: closeCode(error), operation: 'open', action: 'failure' });
|
|
121
123
|
throw error;
|
|
122
124
|
}
|
|
123
125
|
}
|
package/dist/employee-tools.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type CloseCause, type CloseDiagnostics } from './close-diagnostics.ts';
|
|
1
2
|
import type { DetailObservation } from './conversation-details.ts';
|
|
2
3
|
import type { EmployeeShell } from './employee-shell.ts';
|
|
3
4
|
export declare const EMPLOYEE_TOOL_NAMES: readonly ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
|
|
4
5
|
export type EmployeeToolName = typeof EMPLOYEE_TOOL_NAMES[number];
|
|
5
6
|
interface Options {
|
|
7
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
6
8
|
cwd: string;
|
|
7
9
|
shell: string;
|
|
8
10
|
uid: number;
|
|
@@ -112,6 +114,6 @@ export declare function createEmployeeTools(options: Options): {
|
|
|
112
114
|
} | undefined;
|
|
113
115
|
isError?: boolean | undefined;
|
|
114
116
|
}>;
|
|
115
|
-
close(): Promise<void>;
|
|
117
|
+
close(cause?: CloseCause): Promise<void>;
|
|
116
118
|
};
|
|
117
119
|
export {};
|
package/dist/employee-tools.js
CHANGED
|
@@ -94,13 +94,14 @@ export function createEmployeeTools(options) {
|
|
|
94
94
|
throw failure('EMPLOYEE_OUTPUT_TOO_LARGE'); };
|
|
95
95
|
async function run(command, args, signal, detail, timeout) {
|
|
96
96
|
signal.throwIfAborted();
|
|
97
|
+
const diagnostics = options.closeDiagnostics?.child(detail);
|
|
97
98
|
const operation = new Promise((yes, no) => {
|
|
98
99
|
const child = spawn(command, args, { cwd: options.cwd, env: process.env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
99
100
|
child.stdin.end();
|
|
100
101
|
let bytes = 0, stdout = [], stderr = [], failed, killTimer, timeoutTimer;
|
|
101
|
-
const kill = (which) => { if (child.exitCode === null && child.signalCode === null)
|
|
102
|
+
const kill = (which) => { diagnostics?.record({ layer: 'executor', initiator: 'tools.process-kill', reason: failed?.code ?? 'PROCESS_CLEANUP', action: 'terminate', signal: which }); if (child.exitCode === null && child.signalCode === null)
|
|
102
103
|
child.kill(which); };
|
|
103
|
-
const stop = (code) => { if (failed)
|
|
104
|
+
const stop = (code) => { diagnostics?.record({ layer: 'executor', initiator: 'tools.process-stop', reason: code, action: 'terminate', changes: [{ field: 'tools.stop-code', before: failed?.code ?? null, after: failed?.code ?? code }] }); if (failed)
|
|
104
105
|
return; failed = failure(code); kill('SIGTERM'); killTimer = setTimeout(() => kill('SIGKILL'), options.killGraceMs); killTimer.unref(); };
|
|
105
106
|
const aborted = () => stop('EMPLOYEE_CANCELLED_UNKNOWN');
|
|
106
107
|
const collect = (dest, chunk) => { bytes += chunk.length; if (bytes > options.maxBytes) {
|
|
@@ -116,8 +117,8 @@ export function createEmployeeTools(options) {
|
|
|
116
117
|
stop('CONVERSATION_DETAILS_FAILED');
|
|
117
118
|
} if (signal.aborted)
|
|
118
119
|
aborted(); });
|
|
119
|
-
child.on('error', e => { failed ??= failure(e.code === 'ENOENT' ? 'EMPLOYEE_COMMAND_UNAVAILABLE' : 'EMPLOYEE_PROCESS_FAILED'); });
|
|
120
|
-
child.on('close', (exitCode, exitSignal) => { signal.removeEventListener('abort', aborted); if (killTimer)
|
|
120
|
+
child.on('error', e => { diagnostics?.record({ layer: 'executor', initiator: 'tools.process-error', reason: e.code === 'ENOENT' ? 'EMPLOYEE_COMMAND_UNAVAILABLE' : 'EMPLOYEE_PROCESS_FAILED', action: 'failure' }); failed ??= failure(e.code === 'ENOENT' ? 'EMPLOYEE_COMMAND_UNAVAILABLE' : 'EMPLOYEE_PROCESS_FAILED'); });
|
|
121
|
+
child.on('close', (exitCode, exitSignal) => { diagnostics?.record({ layer: 'executor', initiator: 'tools.process-close', reason: failed?.code ?? 'PROCESS_EXIT', action: 'exit', exitCode, signal: exitSignal }); signal.removeEventListener('abort', aborted); if (killTimer)
|
|
121
122
|
clearTimeout(killTimer); if (timeoutTimer)
|
|
122
123
|
clearTimeout(timeoutTimer); failed ? no(failed) : yes({ stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'), exitCode, signal: exitSignal }); });
|
|
123
124
|
signal.addEventListener('abort', aborted, { once: true });
|
|
@@ -287,6 +288,6 @@ export function createEmployeeTools(options) {
|
|
|
287
288
|
}
|
|
288
289
|
return {
|
|
289
290
|
call(name, input, requestSignal, observer) { const operation = call(name, input, requestSignal, observer); calls.add(operation); void operation.then(() => calls.delete(operation), () => calls.delete(operation)); return operation; },
|
|
290
|
-
async close() { lifetime.abort(); await Promise.allSettled([...calls, ...children]); },
|
|
291
|
+
async close(cause = { layer: 'executor', initiator: 'tools.close', reason: 'EMPLOYEE_TOOLS_CLOSED', action: 'close' }) { options.closeDiagnostics?.record({ ...cause, changes: cause.changes ?? [{ field: 'tools.aborted', before: lifetime.signal.aborted, after: true }] }); lifetime.abort(); await Promise.allSettled([...calls, ...children]); },
|
|
291
292
|
};
|
|
292
293
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CloseChange } from './close-diagnostics.ts';
|
|
1
2
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
3
|
import Schema from '@deepseek-ai/schemastery';
|
|
3
4
|
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
@@ -74,6 +75,7 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
74
75
|
private shuttingDown;
|
|
75
76
|
private detached;
|
|
76
77
|
private cleanups;
|
|
78
|
+
private preparationFailures;
|
|
77
79
|
constructor(config: PluginConfig | (() => PluginConfig), resolveRuntime: RuntimeProvider, approve?: InvocationApproval | undefined, providerName?: () => string, agent?: AgentDefinition, readImage?: ImageReader | undefined, reportLoginFailure?: LoginFailureReporter | undefined, directory?: ModelDirectory | undefined, preflightDiagnostics?: PreflightDiagnostics | undefined);
|
|
78
80
|
providerInfo(provider: string): {
|
|
79
81
|
id: string;
|
|
@@ -86,6 +88,7 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
86
88
|
resolveModel(provider: string, id: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
|
|
87
89
|
catalog: HostModel;
|
|
88
90
|
}>;
|
|
91
|
+
observeModelInvalidation(fields: readonly string[], changes?: readonly CloseChange[]): void;
|
|
89
92
|
invalidateModelState(): Promise<void>;
|
|
90
93
|
invalidateEmployeeState(): Promise<void>;
|
|
91
94
|
providerRetryPolicy(): {
|
|
@@ -118,6 +121,7 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
118
121
|
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
|
|
119
122
|
catalog: HostModel;
|
|
120
123
|
}>;
|
|
124
|
+
observeModelInvalidation(fields: readonly string[], changes?: readonly CloseChange[]): void;
|
|
121
125
|
invalidateModelState(): Promise<void>;
|
|
122
126
|
invalidateEmployeeState(): Promise<void>;
|
|
123
127
|
providerRetryPolicy(): {
|