@ai-workbench/remote-agent 0.8.13 → 0.8.15
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 +10 -8
- 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 +62 -35
- 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-presentation.d.ts +6 -0
- package/dist/employee-presentation.js +16 -0
- package/dist/employee-provider.js +59 -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 +18 -4
- package/dist/index.js +183 -55
- 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/model-presentation.d.ts +13 -0
- package/dist/model-presentation.js +18 -0
- 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-runtime.js
CHANGED
|
@@ -6,6 +6,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { CloseDiagnostics, closeCode, flushCloseDiagnostics } from "./close-diagnostics.js";
|
|
9
10
|
import { BrokerTransport, brokerCommand, brokerId, legacyBrokerId, sshIo, controlJson } from "./broker.js";
|
|
10
11
|
import { CODEX_AGENT } from "./agents.js";
|
|
11
12
|
import { protectedOneShotLayout } from "./host-layout.js";
|
|
@@ -63,15 +64,24 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
63
64
|
const contextBoundScope = recoveryKey([...scopeParts, request.contextSha256]);
|
|
64
65
|
// A Codex ACP process can own several immutable native sessions. D18's durable
|
|
65
66
|
// pointer controls their context; the broker still binds every transport identity.
|
|
66
|
-
const scope =
|
|
67
|
+
const scope = recoveryKey([...scopeParts, undefined]);
|
|
68
|
+
const previousContextScope = agent.loginProvider === 'claude' && request.previousContextSha256 ? recoveryKey([...scopeParts, request.previousContextSha256]) : contextBoundScope;
|
|
67
69
|
const existing = request.retainProcess ? retained.get(key) : undefined;
|
|
68
70
|
if (existing) {
|
|
69
71
|
if (existing.scope !== scope)
|
|
70
72
|
throw need('SESSION_CONTEXT_MISMATCH');
|
|
71
73
|
if (existing.runtime.broker?.error)
|
|
72
74
|
throw existing.runtime.broker.error;
|
|
75
|
+
if (request.requireFreshSession) {
|
|
76
|
+
if (existing.runtime.broker?.promptInFlight)
|
|
77
|
+
throw need('INPUT_DELIVERY_UNCONFIRMED');
|
|
78
|
+
await existing.runtime.dispose?.({ layer: 'broker', initiator: 'broker.fresh-session-required', reason: 'PRIVATE_RECOVERY_POINTER_ABSENT', action: 'dispose' });
|
|
79
|
+
return open(target, project, request);
|
|
80
|
+
}
|
|
73
81
|
existing.update(request);
|
|
74
82
|
existing.runtime.restartNotice = undefined;
|
|
83
|
+
existing.runtime.restartInputIds = undefined;
|
|
84
|
+
existing.runtime.freshSessionConfirmed = undefined;
|
|
75
85
|
existing.runtime.recoveredSession = undefined;
|
|
76
86
|
return existing.runtime;
|
|
77
87
|
}
|
|
@@ -83,7 +93,7 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
83
93
|
if (!journal)
|
|
84
94
|
throw need('RECOVERY_STORAGE_REQUIRED');
|
|
85
95
|
let state = await journal.read();
|
|
86
|
-
const legacyContextScope = !!state && state.scope !== scope &&
|
|
96
|
+
const legacyContextScope = !!state && state.scope !== scope && state.scope === previousContextScope;
|
|
87
97
|
if (state && state.scope !== scope && !legacyContextScope)
|
|
88
98
|
throw need(agent.loginProvider === 'codex' ? 'RECOVERY_CONTEXT_POLICY_UNSUPPORTED' : 'SESSION_CONTEXT_MISMATCH');
|
|
89
99
|
if (legacyContextScope && state?.phase !== 'clean')
|
|
@@ -106,9 +116,11 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
106
116
|
const report = async (code, extra = {}, cleanup = false) => { request.detailChannel?.record({ kind: 'need', severity: 'warning', code, summary: '远端会话恢复状态:' + code, payload: extra }); await request.detailChannel?.flush(); await validate(cleanup); };
|
|
107
117
|
const list = async (cleanup = false) => timing.run('broker-list', async () => { const rows = controlJson(await io.run(command(['list']), cleanup ? undefined : request.signal)); if (!Array.isArray(rows) || rows.some(row => typeof row?.id !== 'string'))
|
|
108
118
|
throw need('BROKER_PROTOCOL_INVALID'); return rows; });
|
|
119
|
+
const detailSink = request.detailChannel;
|
|
120
|
+
const closeDiagnostics = request.closeDiagnostics ?? new CloseDiagnostics(event => detailSink?.record(event));
|
|
109
121
|
let rows = await list();
|
|
110
122
|
const id = brokerId(request.sessionId, employeeScope ? agent.id + '/employee/' + employeeScope : agent.id), oldId = legacyBrokerId(request.sessionId, agent.id);
|
|
111
|
-
let restarted =
|
|
123
|
+
let restarted = !!request.requireFreshSession;
|
|
112
124
|
const attachArgs = (entry, after) => ['attach', entry.id, '--after', String(after), '--framed', ...(modern(entry) ? ['--leased', '--if-instance', entry.instanceId] : [])];
|
|
113
125
|
const connectWithLease = async (transport, entry, cleanup = false) => {
|
|
114
126
|
try {
|
|
@@ -143,13 +155,13 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
143
155
|
}
|
|
144
156
|
};
|
|
145
157
|
const claim = async (entry, cleanup = false) => {
|
|
146
|
-
const probe = new BrokerTransport({ discardOutput: true, onNeed: n => request.detailChannel?.record({ kind: 'need', severity: 'warning', code: n.code, summary: '主机会话连接状态', payload: { brokerId: entry.id, instanceId: entry.instanceId ?? null } }), maxFrameBytes: settings.brokerMaxFrameBytes, initialSequence: entry.lastSequence, instanceId: entry.instanceId, leased: modern(entry), controlMaxBytes: entry.controlMaxBytes, attach: after => io.attach(command(attachArgs(entry, after))) });
|
|
158
|
+
const probe = new BrokerTransport({ observeClose: closeDiagnostics.child().capture(), discardOutput: true, onNeed: n => request.detailChannel?.record({ kind: 'need', severity: 'warning', code: n.code, summary: '主机会话连接状态', payload: { brokerId: entry.id, instanceId: entry.instanceId ?? null } }), maxFrameBytes: settings.brokerMaxFrameBytes, initialSequence: entry.lastSequence, instanceId: entry.instanceId, leased: modern(entry), controlMaxBytes: entry.controlMaxBytes, attach: after => io.attach(command(attachArgs(entry, after))) });
|
|
147
159
|
try {
|
|
148
160
|
await connectWithLease(probe, entry, cleanup);
|
|
149
161
|
}
|
|
150
162
|
catch (error) {
|
|
151
163
|
if (!(entry.state === 'exited' && error instanceof NeedError && error.code === 'AGENT_EXITED')) {
|
|
152
|
-
probe.close();
|
|
164
|
+
probe.close({ layer: 'broker', initiator: 'broker.probe-close', reason: 'BROKER_PROBE_FINISHED', action: 'close' });
|
|
153
165
|
throw error;
|
|
154
166
|
}
|
|
155
167
|
}
|
|
@@ -183,7 +195,7 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
183
195
|
throw need('REMOTE_CLEANUP_UNCONFIRMED');
|
|
184
196
|
}
|
|
185
197
|
finally {
|
|
186
|
-
probe.close();
|
|
198
|
+
probe.close({ layer: 'broker', initiator: 'broker.probe-close', reason: 'BROKER_PROBE_FINISHED', action: 'close' });
|
|
187
199
|
}
|
|
188
200
|
};
|
|
189
201
|
const legacy = employeeScope ? undefined : rows.find(row => row.id === oldId);
|
|
@@ -192,13 +204,13 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
192
204
|
let entry = rows.find(row => row.id === id);
|
|
193
205
|
if (entry)
|
|
194
206
|
validEntry(entry);
|
|
195
|
-
if (legacyContextScope && !entry)
|
|
207
|
+
if (legacyContextScope && !entry && !request.requireFreshSession)
|
|
196
208
|
throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
|
|
197
209
|
if (legacy && !entry)
|
|
198
210
|
restarted = true;
|
|
199
211
|
let transport;
|
|
200
212
|
let resume;
|
|
201
|
-
const build = (entry, after, recovery) => new BrokerTransport({ onNeed: n => request.detailChannel?.record({ kind: 'need', severity: 'warning', code: n.code, summary: '主机会话连接状态', payload: { brokerId: entry.id, instanceId: entry.instanceId } }), maxFrameBytes: settings.brokerMaxFrameBytes, maxMessageBytes: request.acpMaxMessageBytes ?? settings.brokerMaxFrameBytes, initialSequence: after, instanceId: entry.instanceId, leased: true, controlMaxBytes: entry.controlMaxBytes, beforeInput: async () => { await validate(); if (state?.phase !== 'dirty' || state.instanceId !== entry.instanceId) {
|
|
213
|
+
const build = (entry, after, recovery) => new BrokerTransport({ observeClose: closeDiagnostics.capture(), onNeed: n => request.detailChannel?.record({ kind: 'need', severity: 'warning', code: n.code, summary: '主机会话连接状态', payload: { brokerId: entry.id, instanceId: entry.instanceId } }), maxFrameBytes: settings.brokerMaxFrameBytes, maxMessageBytes: request.acpMaxMessageBytes ?? settings.brokerMaxFrameBytes, initialSequence: after, instanceId: entry.instanceId, leased: true, controlMaxBytes: entry.controlMaxBytes, beforeInput: async () => { await validate(); if (state?.phase !== 'dirty' || state.instanceId !== entry.instanceId) {
|
|
202
214
|
const dirty = { version: 1, scope, brokerId: id, instanceId: entry.instanceId, pid: entry.pid, phase: 'dirty' };
|
|
203
215
|
await journal.write(dirty);
|
|
204
216
|
state = dirty;
|
|
@@ -225,6 +237,15 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
225
237
|
if (entry) {
|
|
226
238
|
if (!modern(entry))
|
|
227
239
|
throw need('BROKER_CAPABILITY_REQUIRED');
|
|
240
|
+
if (request.requireFreshSession) {
|
|
241
|
+
if (!state || !['clean', 'ended'].includes(state.phase) || state.instanceId !== entry.instanceId)
|
|
242
|
+
throw need('INPUT_DELIVERY_UNCONFIRMED');
|
|
243
|
+
await stop(entry, 'PRIVATE_RECOVERY_POINTER_ABSENT');
|
|
244
|
+
restarted = true;
|
|
245
|
+
entry = undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (entry) {
|
|
228
249
|
const clean = !employeeScope && state?.phase === 'clean' && state.instanceId === entry.instanceId && state.acp?.sessionId === request.resumeSessionId && state.checkpointId === request.recoveryCheckpointId && state.cursor >= entry.firstSequence - 1 && state.cursor <= entry.lastSequence && entry.state === 'running' && (legacyContextScope ? await request.verifyRecovery?.() === true : await request.verifyRecovery?.() !== false);
|
|
229
250
|
if (legacyContextScope && !clean)
|
|
230
251
|
throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
|
|
@@ -237,14 +258,14 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
237
258
|
await validate();
|
|
238
259
|
if (legacyContextScope) {
|
|
239
260
|
const migrated = { ...state, scope };
|
|
240
|
-
await report('CODEX_RECOVERY_SCOPE_MIGRATED', { oldScope: state.scope, newScope: scope, sessionId: state.acp.sessionId, checkpointId: state.checkpointId, instanceId: entry.instanceId, cursor: state.cursor });
|
|
261
|
+
await report(agent.loginProvider === 'codex' ? 'CODEX_RECOVERY_SCOPE_MIGRATED' : 'CLAUDE_RECOVERY_SCOPE_MIGRATED', { oldScope: state.scope, newScope: scope, sessionId: state.acp.sessionId, checkpointId: state.checkpointId, instanceId: entry.instanceId, cursor: state.cursor });
|
|
241
262
|
await journal.write(migrated);
|
|
242
263
|
state = migrated;
|
|
243
264
|
}
|
|
244
265
|
resume = state.acp;
|
|
245
266
|
}
|
|
246
267
|
catch (error) {
|
|
247
|
-
transport.close();
|
|
268
|
+
transport.close({ layer: 'broker', initiator: 'broker.replay-failure', reason: closeCode(error, 'RECOVERY_UNCONFIRMED'), action: 'close' });
|
|
248
269
|
if (legacyContextScope || error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH', 'ABORTED'].includes(error.code))
|
|
249
270
|
throw error;
|
|
250
271
|
await report('SESSION_REPLAY_UNRECOVERABLE', { brokerId: id, instanceId: entry.instanceId, reason: error instanceof NeedError ? error.code : 'UNKNOWN_FAILURE', recordedCursor: state?.cursor ?? null, firstSequence: entry.firstSequence, lastSequence: entry.lastSequence });
|
|
@@ -274,7 +295,7 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
274
295
|
validEntry(entry);
|
|
275
296
|
transport = build(entry, 0);
|
|
276
297
|
const opening = transport;
|
|
277
|
-
const aborted = () => opening.fail(need('ABORTED'));
|
|
298
|
+
const aborted = () => opening.fail(need('ABORTED'), { layer: 'broker', initiator: 'broker.open-signal', reason: 'ABORTED', action: 'abort' });
|
|
278
299
|
request.signal?.addEventListener('abort', aborted, { once: true });
|
|
279
300
|
try {
|
|
280
301
|
request.signal?.throwIfAborted();
|
|
@@ -284,7 +305,7 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
284
305
|
catch (error) {
|
|
285
306
|
if (!(error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH'].includes(error.code)))
|
|
286
307
|
await stop(entry, 'SESSION_OPEN_FAILED', opening, true, true);
|
|
287
|
-
opening.close();
|
|
308
|
+
opening.close({ layer: 'broker', initiator: 'broker.open-failure', reason: closeCode(error), action: 'close' });
|
|
288
309
|
throw error;
|
|
289
310
|
}
|
|
290
311
|
finally {
|
|
@@ -294,8 +315,8 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
294
315
|
const ownedEntry = entry, ownedTransport = transport;
|
|
295
316
|
let disposed = false;
|
|
296
317
|
let disposal;
|
|
297
|
-
const runtime = { identity: identity(target), cwd: project.path, broker: ownedTransport, acpTransport: ownedTransport,
|
|
298
|
-
...(resume ? { recoveredSession: resume } : {}), ...(restarted ? { restartNotice: '上次会话无法接回,已新建会话;本次从你的这条请求继续。', restartInputIds: request.currentInputIds } : {}),
|
|
318
|
+
const runtime = { closeDiagnostics, identity: identity(target), cwd: project.path, broker: ownedTransport, acpTransport: ownedTransport,
|
|
319
|
+
...(resume ? { recoveredSession: resume } : {}), ...(restarted ? { restartNotice: '上次会话无法接回,已新建会话;本次从你的这条请求继续。', restartInputIds: request.currentInputIds } : {}), ...(request.requireFreshSession ? { freshSessionConfirmed: true } : {}),
|
|
299
320
|
saveCheckpoint: async (checkpointId, acp) => { await validate(); const saved = await ownedTransport.commitCheckpoint(async (cursor) => { await validate(); const clean = { version: 1, phase: 'clean', scope, brokerId: id, instanceId: ownedEntry.instanceId, pid: ownedEntry.pid, cursor, checkpointId, acp }; try {
|
|
300
321
|
await journal.write(clean);
|
|
301
322
|
state = clean;
|
|
@@ -305,24 +326,29 @@ export function brokerFactory(get, _ask, configuration, provided) {
|
|
|
305
326
|
throw error;
|
|
306
327
|
} }); if (!saved)
|
|
307
328
|
await report('SESSION_CHECKPOINT_NOT_CLEAN', { brokerId: id, instanceId: ownedEntry.instanceId }); return saved; },
|
|
308
|
-
detach: async () => { ownedTransport.close(); active.delete(key); retained.delete(key); },
|
|
309
|
-
dispose: () => disposal ??= (async () => { if (disposed)
|
|
310
|
-
return; try {
|
|
329
|
+
detach: async (cause = { layer: 'broker', initiator: 'broker.runtime-detach', reason: 'BROKER_DETACH', action: 'detach' }) => { const sink = request.detailChannel; closeDiagnostics.record(cause); ownedTransport.close(cause); active.delete(key); retained.delete(key); await flushCloseDiagnostics(closeDiagnostics, sink ? () => sink.flush() : undefined); },
|
|
330
|
+
dispose: (cause = { layer: 'broker', initiator: 'broker.runtime-dispose', reason: 'BROKER_DISPOSE', action: 'dispose' }) => { const sink = request.detailChannel; closeDiagnostics.record(cause); return disposal ??= (async () => { if (disposed)
|
|
331
|
+
return; let primary; try {
|
|
311
332
|
await stop(ownedEntry, 'SESSION_ENDED', ownedTransport, true);
|
|
312
333
|
await journal.write({ version: 1, phase: 'ended', scope, brokerId: id, instanceId: ownedEntry.instanceId, pid: ownedEntry.pid });
|
|
313
334
|
disposed = true;
|
|
314
335
|
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
primary = { error };
|
|
338
|
+
}
|
|
315
339
|
finally {
|
|
316
|
-
ownedTransport.close();
|
|
340
|
+
ownedTransport.close({ layer: 'broker', initiator: 'broker.dispose-finally', reason: 'BROKER_DISPOSE_CLEANUP', action: 'close' });
|
|
317
341
|
active.delete(key);
|
|
318
342
|
retained.delete(key);
|
|
319
|
-
}
|
|
343
|
+
} await flushCloseDiagnostics(closeDiagnostics, sink ? () => sink.flush() : undefined, primary); })(); },
|
|
344
|
+
};
|
|
320
345
|
if (employeeScope) {
|
|
321
346
|
runtime.saveCheckpoint = undefined;
|
|
322
347
|
runtime.detach = runtime.dispose;
|
|
323
348
|
}
|
|
324
349
|
if (request.retainProcess)
|
|
325
|
-
retained.set(key, { runtime, scope, update: next => {
|
|
350
|
+
retained.set(key, { runtime, scope, update: next => { const sink = next.detailChannel; if (sink)
|
|
351
|
+
closeDiagnostics.bind(event => sink.record(event)); request = next; timing = next.timing ?? new StageTiming(); } });
|
|
326
352
|
return runtime;
|
|
327
353
|
}
|
|
328
354
|
catch (error) {
|
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,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { CLAUDE_EMPLOYEE_SCOPE_NOTICE as NOTICE } from "./employee-presentation.js";
|
|
10
|
+
import { CloseDiagnostics, closeCode } from "./close-diagnostics.js";
|
|
9
11
|
import { isDeepStrictEqual } from 'node:util';
|
|
10
12
|
import { openEmployeeLease } from "./employee-endpoint.js";
|
|
11
13
|
import { openEmployeeMcp } from "./employee-mcp.js";
|
|
@@ -15,7 +17,6 @@ import { sshIo } from "./broker.js";
|
|
|
15
17
|
import { NeedError, shellQuote } from "./remote.js";
|
|
16
18
|
import { awaitL } from "./provider.js";
|
|
17
19
|
const failure = (code) => new NeedError(code, '当前本机执行上下文无法确认,本轮已停止;没有改在主机执行。', 'L当前本地绑定、N员工端点与S同代桥');
|
|
18
|
-
const NOTICE = '本轮六类文件与命令工具在你的电脑执行,使用你选择的文件夹;Git也使用本机仓库。子代理和内部直接调用尚不支持本机,可能仍在主机;已识别的此类审批不会执行。PDF等二进制读取、后台命令暂不支持。';
|
|
19
20
|
/** Local source only. Host and legacy-host remain with the original provider. */
|
|
20
21
|
export function createClaudeLocalProvider(options) {
|
|
21
22
|
const entries = new Map();
|
|
@@ -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>;
|