@ai-workbench/remote-agent 0.8.13 → 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 +8 -6
- 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/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 +2 -1
- 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/package.json +1 -1
package/dist/broker.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type CloseCause, type CloseObserver } from './close-diagnostics.ts';
|
|
1
2
|
import { type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
3
|
import { NeedError, type Need } from './remote.ts';
|
|
3
4
|
import { type ConnectTarget } from './provider.ts';
|
|
4
5
|
export type Attachment = ChildProcessWithoutNullStreams;
|
|
5
6
|
export interface BrokerOptions {
|
|
7
|
+
observeClose?: CloseObserver;
|
|
6
8
|
maxFrameBytes: number;
|
|
7
9
|
initialSequence?: number;
|
|
8
10
|
instanceId?: string;
|
|
@@ -64,8 +66,8 @@ export declare class BrokerTransport {
|
|
|
64
66
|
onChange(fn: () => void): () => void;
|
|
65
67
|
private changed;
|
|
66
68
|
private notice;
|
|
67
|
-
fail(error: NeedError): void;
|
|
68
|
-
close(): void;
|
|
69
|
+
fail(error: NeedError, cause?: CloseCause): void;
|
|
70
|
+
close(cause?: CloseCause): void;
|
|
69
71
|
waitOnline(signal?: AbortSignal): Promise<void>;
|
|
70
72
|
connect(): Promise<void>;
|
|
71
73
|
private attach;
|
package/dist/broker.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeCode } from "./close-diagnostics.js";
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { ProtocolBoundary } from "./protocol-boundary.js";
|
|
@@ -39,8 +40,8 @@ export class BrokerTransport {
|
|
|
39
40
|
this.boundary = new ProtocolBoundary(options.maxMessageBytes);
|
|
40
41
|
this.recovering = !!options.recoverySessionId;
|
|
41
42
|
this.lease.abort();
|
|
42
|
-
this.readable = new ReadableStream({ start: c => { this.output = c; }, pull: () => { this.attachment?.stdout.resume(); }, cancel:
|
|
43
|
-
this.writable = new WritableStream({ write: bytes => this.write(bytes), close: () => this.close(), abort:
|
|
43
|
+
this.readable = new ReadableStream({ start: c => { this.output = c; }, pull: () => { this.attachment?.stdout.resume(); }, cancel: reason => { this.close({ layer: 'broker', initiator: 'broker.reader-cancel', reason: closeCode(reason, 'BROKER_READER_CANCELLED'), action: 'close' }); } });
|
|
44
|
+
this.writable = new WritableStream({ write: bytes => this.write(bytes), close: () => this.close({ layer: 'broker', initiator: 'broker.writer-close', reason: 'BROKER_WRITER_CLOSED', action: 'close' }), abort: reason => this.close({ layer: 'broker', initiator: 'broker.writer-abort', reason: closeCode(reason, 'BROKER_WRITER_ABORTED'), action: 'abort' }) });
|
|
44
45
|
}
|
|
45
46
|
get online() { return this.attached && !this.ended; }
|
|
46
47
|
get generation() { return this.epoch; }
|
|
@@ -93,7 +94,8 @@ export class BrokerTransport {
|
|
|
93
94
|
changed() { for (const fn of this.listeners)
|
|
94
95
|
fn(); }
|
|
95
96
|
notice(code, message) { this.options.onNeed?.({ code, message, requires: '恢复原员工主机连接或显式结束本轮' }); }
|
|
96
|
-
fail(error) {
|
|
97
|
+
fail(error, cause = { layer: 'broker', initiator: 'broker.fail', reason: error.code, action: 'failure' }) {
|
|
98
|
+
this.options.observeClose?.({ ...cause, changes: cause.changes ?? [{ field: 'broker.ended', before: this.ended, after: true }] });
|
|
97
99
|
if (this.ended)
|
|
98
100
|
return;
|
|
99
101
|
this.stopHeartbeat();
|
|
@@ -109,7 +111,8 @@ export class BrokerTransport {
|
|
|
109
111
|
}
|
|
110
112
|
catch { /* Reader already cancelled. */ }
|
|
111
113
|
}
|
|
112
|
-
close() {
|
|
114
|
+
close(cause = { layer: 'broker', initiator: 'broker.close', reason: 'BROKER_CLOSE_REQUESTED', action: 'close' }) {
|
|
115
|
+
this.options.observeClose?.({ ...cause, changes: cause.changes ?? [{ field: 'broker.ended', before: this.ended, after: true }] });
|
|
113
116
|
if (this.ended)
|
|
114
117
|
return;
|
|
115
118
|
this.stopHeartbeat();
|
|
@@ -158,6 +161,7 @@ export class BrokerTransport {
|
|
|
158
161
|
async attach() {
|
|
159
162
|
const child = await this.options.attach(this.cursor);
|
|
160
163
|
if (this.ended) {
|
|
164
|
+
this.options.observeClose?.({ layer: 'broker', initiator: 'broker.late-attachment', reason: 'SESSION_ENDED', action: 'terminate' });
|
|
161
165
|
child.kill('SIGTERM');
|
|
162
166
|
throw new NeedError('SESSION_ENDED', '本轮已结束', '重新连接');
|
|
163
167
|
}
|
|
@@ -189,8 +193,10 @@ export class BrokerTransport {
|
|
|
189
193
|
if (policy.enabled) {
|
|
190
194
|
this.stopHeartbeat();
|
|
191
195
|
this.heartbeat = setInterval(() => { if (this.online && this.attachment === child) {
|
|
192
|
-
child.stdin.write('{"type":"heartbeat"}\n', error => { if (error)
|
|
193
|
-
|
|
196
|
+
child.stdin.write('{"type":"heartbeat"}\n', error => { if (error) {
|
|
197
|
+
this.options.observeClose?.({ layer: 'broker', initiator: 'broker.heartbeat-write', reason: 'BROKER_HEARTBEAT_WRITE_FAILED', action: 'terminate' });
|
|
198
|
+
child.kill('SIGTERM');
|
|
199
|
+
} });
|
|
194
200
|
} }, policy.heartbeatIntervalMs);
|
|
195
201
|
this.heartbeat.unref?.();
|
|
196
202
|
}
|
|
@@ -206,6 +212,7 @@ export class BrokerTransport {
|
|
|
206
212
|
const code = typeof f.code === 'string' && /^[A-Z][A-Z0-9_]{0,63}$/.test(f.code) ? f.code : 'BROKER_PROTOCOL_INVALID';
|
|
207
213
|
const error = new NeedError(code, '主机会话无法续接(' + code + ');没有重放输入或代答审批', '恢复或显式结束原会话');
|
|
208
214
|
if (!ready && f.code === 'ATTACH_BUSY') {
|
|
215
|
+
this.options.observeClose?.({ layer: 'broker', initiator: 'broker.attach-busy', reason: error.code, action: 'terminate' });
|
|
209
216
|
attemptClosed = true;
|
|
210
217
|
connected.reject(error);
|
|
211
218
|
child.kill('SIGTERM');
|
|
@@ -261,7 +268,9 @@ export class BrokerTransport {
|
|
|
261
268
|
fatal(error instanceof NeedError ? error : new NeedError('BROKER_PROTOCOL_INVALID', 'broker 输出帧无法解析', '有效的 broker 输出'));
|
|
262
269
|
}
|
|
263
270
|
});
|
|
264
|
-
const detached = () => {
|
|
271
|
+
const detached = (cause) => {
|
|
272
|
+
const effective = !(attemptClosed || generation !== this.epoch || this.ended);
|
|
273
|
+
this.options.observeClose?.({ ...cause, changes: cause.changes ?? [{ field: 'broker.attached', before: this.attached, after: effective ? false : this.attached }, { field: 'broker.lease-aborted', before: this.lease.signal.aborted, after: effective ? true : this.lease.signal.aborted }] });
|
|
265
274
|
if (attemptClosed || generation !== this.epoch || this.ended)
|
|
266
275
|
return;
|
|
267
276
|
this.stopHeartbeat();
|
|
@@ -281,8 +290,8 @@ export class BrokerTransport {
|
|
|
281
290
|
this.notice('CONNECTION_LOST', '连接中断,主机可能正在等待你的审批;重连前不发送审批');
|
|
282
291
|
void this.options.onDisconnect?.().catch(error => this.fail(error instanceof NeedError ? error : new NeedError('RECONNECT_FAILED', '续接失败,根因未查明', '恢复原连接')));
|
|
283
292
|
};
|
|
284
|
-
child.once('error', detached);
|
|
285
|
-
child.once('close', detached);
|
|
293
|
+
child.once('error', () => detached({ layer: 'broker', initiator: 'broker.attachment-error', reason: 'BROKER_ATTACHMENT_ERROR', action: 'failure' }));
|
|
294
|
+
child.once('close', (code, signal) => detached({ layer: 'broker', initiator: 'broker.attachment-close', reason: 'BROKER_ATTACHMENT_CLOSED', action: 'exit', exitCode: code, signal }));
|
|
286
295
|
return connected.promise.finally(off);
|
|
287
296
|
}
|
|
288
297
|
async write(bytes) {
|
package/dist/claude-local.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CloseCause } from './close-diagnostics.ts';
|
|
1
2
|
import { type EmployeeLease, type EmployeeLeaseOptions, type EmployeeNetworkApi, type EmployeeWorkspace } from './employee-endpoint.ts';
|
|
2
3
|
import { type EmployeeMcpOptions } from './employee-mcp.ts';
|
|
3
4
|
import { createEmployeeShell, type EmployeeShell } from './employee-shell.ts';
|
|
@@ -27,7 +28,7 @@ interface Options {
|
|
|
27
28
|
mcp: (options: EmployeeMcpOptions) => Promise<{
|
|
28
29
|
routing: EmployeeRouting;
|
|
29
30
|
signal?: AbortSignal;
|
|
30
|
-
close(): Promise<void>;
|
|
31
|
+
close(cause?: CloseCause): Promise<void>;
|
|
31
32
|
}>;
|
|
32
33
|
shell: (options: Parameters<typeof createEmployeeShell>[0]) => EmployeeShell;
|
|
33
34
|
};
|
package/dist/claude-local.js
CHANGED
|
@@ -6,6 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { CloseDiagnostics, closeCode } from "./close-diagnostics.js";
|
|
9
10
|
import { isDeepStrictEqual } from 'node:util';
|
|
10
11
|
import { openEmployeeLease } from "./employee-endpoint.js";
|
|
11
12
|
import { openEmployeeMcp } from "./employee-mcp.js";
|
|
@@ -94,6 +95,9 @@ export function createClaudeLocalProvider(options) {
|
|
|
94
95
|
if (entry && (entry.closing || !isDeepStrictEqual(entry.scope, scope) || entry.lease.signal.aborted))
|
|
95
96
|
throw failure('EMPLOYEE_CONTEXT_CHANGED');
|
|
96
97
|
const details = request.detailChannel;
|
|
98
|
+
const closeDiagnostics = entry?.runtime.closeDiagnostics ?? new CloseDiagnostics(event => details.record(event));
|
|
99
|
+
closeDiagnostics.bind(event => details.record(event));
|
|
100
|
+
request.closeDiagnostics = closeDiagnostics;
|
|
97
101
|
request.timing?.bind(observation => details.record(observation));
|
|
98
102
|
const trusted = async (ignoreCancellation = false) => {
|
|
99
103
|
if (!ignoreCancellation)
|
|
@@ -130,60 +134,77 @@ export function createClaudeLocalProvider(options) {
|
|
|
130
134
|
let privateFailure, privateTail = Promise.resolve();
|
|
131
135
|
try {
|
|
132
136
|
const unscoped = (observation) => { if (privateFailure)
|
|
133
|
-
throw privateFailure; privateTail = privateTail.then(() => options.recordUnscoped(request, observation)).catch(error => { privateFailure ??= error; void lease?.close().catch(failure => { privateFailure ??= failure; }); }); };
|
|
134
|
-
lease = await (options.ports?.lease ?? openEmployeeLease)({ getConnect: () => options.getConnect(), getNetwork: options.getNetwork, readWorkspace: () => options.readWorkspace(request.sessionId), host: target, sessionId: request.sessionId, control: async (_target, command, signal) => (await transport()).run(command, signal), detail: unscoped }, request.signal);
|
|
135
|
-
const shell = (options.ports?.shell ?? createEmployeeShell)({ target: lease.target, maxBytes: maximum, killGraceMs: grace, attach: async (spec) => (await transport()).attach([spec.command, ...spec.args].map(shellQuote).join(' ')) });
|
|
136
|
-
mcp = await (options.ports?.mcp ?? openEmployeeMcp)({ lease, maxBytes: maximum, killGraceMs: grace, runShell: shell, activeSignal: () => entry?.active?.signal, captureDetail: () => { const sink = entry?.active?.details; if (!sink)
|
|
137
|
+
throw privateFailure; privateTail = privateTail.then(() => options.recordUnscoped(request, observation)).catch(error => { privateFailure ??= error; closeDiagnostics.record({ layer: 'runtime', initiator: 'claude.private-diagnostic-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'failure' }); void lease?.close({ layer: 'endpoint', initiator: 'claude.private-diagnostic-failure', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' }).catch(failure => { privateFailure ??= failure; }); }); };
|
|
138
|
+
lease = await (options.ports?.lease ?? openEmployeeLease)({ closeDiagnostics, getConnect: () => options.getConnect(), getNetwork: options.getNetwork, readWorkspace: () => options.readWorkspace(request.sessionId), host: target, sessionId: request.sessionId, control: async (_target, command, signal) => (await transport()).run(command, signal), detail: unscoped }, request.signal);
|
|
139
|
+
const shell = (options.ports?.shell ?? createEmployeeShell)({ closeDiagnostics, target: lease.target, maxBytes: maximum, killGraceMs: grace, attach: async (spec) => (await transport()).attach([spec.command, ...spec.args].map(shellQuote).join(' ')) });
|
|
140
|
+
mcp = await (options.ports?.mcp ?? openEmployeeMcp)({ closeDiagnostics, lease, maxBytes: maximum, killGraceMs: grace, runShell: shell, activeSignal: () => entry?.active?.signal, captureDetail: () => { const sink = entry?.active?.details; if (!sink)
|
|
137
141
|
throw failure('EMPLOYEE_NO_ACTIVE_TURN'); return observation => sink.record(observation); }, detail: unscoped });
|
|
138
142
|
entry = { runtime: undefined, scope, lease, details, request };
|
|
139
143
|
const row = entry;
|
|
140
144
|
base = await options.open(target, { name: workspace.workspaceId, path: target.session.root }, openRequest(row, true));
|
|
141
145
|
const underlying = base, endpoint = mcp;
|
|
142
|
-
const close = (detach
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
146
|
+
const close = (detach, cause = { layer: 'runtime', initiator: detach ? 'claude.runtime-detach' : 'claude.runtime-dispose', reason: detach ? 'RUNTIME_DETACH' : 'RUNTIME_DISPOSE', action: detach ? 'detach' : 'dispose' }) => {
|
|
147
|
+
closeDiagnostics.record(cause);
|
|
148
|
+
return row.closing ??= Promise.resolve().then(async () => {
|
|
149
|
+
row.active = undefined;
|
|
150
|
+
const errors = [];
|
|
151
|
+
try {
|
|
152
|
+
await endpoint.close({ layer: 'relay', initiator: 'claude.cleanup-mcp', reason: 'RUNTIME_CLEANUP', action: 'close' });
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
errors.push(error);
|
|
156
|
+
}
|
|
157
|
+
await privateTail;
|
|
158
|
+
if (privateFailure)
|
|
159
|
+
errors.push(privateFailure);
|
|
160
|
+
try {
|
|
161
|
+
await (detach ? underlying.detach?.(cause) : underlying.dispose?.(cause));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
errors.push(error);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await row.details.flush();
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
errors.push(error);
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
closeDiagnostics.assertRecorded();
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
errors.push(error);
|
|
177
|
+
}
|
|
178
|
+
if (errors.length)
|
|
179
|
+
throw new AggregateError(errors, 'EMPLOYEE_CLEANUP_UNCONFIRMED');
|
|
180
|
+
if (entries.get(request.sessionId) === row)
|
|
181
|
+
entries.delete(request.sessionId);
|
|
182
|
+
});
|
|
183
|
+
};
|
|
165
184
|
const executionSignal = AbortSignal.any([lease.signal, ...mcp.signal ? [mcp.signal] : []]);
|
|
166
|
-
row.runtime = { ...underlying, cwd: workspace.directory.path, details, connection: { ...connection }, binding: { bindingId: item.bindingId, workspaceId: workspace.workspaceId, revision: state.revision }, execution: { hostCwd: target.session.root, routing: mcp.routing, notice: NOTICE, signal: executionSignal, scope: { source: 'local', cwd: workspace.directory.path, hostCwd: target.session.root, uid: lease.target.endpoint.remoteUid, claudeAcpVersion: '0.94.0', sdkVersion: '0.3.303', unsupported: ['subagents', 'internal-direct-calls', 'binary-read', 'background-shell'] }, validate: async (signal) => { executionSignal.throwIfAborted(); await row.lease.validate(signal); }, beginTurn: async (signal) => { executionSignal.throwIfAborted(); if (row.closing || row.active)
|
|
167
|
-
throw failure('TURN_BUSY'); await row.lease.validate(signal); executionSignal.throwIfAborted(); row.active = { signal, details: row.details }; }, endTurn: () => { row.active = undefined; } }, dispose:
|
|
185
|
+
row.runtime = { ...underlying, closeDiagnostics, cwd: workspace.directory.path, details, connection: { ...connection }, binding: { bindingId: item.bindingId, workspaceId: workspace.workspaceId, revision: state.revision }, execution: { hostCwd: target.session.root, routing: mcp.routing, notice: NOTICE, signal: executionSignal, scope: { source: 'local', cwd: workspace.directory.path, hostCwd: target.session.root, uid: lease.target.endpoint.remoteUid, claudeAcpVersion: '0.94.0', sdkVersion: '0.3.303', unsupported: ['subagents', 'internal-direct-calls', 'binary-read', 'background-shell'] }, validate: async (signal) => { executionSignal.throwIfAborted(); await row.lease.validate(signal); }, beginTurn: async (signal) => { executionSignal.throwIfAborted(); if (row.closing || row.active)
|
|
186
|
+
throw failure('TURN_BUSY'); await row.lease.validate(signal); executionSignal.throwIfAborted(); row.active = { signal, details: row.details }; }, endTurn: () => { row.active = undefined; } }, dispose: cause => close(false, cause), detach: cause => close(true, cause) };
|
|
168
187
|
entries.set(request.sessionId, row);
|
|
169
|
-
lease.signal.addEventListener('abort', () => { void close(false).catch(() => { }); }, { once: true });
|
|
188
|
+
lease.signal.addEventListener('abort', () => { void close(false, { layer: 'endpoint', initiator: 'claude.lease-abort', reason: closeCode(lease?.signal.reason, 'EMPLOYEE_CONTEXT_CHANGED'), action: 'abort' }).catch(() => { }); }, { once: true });
|
|
170
189
|
if (lease.signal.aborted) {
|
|
171
|
-
await close(false);
|
|
190
|
+
await close(false, { layer: 'endpoint', initiator: 'claude.open-aborted', reason: closeCode(lease.signal.reason, 'EMPLOYEE_CONTEXT_CHANGED'), action: 'abort' });
|
|
172
191
|
throw failure('EMPLOYEE_CONTEXT_CHANGED');
|
|
173
192
|
}
|
|
174
193
|
return row.runtime;
|
|
175
194
|
}
|
|
176
195
|
catch (error) {
|
|
196
|
+
closeDiagnostics.record({ layer: 'runtime', initiator: 'claude.open-failure', reason: closeCode(error, 'EMPLOYEE_OPEN_FAILED'), action: 'failure' });
|
|
197
|
+
const cause = { layer: 'runtime', initiator: 'claude.open-cleanup', reason: 'EMPLOYEE_OPEN_FAILED', action: 'close' };
|
|
177
198
|
const cleanup = [];
|
|
178
199
|
try {
|
|
179
|
-
await mcp?.close();
|
|
180
|
-
await lease?.close();
|
|
200
|
+
await mcp?.close(cause);
|
|
201
|
+
await lease?.close(cause);
|
|
181
202
|
}
|
|
182
203
|
catch (e) {
|
|
183
204
|
cleanup.push(e);
|
|
184
205
|
}
|
|
185
206
|
try {
|
|
186
|
-
await base?.dispose?.();
|
|
207
|
+
await base?.dispose?.(cause);
|
|
187
208
|
}
|
|
188
209
|
catch (e) {
|
|
189
210
|
cleanup.push(e);
|
|
@@ -197,6 +218,12 @@ export function createClaudeLocalProvider(options) {
|
|
|
197
218
|
catch (e) {
|
|
198
219
|
cleanup.push(e);
|
|
199
220
|
}
|
|
221
|
+
try {
|
|
222
|
+
closeDiagnostics.assertRecorded();
|
|
223
|
+
}
|
|
224
|
+
catch (e) {
|
|
225
|
+
cleanup.push(e);
|
|
226
|
+
}
|
|
200
227
|
if (cleanup.length)
|
|
201
228
|
throw new AggregateError([error, ...cleanup], 'EMPLOYEE_OPEN_CLEANUP_UNCONFIRMED');
|
|
202
229
|
throw error;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DetailObservation } from './conversation-details.ts';
|
|
2
|
+
import { NeedError } from './remote.ts';
|
|
3
|
+
export interface CloseChange {
|
|
4
|
+
field: string;
|
|
5
|
+
before: string | number | boolean | null;
|
|
6
|
+
after: string | number | boolean | null;
|
|
7
|
+
}
|
|
8
|
+
export interface CloseCause {
|
|
9
|
+
layer: 'acp' | 'adapter' | 'broker' | 'endpoint' | 'relay' | 'executor' | 'runtime';
|
|
10
|
+
initiator: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
action?: 'close' | 'dispose' | 'detach' | 'invalidate' | 'abort' | 'failure' | 'exit' | 'eof' | 'terminate';
|
|
13
|
+
operation?: string;
|
|
14
|
+
exitCode?: number | null;
|
|
15
|
+
signal?: string | null;
|
|
16
|
+
remoteReason?: 'unavailable';
|
|
17
|
+
changedFields?: readonly string[];
|
|
18
|
+
changes?: readonly CloseChange[];
|
|
19
|
+
}
|
|
20
|
+
export type CloseObserver = (cause: CloseCause) => void;
|
|
21
|
+
/** Only typed codes, never exception messages, causes, stderr or RPC data. */
|
|
22
|
+
export declare function closeCode(error: unknown, fallback?: string): string;
|
|
23
|
+
/** Retain trusted typed codes through wrappers; never traverse error text or invoke getters. */
|
|
24
|
+
export declare function closeErrorCodes(error: unknown, fallback?: string): string[];
|
|
25
|
+
export declare function closeFailure(code: string, message: string, requires: string, codes: readonly string[]): NeedError;
|
|
26
|
+
/** Assign order before abort/kill; persist through the existing asynchronous D19 outbox. */
|
|
27
|
+
export declare class CloseDiagnostics {
|
|
28
|
+
private sink;
|
|
29
|
+
readonly parentId?: string | undefined;
|
|
30
|
+
private state;
|
|
31
|
+
readonly id: `${string}-${string}-${string}-${string}-${string}`;
|
|
32
|
+
private sequence;
|
|
33
|
+
private pending;
|
|
34
|
+
private firstValue?;
|
|
35
|
+
private readonly started;
|
|
36
|
+
stage: string;
|
|
37
|
+
constructor(sink: (detail: DetailObservation) => void, parentId?: string | undefined, state?: {
|
|
38
|
+
failure?: {
|
|
39
|
+
error: unknown;
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
run<T>(operation: string, work: () => Promise<T>): Promise<T>;
|
|
43
|
+
get recordingFailureCode(): string | undefined;
|
|
44
|
+
get first(): Readonly<Record<string, unknown>> | undefined;
|
|
45
|
+
bind(sink: (detail: DetailObservation) => void): void;
|
|
46
|
+
/** A delayed resource callback retains its original accepted detail channel. */
|
|
47
|
+
capture(): CloseObserver;
|
|
48
|
+
child(sink?: (detail: DetailObservation) => void): CloseDiagnostics;
|
|
49
|
+
record(cause: CloseCause): void;
|
|
50
|
+
private recordUsing;
|
|
51
|
+
assertRecorded(): void;
|
|
52
|
+
}
|
|
53
|
+
/** Run after owned cleanup; expose both the primary error code and persistence failure. */
|
|
54
|
+
export declare function flushCloseDiagnostics(diagnostics: CloseDiagnostics, flush: (() => Promise<void>) | undefined, primary?: {
|
|
55
|
+
error: unknown;
|
|
56
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { NeedError } from "./remote.js";
|
|
3
|
+
/** Only typed codes, never exception messages, causes, stderr or RPC data. */
|
|
4
|
+
export function closeCode(error, fallback = 'CLOSE_REASON_UNKNOWN') {
|
|
5
|
+
return error instanceof NeedError && /^[A-Z][A-Z0-9_]*$/.test(error.code) ? error.code : fallback;
|
|
6
|
+
}
|
|
7
|
+
/** Retain trusted typed codes through wrappers; never traverse error text or invoke getters. */
|
|
8
|
+
export function closeErrorCodes(error, fallback = 'CLOSE_REASON_UNKNOWN') {
|
|
9
|
+
const queue = [error], seen = new Set(), codes = new Set();
|
|
10
|
+
while (queue.length) {
|
|
11
|
+
const next = queue.pop();
|
|
12
|
+
if (!(next instanceof NeedError || next instanceof AggregateError) || seen.has(next))
|
|
13
|
+
continue;
|
|
14
|
+
seen.add(next);
|
|
15
|
+
if (next instanceof NeedError) {
|
|
16
|
+
const code = Object.getOwnPropertyDescriptor(next, 'code');
|
|
17
|
+
if (code && 'value' in code && typeof code.value === 'string' && /^[A-Z][A-Z0-9_]*$/.test(code.value))
|
|
18
|
+
codes.add(code.value);
|
|
19
|
+
}
|
|
20
|
+
const cause = Object.getOwnPropertyDescriptor(next, 'cause');
|
|
21
|
+
if (cause && 'value' in cause)
|
|
22
|
+
queue.push(cause.value);
|
|
23
|
+
if (next instanceof AggregateError) {
|
|
24
|
+
const errors = Object.getOwnPropertyDescriptor(next, 'errors');
|
|
25
|
+
if (errors && 'value' in errors && Array.isArray(errors.value))
|
|
26
|
+
for (const child of errors.value)
|
|
27
|
+
queue.push(child);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return codes.size ? [...codes] : [fallback];
|
|
31
|
+
}
|
|
32
|
+
export function closeFailure(code, message, requires, codes) {
|
|
33
|
+
return Object.assign(new NeedError(code, message, requires), { cause: new AggregateError([...new Set(codes)].map(value => new NeedError(value, '关闭收尾代码 ' + value, '按安全代码核对')), 'CLOSE_AND_DIAGNOSTIC_FAILURES') });
|
|
34
|
+
}
|
|
35
|
+
/** Assign order before abort/kill; persist through the existing asynchronous D19 outbox. */
|
|
36
|
+
export class CloseDiagnostics {
|
|
37
|
+
sink;
|
|
38
|
+
parentId;
|
|
39
|
+
state;
|
|
40
|
+
id = randomUUID();
|
|
41
|
+
sequence = 0;
|
|
42
|
+
pending = new Map();
|
|
43
|
+
firstValue;
|
|
44
|
+
started = performance.now();
|
|
45
|
+
stage = 'runtime-admission';
|
|
46
|
+
constructor(sink, parentId, state = {}) {
|
|
47
|
+
this.sink = sink;
|
|
48
|
+
this.parentId = parentId;
|
|
49
|
+
this.state = state;
|
|
50
|
+
}
|
|
51
|
+
async run(operation, work) { this.pending.set(operation, (this.pending.get(operation) ?? 0) + 1); try {
|
|
52
|
+
return await work();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
const count = this.pending.get(operation) - 1;
|
|
56
|
+
if (count)
|
|
57
|
+
this.pending.set(operation, count);
|
|
58
|
+
else
|
|
59
|
+
this.pending.delete(operation);
|
|
60
|
+
} }
|
|
61
|
+
get recordingFailureCode() { return this.state.failure ? closeCode(this.state.failure.error, 'CLOSE_RECORD_FAILED') : undefined; }
|
|
62
|
+
get first() { return this.firstValue; }
|
|
63
|
+
bind(sink) { this.sink = sink; }
|
|
64
|
+
/** A delayed resource callback retains its original accepted detail channel. */
|
|
65
|
+
capture() { const sink = this.sink; return cause => this.recordUsing(sink, cause); }
|
|
66
|
+
child(sink = this.sink) { const child = new CloseDiagnostics(sink, this.id, this.state); child.stage = this.stage; return child; }
|
|
67
|
+
record(cause) { this.recordUsing(this.sink, cause); }
|
|
68
|
+
recordUsing(sink, cause) {
|
|
69
|
+
const first = this.sequence === 0;
|
|
70
|
+
const metadata = { ...cause.changedFields ? { changedFields: Object.freeze([...cause.changedFields]) } : {}, changes: Object.freeze((cause.changes ?? [{ field: 'observer.close-sequence', before: this.sequence, after: this.sequence + 1 }]).map(change => Object.freeze({ field: change.field, before: change.before, after: change.after }))), layer: cause.layer, initiator: cause.initiator, reason: cause.reason, ...cause.action ? { action: cause.action } : {}, ...cause.operation ? { operation: cause.operation } : {}, ...cause.exitCode !== undefined ? { exitCode: cause.exitCode } : {}, ...cause.signal !== undefined ? { signal: cause.signal } : {}, ...cause.remoteReason ? { remoteReason: cause.remoteReason } : {} };
|
|
71
|
+
const payload = Object.freeze({ ...metadata, pendingOperations: Object.freeze([...this.pending.keys()]), closeId: this.id, ...this.parentId ? { parentCloseId: this.parentId } : {}, closeSequence: ++this.sequence, first, stage: this.stage, clock: 'client-monotonic', elapsedMs: performance.now() - this.started });
|
|
72
|
+
this.firstValue ??= payload;
|
|
73
|
+
try {
|
|
74
|
+
sink({ kind: 'session', severity: 'warning', code: first ? 'REMOTE_CLOSE_FIRST' : 'REMOTE_CLOSE_NEXT', summary: first ? '首次观察到关闭(不等同根因)' : '后续关闭或清理', payload });
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
this.state.failure ??= { error };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
assertRecorded() { if (this.state.failure)
|
|
81
|
+
throw new NeedError('CONVERSATION_DETAILS_FAILED', '关闭原因保存未完成;首次观察原因 ' + String(this.firstValue?.reason ?? 'CLOSE_REASON_UNKNOWN') + '。', '关闭诊断保存代码 ' + this.recordingFailureCode + ';不将保存失败当作原始关闭原因'); }
|
|
82
|
+
}
|
|
83
|
+
/** Run after owned cleanup; expose both the primary error code and persistence failure. */
|
|
84
|
+
export async function flushCloseDiagnostics(diagnostics, flush, primary) {
|
|
85
|
+
const reporting = [];
|
|
86
|
+
try {
|
|
87
|
+
await flush?.();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
reporting.push(...closeErrorCodes(error, 'CLOSE_FLUSH_FAILED'));
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
diagnostics.assertRecorded();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
reporting.push(diagnostics.recordingFailureCode ?? closeCode(error, 'CLOSE_RECORD_FAILED'));
|
|
97
|
+
}
|
|
98
|
+
if (reporting.length) {
|
|
99
|
+
const primaryCodes = primary ? closeErrorCodes(primary.error, 'REMOTE_CLEANUP_UNCONFIRMED') : [];
|
|
100
|
+
throw closeFailure('CONVERSATION_DETAILS_FAILED', '关闭诊断保存未完成;原关闭与清理结果需核对。', '首次关闭 ' + String(diagnostics.first?.reason ?? 'CLOSE_REASON_UNKNOWN') + ';清理 ' + (primaryCodes.join(',') || 'closed') + ';保存 ' + reporting.join(','), [...primaryCodes, ...reporting]);
|
|
101
|
+
}
|
|
102
|
+
if (primary)
|
|
103
|
+
throw primary.error;
|
|
104
|
+
}
|
|
@@ -283,6 +283,10 @@ function modelConfigurationStructure(value, level = 'root') {
|
|
|
283
283
|
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
284
284
|
if (typeof child === 'string' && strings[level].includes(key))
|
|
285
285
|
return [key, child];
|
|
286
|
+
if (level === 'root' && key === 'resolvedModelSource' && child === 'host-user-catalog')
|
|
287
|
+
return [key, child];
|
|
288
|
+
if (level === 'root' && key === 'sessionModelReported' && child === false)
|
|
289
|
+
return [key, false];
|
|
286
290
|
if (level === 'root' && key === 'status' && child === 'remote-configured')
|
|
287
291
|
return [key, child];
|
|
288
292
|
if (level === 'actual' && typeof child === 'boolean' && ['confirmed', 'effortAvailable'].includes(key))
|
|
@@ -331,6 +335,58 @@ function employeeStructure(value) {
|
|
|
331
335
|
return [key, opaqueStructure(child)];
|
|
332
336
|
}));
|
|
333
337
|
}
|
|
338
|
+
/** Closure metadata has no arbitrary exception or transport payload fields. */
|
|
339
|
+
function closeStructure(value) {
|
|
340
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
341
|
+
return opaqueStructure(value);
|
|
342
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
343
|
+
if (key === 'changes' && Array.isArray(child))
|
|
344
|
+
return [key, child.map(change => {
|
|
345
|
+
if (!change || typeof change !== 'object' || Array.isArray(change))
|
|
346
|
+
return opaqueStructure(change);
|
|
347
|
+
const field = change.field;
|
|
348
|
+
const known = typeof field === 'string' && (/^(?:observer.close-sequence|executor.ended|transport.closed|transport.termination-requested|provider.ended|broker.ended|broker.attached|broker.lease-aborted|lease.aborted|lease.closed|mcp.ended|mcp.closing|mcp.poisoned|shell.settled|shell.stop-code|tools.aborted|tools.stop-code|acp.close-requested|adapter.turn-aborted|adapter.detached|adapter.shutting-down|workspace.binding-id|workspace.source|workspace.status|workspace.match-count|network.phase|catalog.api-replaced|catalog.capability|catalog.signature-sha256|connection.instance-id|connection.epoch|connection.phase|connection.need-code)$/.test(field) || /^catalog\.(?:codex|claude)\.(?:ok|error-code|instance-id|epoch|provider|target-sha256|metadata-sha256)$/.test(field));
|
|
349
|
+
if (!known)
|
|
350
|
+
return opaqueStructure(change);
|
|
351
|
+
const scalar = (value) => value === null || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value) || typeof value === 'string' && /^[a-zA-Z0-9_.:@-]+$/.test(value) ? value : opaqueStructure(value);
|
|
352
|
+
return Object.fromEntries(Object.entries(change).map(([name, value]) => [name, name === 'field' ? field : ['before', 'after'].includes(name) ? scalar(value) : opaqueStructure(value)]));
|
|
353
|
+
})];
|
|
354
|
+
if (key === 'pendingOperations' && Array.isArray(child))
|
|
355
|
+
return [key, child.map(field => typeof field === 'string' && /^[a-z][a-z0-9._/-]*$/.test(field) ? field : opaqueStructure(field))];
|
|
356
|
+
if (key === 'changedFields' && Array.isArray(child))
|
|
357
|
+
return [key, child.map(field => typeof field === 'string' && /^[a-z][a-z-]*$/.test(field) ? field : opaqueStructure(field))];
|
|
358
|
+
if (['closeId', 'parentCloseId'].includes(key) && typeof child === 'string' && /^[a-f0-9-]+$/.test(child))
|
|
359
|
+
return [key, child];
|
|
360
|
+
if (['layer', 'initiator', 'action', 'operation', 'stage', 'clock'].includes(key) && typeof child === 'string' && /^[a-z][a-z0-9._/-]*$/.test(child))
|
|
361
|
+
return [key, child];
|
|
362
|
+
if (key === 'reason' && typeof child === 'string' && /^[A-Z][A-Z0-9_]*$/.test(child))
|
|
363
|
+
return [key, child];
|
|
364
|
+
if (key === 'remoteReason' && child === 'unavailable' || key === 'first' && typeof child === 'boolean')
|
|
365
|
+
return [key, child];
|
|
366
|
+
if (['closeSequence', 'elapsedMs'].includes(key) && typeof child === 'number' && Number.isFinite(child) && child >= 0)
|
|
367
|
+
return [key, child];
|
|
368
|
+
if (key === 'exitCode' && (child === null || typeof child === 'number' && Number.isSafeInteger(child)))
|
|
369
|
+
return [key, child];
|
|
370
|
+
if (key === 'signal' && (child === null || typeof child === 'string' && /^SIG[A-Z]+$/.test(child)))
|
|
371
|
+
return [key, child];
|
|
372
|
+
return [key, opaqueStructure(child)];
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
function catalogScopeStructure(value, nested = false) {
|
|
376
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || isSanitizedRedaction(value))
|
|
377
|
+
return opaqueStructure(value);
|
|
378
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
379
|
+
if (!nested && key === 'reason' && child === 'CATALOG_PRESENTATION_CHANGED')
|
|
380
|
+
return [key, child];
|
|
381
|
+
if (!nested && ['before', 'after'].includes(key))
|
|
382
|
+
return [key, catalogScopeStructure(child, true)];
|
|
383
|
+
if (nested && key === 'presentationSha256' && (child === null || typeof child === 'string' && /^[a-f0-9]{64}$/.test(child)))
|
|
384
|
+
return [key, child];
|
|
385
|
+
if (nested && key === 'needCode' && (child === null || typeof child === 'string' && /^[A-Z][A-Z0-9_]*$/.test(child)))
|
|
386
|
+
return [key, child];
|
|
387
|
+
return [key, opaqueStructure(child)];
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
334
390
|
export function sanitizeObservation(observation, protectModelMaterial = false) {
|
|
335
391
|
const payload = snapshotPayload(observation.payload, new Set(), protectModelMaterial && observation.kind !== 'commands');
|
|
336
392
|
return freezeDeep({
|
|
@@ -338,15 +394,17 @@ export function sanitizeObservation(observation, protectModelMaterial = false) {
|
|
|
338
394
|
code: sanitizeDisplayLabel(observation.code), summary: sanitizeDisplayLabel(observation.summary),
|
|
339
395
|
...(observation.method === undefined ? {} : { method: sanitizeDisplayLabel(observation.method) }),
|
|
340
396
|
payload: !protectModelMaterial ? payload
|
|
341
|
-
: observation.kind === 'session' && observation.code === '
|
|
342
|
-
: observation.kind === 'session' &&
|
|
343
|
-
: observation.kind === '
|
|
344
|
-
: observation.kind === '
|
|
345
|
-
: observation.kind === '
|
|
346
|
-
: observation.kind === '
|
|
347
|
-
: observation.kind === '
|
|
348
|
-
: observation.kind === '
|
|
349
|
-
:
|
|
397
|
+
: observation.kind === 'session' && observation.code === 'CATALOG_SCOPE_INFORMATION' ? catalogScopeStructure(payload)
|
|
398
|
+
: observation.kind === 'session' && observation.code === 'CODEX_CONTEXT_SESSION_REPLACED' ? contextTransitionStructure(payload)
|
|
399
|
+
: observation.kind === 'session' && ['MODEL_CONFIGURATION_CONFIRMED', 'MODEL_CONFIGURATION_FAILED', 'MODEL_CONFIGURATION_INFORMATION', 'MODEL_CONFIGURATION_CHANGED_DURING_TURN', 'TURN_GATE_INFORMATION'].includes(observation.code) ? modelConfigurationStructure(payload)
|
|
400
|
+
: observation.kind === 'session' && ['REMOTE_CLOSE_FIRST', 'REMOTE_CLOSE_NEXT'].includes(observation.code) ? closeStructure(payload)
|
|
401
|
+
: observation.kind === 'commands' ? catalogStructure(payload)
|
|
402
|
+
: observation.kind === 'usage' ? usageStructure(payload)
|
|
403
|
+
: observation.kind === 'session' && observation.code === 'REMOTE_STAGE_TIMING' ? timingStructure(payload)
|
|
404
|
+
: observation.kind === 'session' && ['CLAUDE_EMPLOYEE_OPERATION', 'CLAUDE_EMPLOYEE_PROCESS', 'CLAUDE_EMPLOYEE_REVOKED', 'CLAUDE_EMPLOYEE_SCOPE'].includes(observation.code) ? employeeStructure(payload)
|
|
405
|
+
: observation.kind === 'need' ? payload
|
|
406
|
+
: observation.kind === 'tool' || ['SDK_ITEM', 'SDK_TOOL', 'ACP_SESSION_UPDATE', 'ACP_IDLE_UPDATE'].includes(observation.code) ? toolStructure(payload)
|
|
407
|
+
: opaqueStructure(payload),
|
|
350
408
|
});
|
|
351
409
|
}
|
|
352
410
|
function validatedScope(scope) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CloseCause, type CloseDiagnostics } from './close-diagnostics.ts';
|
|
1
2
|
import type { ConnectApi as LApi, LocalWorkspaceContextRequest } from '@ai-workbench/connect/api';
|
|
2
3
|
import type { ConnectTarget } from './provider.ts';
|
|
3
4
|
import type { DetailObservation } from './conversation-details.ts';
|
|
@@ -66,6 +67,7 @@ export interface EmployeeWorkspace {
|
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
69
|
export interface EmployeeLeaseOptions {
|
|
70
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
69
71
|
getConnect: () => EmployeeConnectApi | undefined;
|
|
70
72
|
getNetwork: () => EmployeeNetworkApi | undefined;
|
|
71
73
|
readWorkspace: () => Promise<EmployeeWorkspace>;
|
|
@@ -86,6 +88,6 @@ export interface EmployeeLease {
|
|
|
86
88
|
request: Readonly<LocalWorkspaceContextRequest>;
|
|
87
89
|
signal: AbortSignal;
|
|
88
90
|
validate(callerSignal?: AbortSignal): Promise<EmployeeEndpointTarget>;
|
|
89
|
-
close(): Promise<void>;
|
|
91
|
+
close(cause?: CloseCause): Promise<void>;
|
|
90
92
|
}
|
|
91
93
|
export declare function openEmployeeLease(options: EmployeeLeaseOptions, initialSignal?: AbortSignal): Promise<EmployeeLease>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeCode } from "./close-diagnostics.js";
|
|
1
2
|
import { lstat, realpath } from 'node:fs/promises';
|
|
2
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
4
|
import { sameTarget, unwrap } from "./provider.js";
|
|
@@ -41,7 +42,7 @@ export async function openEmployeeLease(options, initialSignal) {
|
|
|
41
42
|
throw fail();
|
|
42
43
|
const request = { instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.workspaceId, expectedBindingId: items[0].bindingId };
|
|
43
44
|
let target, closed = false, diagnosticFailure;
|
|
44
|
-
const invalidate = (code) => { if (lifetime.signal.aborted)
|
|
45
|
+
const invalidate = (code, initiator = 'lease.validate') => { options.closeDiagnostics?.record({ layer: 'endpoint', initiator, reason: code, action: 'invalidate', changes: [{ field: 'lease.aborted', before: lifetime.signal.aborted, after: true }] }); if (lifetime.signal.aborted)
|
|
45
46
|
return; try {
|
|
46
47
|
options.detail({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_REVOKED', summary: '本机旧绑定已停止派发,在途结果按实际确认;不会重放', payload: { reason: code, workspaceId: request.workspaceId, bindingId: request.expectedBindingId } });
|
|
47
48
|
}
|
|
@@ -59,14 +60,14 @@ export async function openEmployeeLease(options, initialSignal) {
|
|
|
59
60
|
throw fail();
|
|
60
61
|
lifetime.signal.throwIfAborted();
|
|
61
62
|
};
|
|
62
|
-
const changed = () => { try {
|
|
63
|
+
const changed = (initiator) => { try {
|
|
63
64
|
current();
|
|
64
65
|
}
|
|
65
66
|
catch {
|
|
66
|
-
invalidate('EMPLOYEE_CONTEXT_CHANGED');
|
|
67
|
+
invalidate('EMPLOYEE_CONTEXT_CHANGED', initiator);
|
|
67
68
|
} };
|
|
68
|
-
offs.push(api.onConnectionState(changed), api.onWorkspaceLocationState(changed), network.onState(event => { if (event?.kind === 'lost' || network.getState().phase !== 'online')
|
|
69
|
-
invalidate('EMPLOYEE_ENDPOINT_UNAVAILABLE'); }));
|
|
69
|
+
offs.push(api.onConnectionState(() => changed('lease.connection-change')), api.onWorkspaceLocationState(() => changed('lease.workspace-change')), network.onState(event => { if (event?.kind === 'lost' || network.getState().phase !== 'online')
|
|
70
|
+
invalidate('EMPLOYEE_ENDPOINT_UNAVAILABLE', 'lease.network-loss'); }));
|
|
70
71
|
async function validate(callerSignal) {
|
|
71
72
|
if (diagnosticFailure)
|
|
72
73
|
throw diagnosticFailure;
|
|
@@ -124,7 +125,7 @@ export async function openEmployeeLease(options, initialSignal) {
|
|
|
124
125
|
}
|
|
125
126
|
try {
|
|
126
127
|
await validate(initialSignal);
|
|
127
|
-
return { target: structuredClone(target), request: Object.freeze({ ...request }), signal: lifetime.signal, validate, close: async () => { if (!closed) {
|
|
128
|
+
return { target: structuredClone(target), request: Object.freeze({ ...request }), signal: lifetime.signal, validate, close: async (cause = { layer: 'endpoint', initiator: 'lease.close', reason: 'EMPLOYEE_EXECUTOR_CLOSED', action: 'close' }) => { options.closeDiagnostics?.record({ ...cause, changes: cause.changes ?? [{ field: 'lease.closed', before: closed, after: true }] }); if (!closed) {
|
|
128
129
|
closed = true;
|
|
129
130
|
lifetime.abort(fail('EMPLOYEE_EXECUTOR_CLOSED'));
|
|
130
131
|
for (const off of offs)
|
|
@@ -133,6 +134,7 @@ export async function openEmployeeLease(options, initialSignal) {
|
|
|
133
134
|
throw diagnosticFailure; } };
|
|
134
135
|
}
|
|
135
136
|
catch (error) {
|
|
137
|
+
options.closeDiagnostics?.record({ layer: 'endpoint', initiator: 'lease.open-failure', reason: closeCode(error), action: 'failure' });
|
|
136
138
|
for (const off of offs)
|
|
137
139
|
off();
|
|
138
140
|
lifetime.abort();
|