@ai-workbench/remote-agent 0.8.2 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/acp.d.ts +25 -0
- package/dist/acp.js +159 -17
- package/dist/approval-label.d.ts +6 -0
- package/dist/approval-label.js +45 -0
- package/dist/broker-runtime.d.ts +10 -0
- package/dist/broker-runtime.js +302 -0
- package/dist/broker.d.ts +29 -6
- package/dist/broker.js +141 -141
- package/dist/conversation-details.d.ts +1 -0
- package/dist/conversation-details.js +58 -8
- package/dist/index.d.ts +25 -14
- package/dist/index.js +205 -32
- package/dist/input.d.ts +4 -0
- package/dist/input.js +4 -1
- package/dist/model-catalog.d.ts +55 -0
- package/dist/model-catalog.js +254 -0
- package/dist/model-configuration.d.ts +51 -0
- package/dist/model-configuration.js +212 -0
- package/dist/presentation.js +4 -0
- package/dist/protocol-boundary.d.ts +13 -0
- package/dist/protocol-boundary.js +49 -0
- package/dist/provider.d.ts +19 -1
- package/dist/provider.js +59 -29
- package/dist/recovery-checkpoint.d.ts +6 -0
- package/dist/recovery-checkpoint.js +71 -0
- package/dist/recovery.d.ts +34 -0
- package/dist/recovery.js +52 -0
- package/dist/remote-details.d.ts +3 -0
- package/dist/remote-details.js +3 -3
- package/dist/rpc-namespace.d.ts +3 -0
- package/dist/rpc-namespace.js +50 -0
- package/dist/stage-timing.d.ts +27 -0
- package/dist/stage-timing.js +39 -0
- package/package.json +4 -4
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { BrokerTransport, brokerCommand, brokerId, legacyBrokerId, sshIo, controlJson } from "./broker.js";
|
|
10
|
+
import { CODEX_AGENT } from "./agents.js";
|
|
11
|
+
import { protectedOneShotLayout } from "./host-layout.js";
|
|
12
|
+
import { NeedError } from "./remote.js";
|
|
13
|
+
import { identity, unwrap, validateTarget, sameTarget } from "./provider.js";
|
|
14
|
+
import { StageTiming } from "./stage-timing.js";
|
|
15
|
+
import { recoveryKey } from "./recovery.js";
|
|
16
|
+
const need = (code) => new NeedError(code, '原主机会话暂时无法安全接续,本轮尚未发送', '技术详情中的原会话状态');
|
|
17
|
+
const modern = (entry) => Object.hasOwn(entry, 'instanceId');
|
|
18
|
+
function validEntry(entry) {
|
|
19
|
+
if (!entry || typeof entry.id !== 'string' || !Number.isSafeInteger(entry.pid) || entry.pid < 1 || !['running', 'exited'].includes(entry.state) || ![entry.firstSequence, entry.lastSequence].every(Number.isSafeInteger) || entry.lastSequence < 0 || entry.firstSequence < 1 || entry.firstSequence > entry.lastSequence + 1)
|
|
20
|
+
throw need('BROKER_PROTOCOL_INVALID');
|
|
21
|
+
if (modern(entry) && (typeof entry.instanceId !== 'string' || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(entry.instanceId) || !Number.isSafeInteger(entry.controlMaxBytes) || entry.controlMaxBytes < 1))
|
|
22
|
+
throw need('BROKER_PROTOCOL_INVALID');
|
|
23
|
+
}
|
|
24
|
+
async function leaseWait(ms, signal) {
|
|
25
|
+
if (!Number.isSafeInteger(ms) || ms < 0 || ms > 2147483647)
|
|
26
|
+
throw need('LEASE_POLICY_INVALID');
|
|
27
|
+
signal?.throwIfAborted();
|
|
28
|
+
await new Promise((resolve, reject) => { const clear = () => signal?.removeEventListener('abort', abort); const timer = setTimeout(() => { clear(); resolve(); }, ms); const abort = () => { clearTimeout(timer); clear(); reject(need('ABORTED')); }; signal?.addEventListener('abort', abort, { once: true }); if (signal?.aborted)
|
|
29
|
+
abort(); });
|
|
30
|
+
}
|
|
31
|
+
/** Test ports are explicit; deployed code always supplies the official-profile journal. */
|
|
32
|
+
export function brokerFactory(get, _ask, configuration, provided) {
|
|
33
|
+
const active = new Set();
|
|
34
|
+
const retained = new Map();
|
|
35
|
+
const testJournals = new Map();
|
|
36
|
+
const open = async (target, project, initialRequest) => {
|
|
37
|
+
let request = initialRequest;
|
|
38
|
+
const settings = request.brokerSettings ?? (typeof configuration === 'function' ? configuration() : configuration);
|
|
39
|
+
const agent = request.agent ?? CODEX_AGENT;
|
|
40
|
+
if (!request.sessionId)
|
|
41
|
+
throw need('SESSION_REQUIRED');
|
|
42
|
+
if (request.mode === 'one-shot') {
|
|
43
|
+
const key = 'one-shot/' + identity(target) + '/' + agent.id + '/' + request.sessionId;
|
|
44
|
+
if (active.has(key))
|
|
45
|
+
throw need('TURN_BUSY');
|
|
46
|
+
const layout = protectedOneShotLayout(target);
|
|
47
|
+
active.add(key);
|
|
48
|
+
return { identity: identity(target), cwd: project.path, oneShot: { target, cwd: project.path, ...layout }, dispose: async () => { active.delete(key); } };
|
|
49
|
+
}
|
|
50
|
+
if (![settings.brokerMaxFrameBytes, settings.brokerControlMaxBytes].every(n => Number.isSafeInteger(n) && n > 0))
|
|
51
|
+
throw need('SETTINGS_REQUIRED');
|
|
52
|
+
const key = recoveryKey([identity(target), agent.id, request.sessionId]);
|
|
53
|
+
const scope = recoveryKey([target.host, target.username, target.port, target.proxyCommand, target.knownHostsFile, target.projectRoot, target.session, project.path, agent.id, settings, request.bindingId, request.workspaceId, request.contextSha256]);
|
|
54
|
+
const existing = request.retainProcess ? retained.get(key) : undefined;
|
|
55
|
+
if (existing) {
|
|
56
|
+
if (existing.scope !== scope)
|
|
57
|
+
throw need('SESSION_CONTEXT_MISMATCH');
|
|
58
|
+
if (existing.runtime.broker?.error)
|
|
59
|
+
throw existing.runtime.broker.error;
|
|
60
|
+
existing.update(request);
|
|
61
|
+
existing.runtime.restartNotice = undefined;
|
|
62
|
+
existing.runtime.recoveredSession = undefined;
|
|
63
|
+
return existing.runtime;
|
|
64
|
+
}
|
|
65
|
+
if (active.has(key))
|
|
66
|
+
throw need('TURN_BUSY');
|
|
67
|
+
active.add(key);
|
|
68
|
+
try {
|
|
69
|
+
const journal = request.recoveryJournal ?? provided?.journal?.(key) ?? (provided?.io ? { read: async () => structuredClone(testJournals.get(key)), write: async (value) => { testJournals.set(key, structuredClone(value)); } } : undefined);
|
|
70
|
+
if (!journal)
|
|
71
|
+
throw need('RECOVERY_STORAGE_REQUIRED');
|
|
72
|
+
let state = await journal.read();
|
|
73
|
+
if (state && state.scope !== scope)
|
|
74
|
+
throw need('SESSION_CONTEXT_MISMATCH');
|
|
75
|
+
let argv = provided?.argv;
|
|
76
|
+
if (!provided?.io && !argv) {
|
|
77
|
+
const name = '@ai-workbench/connect';
|
|
78
|
+
const supplied = await import(__rewriteRelativeImportExtension(name));
|
|
79
|
+
if (typeof supplied.sshArgs !== 'function')
|
|
80
|
+
throw need('DEPENDENCY_REQUIRED');
|
|
81
|
+
argv = supplied.sshArgs;
|
|
82
|
+
}
|
|
83
|
+
const ioFor = (t) => provided?.io?.(t) ?? sshIo(t, argv, settings.brokerControlMaxBytes);
|
|
84
|
+
let io = ioFor(target);
|
|
85
|
+
const command = (args) => brokerCommand(target, args);
|
|
86
|
+
let timing = request.timing ?? new StageTiming();
|
|
87
|
+
const validate = async (cleanup = false) => { if (!cleanup)
|
|
88
|
+
request.signal?.throwIfAborted(); await request.validateContext?.(cleanup); if (!cleanup)
|
|
89
|
+
request.signal?.throwIfAborted(); };
|
|
90
|
+
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); };
|
|
91
|
+
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'))
|
|
92
|
+
throw need('BROKER_PROTOCOL_INVALID'); return rows; });
|
|
93
|
+
let rows = await list();
|
|
94
|
+
const id = brokerId(request.sessionId, agent.id), oldId = legacyBrokerId(request.sessionId, agent.id);
|
|
95
|
+
let restarted = false;
|
|
96
|
+
const attachArgs = (entry, after) => ['attach', entry.id, '--after', String(after), '--framed', ...(modern(entry) ? ['--leased', '--if-instance', entry.instanceId] : [])];
|
|
97
|
+
const connectWithLease = async (transport, entry, cleanup = false) => {
|
|
98
|
+
try {
|
|
99
|
+
await transport.connect();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!(error instanceof NeedError) || error.code !== 'ATTACH_BUSY' || !modern(entry))
|
|
103
|
+
throw error;
|
|
104
|
+
const current = (await list(cleanup)).find(row => row.id === entry.id);
|
|
105
|
+
if (!current || current.instanceId !== entry.instanceId)
|
|
106
|
+
throw need('SESSION_INSTANCE_MISMATCH');
|
|
107
|
+
validEntry(current);
|
|
108
|
+
const policy = current.lifecycle;
|
|
109
|
+
if (!policy || typeof policy.enabled !== 'boolean' || typeof policy.attached !== 'boolean')
|
|
110
|
+
throw need('LEASE_POLICY_INVALID');
|
|
111
|
+
if (policy.attached && !policy.enabled)
|
|
112
|
+
throw need('SESSION_IN_USE');
|
|
113
|
+
const remaining = policy.attached ? policy.remainingLeaseMs : 0;
|
|
114
|
+
if (remaining === undefined || remaining === null || !Number.isSafeInteger(remaining) || remaining < 0)
|
|
115
|
+
throw need('LEASE_POLICY_INVALID');
|
|
116
|
+
await report('WAITING_PRIOR_CLIENT_LEASE', { brokerId: entry.id, instanceId: entry.instanceId, remainingLeaseMs: remaining }, cleanup);
|
|
117
|
+
await leaseWait(remaining, cleanup ? undefined : request.signal);
|
|
118
|
+
await validate(cleanup);
|
|
119
|
+
try {
|
|
120
|
+
await transport.connect();
|
|
121
|
+
}
|
|
122
|
+
catch (next) {
|
|
123
|
+
if (next instanceof NeedError && next.code === 'ATTACH_BUSY')
|
|
124
|
+
throw need('SESSION_IN_USE');
|
|
125
|
+
throw next;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const claim = async (entry, cleanup = false) => {
|
|
130
|
+
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))) });
|
|
131
|
+
try {
|
|
132
|
+
await connectWithLease(probe, entry, cleanup);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (!(entry.state === 'exited' && error instanceof NeedError && error.code === 'AGENT_EXITED')) {
|
|
136
|
+
probe.close();
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return probe;
|
|
141
|
+
};
|
|
142
|
+
const stop = async (entry, code, owned, cleanup = false, freshOwned = false) => {
|
|
143
|
+
validEntry(entry);
|
|
144
|
+
if (!modern(entry))
|
|
145
|
+
throw need('BROKER_CAPABILITY_REQUIRED');
|
|
146
|
+
if (!owned?.online && !freshOwned) {
|
|
147
|
+
const current = (await list(cleanup)).find(row => row.id === entry.id);
|
|
148
|
+
if (!current) {
|
|
149
|
+
await report('SESSION_ALREADY_ABSENT', { brokerId: entry.id, instanceId: entry.instanceId ?? null }, cleanup);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
validEntry(current);
|
|
153
|
+
if (modern(current) !== modern(entry) || current.instanceId !== entry.instanceId)
|
|
154
|
+
throw need('SESSION_INSTANCE_MISMATCH');
|
|
155
|
+
entry = current;
|
|
156
|
+
}
|
|
157
|
+
const probe = owned?.online || freshOwned ? owned : await claim(entry, cleanup);
|
|
158
|
+
try {
|
|
159
|
+
await report(code, { brokerId: entry.id, ...(entry.instanceId ? { instanceId: entry.instanceId } : {}), observedPid: entry.pid, legacySessionSlot: !modern(entry) }, cleanup);
|
|
160
|
+
if (entry.state === 'running' && !probe.online && !freshOwned)
|
|
161
|
+
throw need('MIGRATION_CLAIM_LOST');
|
|
162
|
+
await validate(cleanup);
|
|
163
|
+
const ack = controlJson(await io.run(command(['kill', entry.id, ...(entry.instanceId ? ['--if-instance', entry.instanceId] : [])]), cleanup ? undefined : request.signal));
|
|
164
|
+
if (ack?.type !== 'killed' || ack.id !== entry.id || modern(entry) && ack.instanceId !== entry.instanceId)
|
|
165
|
+
throw need('REMOTE_CLEANUP_UNCONFIRMED');
|
|
166
|
+
if ((await list(cleanup)).some(row => row.id === entry.id))
|
|
167
|
+
throw need('REMOTE_CLEANUP_UNCONFIRMED');
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
probe.close();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const legacy = rows.find(row => row.id === oldId);
|
|
174
|
+
if (legacy)
|
|
175
|
+
await report('PRE_0410_TEST_SESSION_NOT_RESUMED', { brokerId: legacy.id, observedPid: legacy.pid, state: legacy.state, instanceId: legacy.instanceId ?? null, reason: 'OLD_TEST_SESSION_COORDINATOR_CLEANUP', decision: '2026-10-22-00:05', approvalReplies: 0 });
|
|
176
|
+
let entry = rows.find(row => row.id === id);
|
|
177
|
+
if (entry)
|
|
178
|
+
validEntry(entry);
|
|
179
|
+
if (legacy && !entry)
|
|
180
|
+
restarted = true;
|
|
181
|
+
let transport;
|
|
182
|
+
let resume;
|
|
183
|
+
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) {
|
|
184
|
+
const dirty = { version: 1, scope, brokerId: id, instanceId: entry.instanceId, pid: entry.pid, phase: 'dirty' };
|
|
185
|
+
await journal.write(dirty);
|
|
186
|
+
state = dirty;
|
|
187
|
+
} await validate(); }, ...(recovery ? { recoverySessionId: recovery.sessionId, onRecoveryUpdate: (message) => { const params = message.params; if (params.update?.sessionUpdate === 'config_option_update' && recovery.modelConfiguration) {
|
|
188
|
+
if (!Array.isArray(params.update.configOptions))
|
|
189
|
+
throw need('RECOVERY_MODEL_CONFIGURATION_INVALID');
|
|
190
|
+
(recovery.configUpdates ??= []).push(structuredClone(params.update.configOptions));
|
|
191
|
+
} request.detailChannel?.record({ kind: 'session', severity: 'info', code: 'ACP_IDLE_UPDATE', summary: '恢复期间的远端状态轨迹', method: 'session/update', payload: message.params }); } } : {}), attach: after => io.attach(command(attachArgs(entry, after))), onDisconnect: async () => {
|
|
192
|
+
const api = get();
|
|
193
|
+
if (!provided?.io) {
|
|
194
|
+
if (!api)
|
|
195
|
+
throw need('DEPENDENCY_REQUIRED');
|
|
196
|
+
const next = unwrap(await api.getSshTarget());
|
|
197
|
+
validateTarget(next);
|
|
198
|
+
if (!sameTarget(next, target))
|
|
199
|
+
throw need('SESSION_CONTEXT_MISMATCH');
|
|
200
|
+
io = ioFor(next);
|
|
201
|
+
}
|
|
202
|
+
await validate();
|
|
203
|
+
await connectWithLease(transport, entry);
|
|
204
|
+
} });
|
|
205
|
+
if (entry) {
|
|
206
|
+
if (!modern(entry))
|
|
207
|
+
throw need('BROKER_CAPABILITY_REQUIRED');
|
|
208
|
+
const clean = 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' && await request.verifyRecovery?.() !== false;
|
|
209
|
+
if (clean) {
|
|
210
|
+
transport = build(entry, state.cursor, state.acp);
|
|
211
|
+
try {
|
|
212
|
+
await timing.run('broker-attach', () => connectWithLease(transport, entry));
|
|
213
|
+
await transport.replayThrough(entry.lastSequence, request.signal);
|
|
214
|
+
await request.detailChannel?.flush();
|
|
215
|
+
await validate();
|
|
216
|
+
resume = state.acp;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
transport.close();
|
|
220
|
+
if (error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH', 'ABORTED'].includes(error.code))
|
|
221
|
+
throw error;
|
|
222
|
+
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 });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!resume) {
|
|
226
|
+
await stop(entry, 'SESSION_CHECKPOINT_UNRECOVERABLE', transport);
|
|
227
|
+
restarted = true;
|
|
228
|
+
entry = undefined;
|
|
229
|
+
transport = undefined;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!entry) {
|
|
233
|
+
if (request.resumeSessionId) {
|
|
234
|
+
restarted = true;
|
|
235
|
+
await report('SESSION_BROKER_ABSENT', { brokerId: id });
|
|
236
|
+
}
|
|
237
|
+
await validate();
|
|
238
|
+
state = { version: 1, scope, brokerId: id, phase: 'dirty' };
|
|
239
|
+
await journal.write(state);
|
|
240
|
+
const started = await timing.run('broker-start', async () => controlJson(await io.run(command(['start', id, '--cwd', project.path, '--settings', target.session.settingsPath, '--', agent.command]), request.signal)));
|
|
241
|
+
if (started?.protocolVersion !== 1 || started.id !== id || typeof started.socket !== 'string' || !started.instanceId || !Number.isSafeInteger(started.controlMaxBytes) || !Number.isSafeInteger(started.pid) || started.pid < 1)
|
|
242
|
+
throw need('BROKER_START_UNCONFIRMED');
|
|
243
|
+
entry = { ...started, state: 'running', firstSequence: 1, lastSequence: 0 };
|
|
244
|
+
state = { ...state, instanceId: entry.instanceId, pid: entry.pid };
|
|
245
|
+
await journal.write(state);
|
|
246
|
+
validEntry(entry);
|
|
247
|
+
transport = build(entry, 0);
|
|
248
|
+
const opening = transport;
|
|
249
|
+
const aborted = () => opening.fail(need('ABORTED'));
|
|
250
|
+
request.signal?.addEventListener('abort', aborted, { once: true });
|
|
251
|
+
try {
|
|
252
|
+
request.signal?.throwIfAborted();
|
|
253
|
+
await timing.run('broker-attach', () => connectWithLease(opening, entry));
|
|
254
|
+
await validate();
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (!(error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH'].includes(error.code)))
|
|
258
|
+
await stop(entry, 'SESSION_OPEN_FAILED', opening, true, true);
|
|
259
|
+
opening.close();
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
request.signal?.removeEventListener('abort', aborted);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const ownedEntry = entry, ownedTransport = transport;
|
|
267
|
+
let disposed = false;
|
|
268
|
+
let disposal;
|
|
269
|
+
const runtime = { identity: identity(target), cwd: project.path, broker: ownedTransport, acpTransport: ownedTransport,
|
|
270
|
+
...(resume ? { recoveredSession: resume } : {}), ...(restarted ? { restartNotice: '上次会话无法接回,已新建会话;本次从你的这条请求继续。', restartInputIds: request.currentInputIds } : {}),
|
|
271
|
+
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 {
|
|
272
|
+
await journal.write(clean);
|
|
273
|
+
state = clean;
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
state = undefined;
|
|
277
|
+
throw error;
|
|
278
|
+
} }); if (!saved)
|
|
279
|
+
await report('SESSION_CHECKPOINT_NOT_CLEAN', { brokerId: id, instanceId: ownedEntry.instanceId }); return saved; },
|
|
280
|
+
detach: async () => { ownedTransport.close(); active.delete(key); retained.delete(key); },
|
|
281
|
+
dispose: () => disposal ??= (async () => { if (disposed)
|
|
282
|
+
return; try {
|
|
283
|
+
await stop(ownedEntry, 'SESSION_ENDED', ownedTransport, true);
|
|
284
|
+
await journal.write({ version: 1, phase: 'ended', scope, brokerId: id, instanceId: ownedEntry.instanceId, pid: ownedEntry.pid });
|
|
285
|
+
disposed = true;
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
ownedTransport.close();
|
|
289
|
+
active.delete(key);
|
|
290
|
+
retained.delete(key);
|
|
291
|
+
} })(), };
|
|
292
|
+
if (request.retainProcess)
|
|
293
|
+
retained.set(key, { runtime, scope, update: next => { request = next; timing = next.timing ?? new StageTiming(); } });
|
|
294
|
+
return runtime;
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
active.delete(key);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
return open;
|
|
302
|
+
}
|
package/dist/broker.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
2
|
import { NeedError, type Need } from './remote.ts';
|
|
3
|
-
import type
|
|
4
|
-
import { type ConnectApi, type ConnectTarget, type Project, type Runtime, type RuntimeRequest } from './provider.ts';
|
|
3
|
+
import { type ConnectTarget } from './provider.ts';
|
|
5
4
|
export type Attachment = ChildProcessWithoutNullStreams;
|
|
6
5
|
export interface BrokerOptions {
|
|
7
6
|
maxFrameBytes: number;
|
|
7
|
+
initialSequence?: number;
|
|
8
|
+
instanceId?: string;
|
|
9
|
+
leased?: boolean;
|
|
10
|
+
controlMaxBytes?: number;
|
|
11
|
+
maxMessageBytes?: number;
|
|
12
|
+
beforeInput?: () => Promise<void>;
|
|
13
|
+
recoverySessionId?: string;
|
|
14
|
+
onRecoveryUpdate?: (message: Record<string, unknown>) => void;
|
|
15
|
+
discardOutput?: boolean;
|
|
8
16
|
attach: (after: number) => Promise<Attachment>;
|
|
9
17
|
onNeed?: (need: Need) => void;
|
|
10
18
|
onDisconnect?: () => Promise<void>;
|
|
@@ -27,12 +35,27 @@ export declare class BrokerTransport {
|
|
|
27
35
|
private approvalSent;
|
|
28
36
|
private unconfirmed;
|
|
29
37
|
private failure?;
|
|
38
|
+
private heartbeat?;
|
|
39
|
+
private boundary?;
|
|
40
|
+
private recovering;
|
|
41
|
+
private promptSent;
|
|
42
|
+
private preparingInputs;
|
|
43
|
+
private inputTail;
|
|
44
|
+
private exclusive;
|
|
30
45
|
constructor(options: BrokerOptions);
|
|
31
46
|
get online(): boolean;
|
|
32
47
|
get generation(): number;
|
|
33
48
|
get signal(): AbortSignal;
|
|
34
49
|
get lastSequence(): number;
|
|
35
50
|
get error(): NeedError | undefined;
|
|
51
|
+
get promptInFlight(): boolean;
|
|
52
|
+
processedMessage(message: Record<string, unknown>): void;
|
|
53
|
+
checkpointSequence(): number | null;
|
|
54
|
+
commitCheckpoint(commit: (cursor: number) => Promise<void>): Promise<boolean>;
|
|
55
|
+
finishRecovery(): void;
|
|
56
|
+
replayThrough(sequence: number, signal?: AbortSignal): Promise<void>;
|
|
57
|
+
private stopHeartbeat;
|
|
58
|
+
private recoveryMessage;
|
|
36
59
|
authorizeResponse(id: string | number | null, generation: number): void;
|
|
37
60
|
/** Called only by the ACP protocol layer after a matching result or current-turn output. */
|
|
38
61
|
acknowledge(method: string): void;
|
|
@@ -47,6 +70,7 @@ export declare class BrokerTransport {
|
|
|
47
70
|
connect(): Promise<void>;
|
|
48
71
|
private attach;
|
|
49
72
|
private write;
|
|
73
|
+
private send;
|
|
50
74
|
}
|
|
51
75
|
export type SshArgv = (target: ConnectTarget) => string[];
|
|
52
76
|
export interface BrokerIo {
|
|
@@ -56,12 +80,11 @@ export interface BrokerIo {
|
|
|
56
80
|
export declare function controlNeed(stderr: string): NeedError;
|
|
57
81
|
export declare function sshIo(target: ConnectTarget, argv: SshArgv, maxControlBytes: number): BrokerIo;
|
|
58
82
|
export declare function brokerCommand(t: ConnectTarget, args: string[]): string;
|
|
83
|
+
export declare function legacyBrokerId(sessionId: string, agentId?: string): string;
|
|
59
84
|
export declare function brokerId(sessionId: string, agentId?: string): string;
|
|
85
|
+
export declare function controlJson(text: string): unknown;
|
|
60
86
|
export interface BrokerSettings {
|
|
61
87
|
brokerMaxFrameBytes?: number;
|
|
62
88
|
brokerControlMaxBytes?: number;
|
|
63
89
|
}
|
|
64
|
-
export
|
|
65
|
-
argv?: SshArgv;
|
|
66
|
-
io?: (target: ConnectTarget) => BrokerIo;
|
|
67
|
-
}): (target: ConnectTarget, project: Project, request: RuntimeRequest) => Promise<Runtime>;
|
|
90
|
+
export { brokerFactory } from './broker-runtime.ts';
|