@ai-workbench/remote-agent 0.8.21 → 0.8.24
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 +28 -155
- package/dist/acp.d.ts +20 -15
- package/dist/acp.js +197 -224
- package/dist/agents.d.ts +0 -1
- package/dist/agents.js +2 -2
- package/dist/broker-runtime.d.ts +3 -5
- package/dist/broker-runtime.js +45 -342
- package/dist/broker.d.ts +6 -31
- package/dist/broker.js +42 -113
- package/dist/catalog-ssh.d.ts +2 -31
- package/dist/catalog-ssh.js +9 -39
- package/dist/claude-local.d.ts +1 -1
- package/dist/claude-local.js +40 -105
- package/dist/client.js +34 -127
- package/dist/close-diagnostics.d.ts +6 -20
- package/dist/close-diagnostics.js +13 -57
- package/dist/connect-contract.d.ts +4 -8
- package/dist/conversation-details.d.ts +0 -29
- package/dist/conversation-details.js +8 -99
- package/dist/employee-endpoint.d.ts +1 -3
- package/dist/employee-endpoint.js +8 -39
- package/dist/employee-exec.d.ts +4 -3
- package/dist/employee-exec.js +25 -13
- package/dist/employee-info.d.ts +5 -1
- package/dist/employee-info.js +16 -5
- package/dist/employee-location.js +3 -4
- package/dist/employee-mcp.js +44 -69
- package/dist/employee-native.d.ts +1 -3
- package/dist/employee-native.js +6 -27
- package/dist/employee-presentation.d.ts +0 -5
- package/dist/employee-presentation.js +0 -15
- package/dist/employee-provider.d.ts +3 -11
- package/dist/employee-provider.js +69 -205
- package/dist/employee-reasons.js +1 -5
- package/dist/employee-shell.d.ts +2 -1
- package/dist/employee-shell.js +10 -13
- package/dist/employee-ssh.d.ts +1 -3
- package/dist/employee-ssh.js +13 -16
- package/dist/employee-tools.d.ts +5 -5
- package/dist/failure-evidence.d.ts +2 -10
- package/dist/failure-evidence.js +12 -61
- package/dist/host-workspace.d.ts +4 -2
- package/dist/host-workspace.js +1 -1
- package/dist/index.d.ts +12 -42
- package/dist/index.js +203 -471
- package/dist/input.js +2 -3
- package/dist/local-user-turn.js +5 -6
- package/dist/mapping.d.ts +0 -3
- package/dist/mapping.js +0 -34
- package/dist/model-catalog.d.ts +2 -41
- package/dist/model-catalog.js +24 -282
- package/dist/model-configuration.d.ts +0 -3
- package/dist/model-configuration.js +8 -47
- package/dist/model-presentation.d.ts +1 -0
- package/dist/model-presentation.js +2 -2
- package/dist/permissions.d.ts +15 -0
- package/dist/permissions.js +30 -0
- package/dist/preflight-diagnostic.js +11 -22
- package/dist/preflight-payload.d.ts +1 -1
- package/dist/preflight-payload.js +2 -2
- package/dist/presentation.d.ts +0 -1
- package/dist/presentation.js +21 -42
- package/dist/provider.d.ts +12 -48
- package/dist/provider.js +18 -106
- package/dist/readiness.js +1 -7
- package/dist/remote-details.d.ts +41 -17
- package/dist/remote-details.js +84 -381
- package/dist/workspace-source.d.ts +1 -25
- package/dist/workspace-source.js +1 -138
- package/package.json +11 -12
- package/dist/catalog-diagnostics.d.ts +0 -16
- package/dist/catalog-diagnostics.js +0 -108
- package/dist/host-layout.d.ts +0 -8
- package/dist/host-layout.js +0 -9
- package/dist/job.d.ts +0 -17
- package/dist/job.js +0 -57
- package/dist/model-defaults.d.ts +0 -29
- package/dist/model-defaults.js +0 -62
- package/dist/oneshot.d.ts +0 -13
- package/dist/oneshot.js +0 -78
- package/dist/protocol-boundary.d.ts +0 -13
- package/dist/protocol-boundary.js +0 -49
- package/dist/provider-locale.d.ts +0 -19
- package/dist/provider-locale.js +0 -73
- package/dist/recovery-checkpoint.d.ts +0 -6
- package/dist/recovery-checkpoint.js +0 -71
- package/dist/recovery.d.ts +0 -51
- package/dist/recovery.js +0 -61
- package/dist/sdk-ssh-wrapper.d.ts +0 -2
- package/dist/sdk-ssh-wrapper.js +0 -34
- package/dist/stage-timing.d.ts +0 -31
- package/dist/stage-timing.js +0 -61
- package/dist/workspace-notice.d.ts +0 -47
- package/dist/workspace-notice.js +0 -273
- package/dist/workspace-source-confirmation.d.ts +0 -4
- package/dist/workspace-source-confirmation.js +0 -12
package/dist/agents.d.ts
CHANGED
package/dist/agents.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const CODEX_AGENT = Object.freeze({ id: 'codex-remote', name: Object.freeze({ zh: 'Codex(远程主机)', en: 'Codex (Remote Host)' }), command: '/usr/local/aw-host/bin/codex-acp', loginProvider: 'codex'
|
|
2
|
-
export const CLAUDE_AGENT = Object.freeze({ id: 'claude-code-workbench', name: Object.freeze({ zh: 'Claude(远程主机)', en: 'Claude (Remote Host)' }), command: '/usr/local/aw-host/bin/claude-agent-acp', loginProvider: 'claude'
|
|
1
|
+
export const CODEX_AGENT = Object.freeze({ id: 'codex-remote', name: Object.freeze({ zh: 'Codex(远程主机)', en: 'Codex (Remote Host)' }), command: '/usr/local/aw-host/bin/codex-acp', loginProvider: 'codex' });
|
|
2
|
+
export const CLAUDE_AGENT = Object.freeze({ id: 'claude-code-workbench', name: Object.freeze({ zh: 'Claude(远程主机)', en: 'Claude (Remote Host)' }), command: '/usr/local/aw-host/bin/claude-agent-acp', loginProvider: 'claude' });
|
|
3
3
|
export const AGENTS = Object.freeze([CODEX_AGENT, CLAUDE_AGENT]);
|
|
4
4
|
export function agentForProvider(id) { return AGENTS.find(agent => agent.id === id); }
|
package/dist/broker-runtime.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { type SshArgv, type BrokerIo, type BrokerSettings } from './broker.ts';
|
|
2
2
|
import { type ConnectApi, type ConnectTarget, type Project, type Runtime, type RuntimeRequest } from './provider.ts';
|
|
3
3
|
import type { Interaction } from './interaction.ts';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export declare function brokerFactory(get: () => ConnectApi | undefined, _ask: Interaction | undefined, configuration: BrokerSettings | (() => BrokerSettings), provided?: {
|
|
4
|
+
/** Each invocation owns a new agent. Context resumes by native session ID, never by reattaching a broker. */
|
|
5
|
+
export declare function brokerFactory(_get: () => ConnectApi | undefined, _ask: Interaction | undefined, configuration: BrokerSettings | (() => BrokerSettings), provided?: {
|
|
7
6
|
argv?: SshArgv;
|
|
8
7
|
io?: (target: ConnectTarget) => BrokerIo;
|
|
9
|
-
|
|
10
|
-
}): (target: ConnectTarget, project: Project, initialRequest: RuntimeRequest) => Promise<Runtime>;
|
|
8
|
+
}): (target: ConnectTarget, project: Project, request: RuntimeRequest) => Promise<Runtime>;
|
package/dist/broker-runtime.js
CHANGED
|
@@ -6,355 +6,58 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { CloseDiagnostics } from "./close-diagnostics.js";
|
|
11
|
+
import { BrokerTransport, brokerCommand, brokerId, sshIo, controlJson } from "./broker.js";
|
|
11
12
|
import { CODEX_AGENT } from "./agents.js";
|
|
12
|
-
import { protectedOneShotLayout } from "./host-layout.js";
|
|
13
13
|
import { NeedError } from "./remote.js";
|
|
14
|
-
import { identity
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw need('BROKER_PROTOCOL_INVALID');
|
|
24
|
-
}
|
|
25
|
-
async function leaseWait(ms, signal) {
|
|
26
|
-
if (!Number.isSafeInteger(ms) || ms < 0 || ms > 2147483647)
|
|
27
|
-
throw need('LEASE_POLICY_INVALID');
|
|
28
|
-
signal?.throwIfAborted();
|
|
29
|
-
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)
|
|
30
|
-
abort(); });
|
|
31
|
-
}
|
|
32
|
-
/** Test ports are explicit; deployed code always supplies the official-profile journal. */
|
|
33
|
-
export function brokerFactory(get, _ask, configuration, provided) {
|
|
34
|
-
const active = new Set();
|
|
35
|
-
const retained = new Map();
|
|
36
|
-
const testJournals = new Map();
|
|
37
|
-
const open = async (target, project, initialRequest) => {
|
|
38
|
-
let request = initialRequest;
|
|
39
|
-
const settings = request.brokerSettings ?? (typeof configuration === 'function' ? configuration() : configuration);
|
|
40
|
-
const agent = request.agent ?? CODEX_AGENT;
|
|
41
|
-
if (!request.sessionId)
|
|
42
|
-
throw need('SESSION_REQUIRED');
|
|
43
|
-
const employeeScope = request.employeeScope;
|
|
44
|
-
if (employeeScope && (request.mode !== 'session' || request.resumeSessionId || request.recoveryCheckpointId))
|
|
45
|
-
throw need('EMPLOYEE_OPERATION_UNSUPPORTED');
|
|
46
|
-
if (request.mode === 'one-shot') {
|
|
47
|
-
const key = 'one-shot/' + identity(target) + '/' + agent.id + '/' + request.sessionId;
|
|
48
|
-
if (active.has(key))
|
|
49
|
-
throw need('TURN_BUSY');
|
|
50
|
-
const layout = protectedOneShotLayout(target);
|
|
51
|
-
active.add(key);
|
|
52
|
-
return { identity: identity(target), cwd: project.path, oneShot: { target, cwd: project.path, ...layout }, dispose: async () => { active.delete(key); } };
|
|
14
|
+
import { identity } from "./provider.js";
|
|
15
|
+
/** Each invocation owns a new agent. Context resumes by native session ID, never by reattaching a broker. */
|
|
16
|
+
export function brokerFactory(_get, _ask, configuration, provided) {
|
|
17
|
+
return async (target, project, request) => {
|
|
18
|
+
const agent = request.agent ?? CODEX_AGENT, settings = request.brokerSettings ?? (typeof configuration === 'function' ? configuration() : configuration);
|
|
19
|
+
let argv = provided?.argv;
|
|
20
|
+
if (!provided?.io && !argv) {
|
|
21
|
+
const module = '@ai-workbench/connect';
|
|
22
|
+
argv = (await import(__rewriteRelativeImportExtension(module))).sshArgs;
|
|
53
23
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
existing.update(request);
|
|
82
|
-
existing.runtime.restartNotice = undefined;
|
|
83
|
-
existing.runtime.restartInputIds = undefined;
|
|
84
|
-
existing.runtime.freshSessionConfirmed = undefined;
|
|
85
|
-
existing.runtime.recoveredSession = undefined;
|
|
86
|
-
return existing.runtime;
|
|
87
|
-
}
|
|
88
|
-
if (active.has(key))
|
|
89
|
-
throw need('TURN_BUSY');
|
|
90
|
-
active.add(key);
|
|
24
|
+
const io = provided?.io?.(target) ?? sshIo(target, argv, settings.brokerControlMaxBytes);
|
|
25
|
+
const command = (args) => brokerCommand(target, args);
|
|
26
|
+
const diagnostics = request.closeDiagnostics ?? new CloseDiagnostics(detail => request.detailChannel?.record(detail));
|
|
27
|
+
const id = brokerId(request.sessionId + '/' + randomUUID(), agent.id);
|
|
28
|
+
const startCommand = command(['start', id, '--cwd', project.path, '--settings', target.session.settingsPath, '--', agent.command]);
|
|
29
|
+
// environments.toml overrides CODEX_EXEC_SERVER_URL. Refuse that host default rather
|
|
30
|
+
// than allowing a local-workspace resume to select a host executor.
|
|
31
|
+
const employeeStart = request.employeeScope && agent.loginProvider === 'codex';
|
|
32
|
+
const guardedStart = employeeStart ? `if [ -e "\${CODEX_HOME:-$HOME/.codex}/environments.toml" ] || [ -L "\${CODEX_HOME:-$HOME/.codex}/environments.toml" ]; then printf '%s\\n' '{"code":"AW_EMPLOYEE_ENVIRONMENTS_CONFIG_UNSUPPORTED"}'; else CODEX_EXEC_SERVER_URL=none ${startCommand}; fi` : startCommand;
|
|
33
|
+
const started = controlJson(await io.run(guardedStart, request.signal));
|
|
34
|
+
if (started.code === 'AW_EMPLOYEE_ENVIRONMENTS_CONFIG_UNSUPPORTED')
|
|
35
|
+
throw new NeedError(started.code, '主机的环境配置会覆盖本机执行位置,本轮已停止;请由管理员移开该配置后在原会话重试。', '主机 CODEX_HOME/environments.toml 不存在');
|
|
36
|
+
if (started.protocolVersion !== 1 || started.id !== id || !started.instanceId)
|
|
37
|
+
throw new NeedError('BROKER_PROTOCOL_INVALID', '主机会话未能启动,请恢复连接后再试。', '有效的主机会话启动回应');
|
|
38
|
+
const channel = new BrokerTransport({ observeClose: diagnostics.capture(), maxFrameBytes: settings.brokerMaxFrameBytes, instanceId: started.instanceId, leased: true, controlMaxBytes: started.controlMaxBytes, attach: () => io.attach(command(['attach', id, '--after', '0', '--framed', '--leased', '--if-instance', started.instanceId])) });
|
|
39
|
+
let ended = false;
|
|
40
|
+
const dispose = async (cause = { layer: 'broker', initiator: 'broker.dispose', reason: 'BROKER_DISPOSE', action: 'dispose' }) => {
|
|
41
|
+
if (ended)
|
|
42
|
+
return;
|
|
43
|
+
ended = true;
|
|
44
|
+
diagnostics.record(cause);
|
|
45
|
+
channel.close(cause);
|
|
46
|
+
void io.run(command(['kill', id, '--if-instance', started.instanceId])).catch(() => console.warn('[remote-agent] 旧主机会话暂未清理,将由主机自动回收'));
|
|
47
|
+
};
|
|
48
|
+
const aborted = () => channel.fail(new NeedError('ABORTED', '本轮已取消。', '确认主机状态'), { layer: 'broker', initiator: 'broker.open-signal', reason: 'ABORTED', action: 'abort' });
|
|
49
|
+
request.signal?.addEventListener('abort', aborted, { once: true });
|
|
91
50
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
throw need('RECOVERY_STORAGE_REQUIRED');
|
|
95
|
-
let state = await journal.read();
|
|
96
|
-
const legacyContextScope = !!state && state.scope !== scope && state.scope === previousContextScope;
|
|
97
|
-
if (state && state.scope !== scope && !legacyContextScope)
|
|
98
|
-
throw need(agent.loginProvider === 'codex' ? 'RECOVERY_CONTEXT_POLICY_UNSUPPORTED' : 'SESSION_CONTEXT_MISMATCH');
|
|
99
|
-
if (legacyContextScope && state?.phase !== 'clean')
|
|
100
|
-
throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
|
|
101
|
-
let argv = provided?.argv;
|
|
102
|
-
if (!provided?.io && !argv) {
|
|
103
|
-
const name = '@ai-workbench/connect';
|
|
104
|
-
const supplied = await import(__rewriteRelativeImportExtension(name));
|
|
105
|
-
if (typeof supplied.sshArgs !== 'function')
|
|
106
|
-
throw need('DEPENDENCY_REQUIRED');
|
|
107
|
-
argv = supplied.sshArgs;
|
|
108
|
-
}
|
|
109
|
-
const ioFor = (t) => provided?.io?.(t) ?? sshIo(t, argv, settings.brokerControlMaxBytes);
|
|
110
|
-
let io = ioFor(target);
|
|
111
|
-
const command = (args) => brokerCommand(target, args);
|
|
112
|
-
let timing = request.timing ?? new StageTiming();
|
|
113
|
-
const validate = async (cleanup = false) => { if (!cleanup)
|
|
114
|
-
request.signal?.throwIfAborted(); await request.validateContext?.(cleanup); if (!cleanup)
|
|
115
|
-
request.signal?.throwIfAborted(); };
|
|
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); };
|
|
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'))
|
|
118
|
-
throw need('BROKER_PROTOCOL_INVALID'); return rows; });
|
|
119
|
-
const detailSink = request.detailChannel;
|
|
120
|
-
const closeDiagnostics = request.closeDiagnostics ?? new CloseDiagnostics(event => detailSink?.record(event));
|
|
121
|
-
let rows = await list();
|
|
122
|
-
const id = brokerId(request.sessionId, employeeScope ? agent.id + '/employee/' + employeeScope : agent.id), oldId = legacyBrokerId(request.sessionId, agent.id);
|
|
123
|
-
let restarted = !!request.requireFreshSession;
|
|
124
|
-
const attachArgs = (entry, after) => ['attach', entry.id, '--after', String(after), '--framed', ...(modern(entry) ? ['--leased', '--if-instance', entry.instanceId] : [])];
|
|
125
|
-
const connectWithLease = async (transport, entry, cleanup = false) => {
|
|
126
|
-
try {
|
|
127
|
-
await transport.connect();
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
if (!(error instanceof NeedError) || error.code !== 'ATTACH_BUSY' || !modern(entry))
|
|
131
|
-
throw error;
|
|
132
|
-
const current = (await list(cleanup)).find(row => row.id === entry.id);
|
|
133
|
-
if (!current || current.instanceId !== entry.instanceId)
|
|
134
|
-
throw need('SESSION_INSTANCE_MISMATCH');
|
|
135
|
-
validEntry(current);
|
|
136
|
-
const policy = current.lifecycle;
|
|
137
|
-
if (!policy || typeof policy.enabled !== 'boolean' || typeof policy.attached !== 'boolean')
|
|
138
|
-
throw need('LEASE_POLICY_INVALID');
|
|
139
|
-
if (policy.attached && !policy.enabled)
|
|
140
|
-
throw need('SESSION_IN_USE');
|
|
141
|
-
const remaining = policy.attached ? policy.remainingLeaseMs : 0;
|
|
142
|
-
if (remaining === undefined || remaining === null || !Number.isSafeInteger(remaining) || remaining < 0)
|
|
143
|
-
throw need('LEASE_POLICY_INVALID');
|
|
144
|
-
await report('WAITING_PRIOR_CLIENT_LEASE', { brokerId: entry.id, instanceId: entry.instanceId, remainingLeaseMs: remaining }, cleanup);
|
|
145
|
-
await leaseWait(remaining, cleanup ? undefined : request.signal);
|
|
146
|
-
await validate(cleanup);
|
|
147
|
-
try {
|
|
148
|
-
await transport.connect();
|
|
149
|
-
}
|
|
150
|
-
catch (next) {
|
|
151
|
-
if (next instanceof NeedError && next.code === 'ATTACH_BUSY')
|
|
152
|
-
throw need('SESSION_IN_USE');
|
|
153
|
-
throw next;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
const claim = async (entry, cleanup = false) => {
|
|
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))) });
|
|
159
|
-
try {
|
|
160
|
-
await connectWithLease(probe, entry, cleanup);
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
if (!(entry.state === 'exited' && error instanceof NeedError && error.code === 'AGENT_EXITED')) {
|
|
164
|
-
probe.close({ layer: 'broker', initiator: 'broker.probe-close', reason: 'BROKER_PROBE_FINISHED', action: 'close' });
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return probe;
|
|
169
|
-
};
|
|
170
|
-
const stop = async (entry, code, owned, cleanup = false, freshOwned = false) => {
|
|
171
|
-
validEntry(entry);
|
|
172
|
-
if (!modern(entry))
|
|
173
|
-
throw need('BROKER_CAPABILITY_REQUIRED');
|
|
174
|
-
if (!owned?.online && !freshOwned) {
|
|
175
|
-
const current = (await list(cleanup)).find(row => row.id === entry.id);
|
|
176
|
-
if (!current) {
|
|
177
|
-
await report('SESSION_ALREADY_ABSENT', { brokerId: entry.id, instanceId: entry.instanceId ?? null }, cleanup);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
validEntry(current);
|
|
181
|
-
if (modern(current) !== modern(entry) || current.instanceId !== entry.instanceId)
|
|
182
|
-
throw need('SESSION_INSTANCE_MISMATCH');
|
|
183
|
-
entry = current;
|
|
184
|
-
}
|
|
185
|
-
const probe = owned?.online || freshOwned ? owned : await claim(entry, cleanup);
|
|
186
|
-
try {
|
|
187
|
-
await report(code, { brokerId: entry.id, ...(entry.instanceId ? { instanceId: entry.instanceId } : {}), observedPid: entry.pid, legacySessionSlot: !modern(entry) }, cleanup);
|
|
188
|
-
if (entry.state === 'running' && !probe.online && !freshOwned)
|
|
189
|
-
throw need('MIGRATION_CLAIM_LOST');
|
|
190
|
-
await validate(cleanup);
|
|
191
|
-
const ack = controlJson(await io.run(command(['kill', entry.id, ...(entry.instanceId ? ['--if-instance', entry.instanceId] : [])]), cleanup ? undefined : request.signal));
|
|
192
|
-
if (ack?.type !== 'killed' || ack.id !== entry.id || modern(entry) && ack.instanceId !== entry.instanceId)
|
|
193
|
-
throw need('REMOTE_CLEANUP_UNCONFIRMED');
|
|
194
|
-
if ((await list(cleanup)).some(row => row.id === entry.id))
|
|
195
|
-
throw need('REMOTE_CLEANUP_UNCONFIRMED');
|
|
196
|
-
}
|
|
197
|
-
finally {
|
|
198
|
-
probe.close({ layer: 'broker', initiator: 'broker.probe-close', reason: 'BROKER_PROBE_FINISHED', action: 'close' });
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
const legacy = employeeScope ? undefined : rows.find(row => row.id === oldId);
|
|
202
|
-
if (legacy)
|
|
203
|
-
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 });
|
|
204
|
-
let entry = rows.find(row => row.id === id);
|
|
205
|
-
if (entry)
|
|
206
|
-
validEntry(entry);
|
|
207
|
-
if (legacyContextScope && !entry && !request.requireFreshSession)
|
|
208
|
-
throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
|
|
209
|
-
if (legacy && !entry)
|
|
210
|
-
restarted = true;
|
|
211
|
-
let transport;
|
|
212
|
-
let resume;
|
|
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) {
|
|
214
|
-
const dirty = { version: 1, scope, brokerId: id, instanceId: entry.instanceId, pid: entry.pid, phase: 'dirty' };
|
|
215
|
-
await journal.write(dirty);
|
|
216
|
-
state = dirty;
|
|
217
|
-
} await validate(); }, ...(recovery ? { recoverySessionId: recovery.sessionId, onRecoveryUpdate: (message) => { const params = message.params; if (params.update?.sessionUpdate === 'config_option_update' && recovery.modelConfiguration) {
|
|
218
|
-
if (!Array.isArray(params.update.configOptions))
|
|
219
|
-
throw need('RECOVERY_MODEL_CONFIGURATION_INVALID');
|
|
220
|
-
(recovery.configUpdates ??= []).push(structuredClone(params.update.configOptions));
|
|
221
|
-
} 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 () => {
|
|
222
|
-
if (employeeScope)
|
|
223
|
-
throw need('EMPLOYEE_EXEC_UNKNOWN');
|
|
224
|
-
const api = get();
|
|
225
|
-
if (!provided?.io) {
|
|
226
|
-
if (!api)
|
|
227
|
-
throw need('DEPENDENCY_REQUIRED');
|
|
228
|
-
const next = unwrap(await api.getSshTarget());
|
|
229
|
-
validateTarget(next);
|
|
230
|
-
if (!sameTarget(next, target))
|
|
231
|
-
throw need('SESSION_CONTEXT_MISMATCH');
|
|
232
|
-
io = ioFor(next);
|
|
233
|
-
}
|
|
234
|
-
await validate();
|
|
235
|
-
await connectWithLease(transport, entry);
|
|
236
|
-
} });
|
|
237
|
-
if (entry) {
|
|
238
|
-
if (!modern(entry))
|
|
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) {
|
|
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);
|
|
250
|
-
if (legacyContextScope && !clean)
|
|
251
|
-
throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
|
|
252
|
-
if (clean) {
|
|
253
|
-
transport = build(entry, state.cursor, state.acp);
|
|
254
|
-
try {
|
|
255
|
-
await timing.run('broker-attach', () => connectWithLease(transport, entry));
|
|
256
|
-
await transport.replayThrough(entry.lastSequence, request.signal);
|
|
257
|
-
await request.detailChannel?.flush();
|
|
258
|
-
await validate();
|
|
259
|
-
if (legacyContextScope) {
|
|
260
|
-
const migrated = { ...state, scope };
|
|
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 });
|
|
262
|
-
await journal.write(migrated);
|
|
263
|
-
state = migrated;
|
|
264
|
-
}
|
|
265
|
-
resume = state.acp;
|
|
266
|
-
}
|
|
267
|
-
catch (error) {
|
|
268
|
-
transport.close({ layer: 'broker', initiator: 'broker.replay-failure', reason: closeCode(error, 'RECOVERY_UNCONFIRMED'), action: 'close' });
|
|
269
|
-
if (legacyContextScope || error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH', 'ABORTED'].includes(error.code))
|
|
270
|
-
throw error;
|
|
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 });
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (!resume) {
|
|
275
|
-
await stop(entry, 'SESSION_CHECKPOINT_UNRECOVERABLE', transport);
|
|
276
|
-
restarted = true;
|
|
277
|
-
entry = undefined;
|
|
278
|
-
transport = undefined;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (!entry) {
|
|
282
|
-
if (request.resumeSessionId) {
|
|
283
|
-
restarted = true;
|
|
284
|
-
await report('SESSION_BROKER_ABSENT', { brokerId: id });
|
|
285
|
-
}
|
|
286
|
-
await validate();
|
|
287
|
-
state = { version: 1, scope, brokerId: id, phase: 'dirty' };
|
|
288
|
-
await journal.write(state);
|
|
289
|
-
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)));
|
|
290
|
-
if (started?.protocolVersion !== 1 || started.id !== id || typeof started.socket !== 'string' || !started.instanceId || !Number.isSafeInteger(started.controlMaxBytes) || !Number.isSafeInteger(started.pid) || started.pid < 1)
|
|
291
|
-
throw need('BROKER_START_UNCONFIRMED');
|
|
292
|
-
entry = { ...started, state: 'running', firstSequence: 1, lastSequence: 0 };
|
|
293
|
-
state = { ...state, instanceId: entry.instanceId, pid: entry.pid };
|
|
294
|
-
await journal.write(state);
|
|
295
|
-
validEntry(entry);
|
|
296
|
-
transport = build(entry, 0);
|
|
297
|
-
const opening = transport;
|
|
298
|
-
const aborted = () => opening.fail(need('ABORTED'), { layer: 'broker', initiator: 'broker.open-signal', reason: 'ABORTED', action: 'abort' });
|
|
299
|
-
request.signal?.addEventListener('abort', aborted, { once: true });
|
|
300
|
-
try {
|
|
301
|
-
request.signal?.throwIfAborted();
|
|
302
|
-
await timing.run('broker-attach', () => connectWithLease(opening, entry));
|
|
303
|
-
await validate();
|
|
304
|
-
}
|
|
305
|
-
catch (error) {
|
|
306
|
-
if (!(error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH'].includes(error.code)))
|
|
307
|
-
await stop(entry, 'SESSION_OPEN_FAILED', opening, true, true);
|
|
308
|
-
opening.close({ layer: 'broker', initiator: 'broker.open-failure', reason: closeCode(error), action: 'close' });
|
|
309
|
-
throw error;
|
|
310
|
-
}
|
|
311
|
-
finally {
|
|
312
|
-
request.signal?.removeEventListener('abort', aborted);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
const ownedEntry = entry, ownedTransport = transport;
|
|
316
|
-
let disposed = false;
|
|
317
|
-
let disposal;
|
|
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 } : {}),
|
|
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 {
|
|
321
|
-
await journal.write(clean);
|
|
322
|
-
state = clean;
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
state = undefined;
|
|
326
|
-
throw error;
|
|
327
|
-
} }); if (!saved)
|
|
328
|
-
await report('SESSION_CHECKPOINT_NOT_CLEAN', { brokerId: id, instanceId: ownedEntry.instanceId }); return saved; },
|
|
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 {
|
|
332
|
-
await stop(ownedEntry, 'SESSION_ENDED', ownedTransport, true);
|
|
333
|
-
await journal.write({ version: 1, phase: 'ended', scope, brokerId: id, instanceId: ownedEntry.instanceId, pid: ownedEntry.pid });
|
|
334
|
-
disposed = true;
|
|
335
|
-
}
|
|
336
|
-
catch (error) {
|
|
337
|
-
primary = { error };
|
|
338
|
-
}
|
|
339
|
-
finally {
|
|
340
|
-
ownedTransport.close({ layer: 'broker', initiator: 'broker.dispose-finally', reason: 'BROKER_DISPOSE_CLEANUP', action: 'close' });
|
|
341
|
-
active.delete(key);
|
|
342
|
-
retained.delete(key);
|
|
343
|
-
} await flushCloseDiagnostics(closeDiagnostics, sink ? () => sink.flush() : undefined, primary); })(); },
|
|
344
|
-
};
|
|
345
|
-
if (employeeScope) {
|
|
346
|
-
runtime.saveCheckpoint = undefined;
|
|
347
|
-
runtime.detach = runtime.dispose;
|
|
348
|
-
}
|
|
349
|
-
if (request.retainProcess)
|
|
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(); } });
|
|
352
|
-
return runtime;
|
|
51
|
+
request.signal?.throwIfAborted();
|
|
52
|
+
await channel.connect();
|
|
353
53
|
}
|
|
354
54
|
catch (error) {
|
|
355
|
-
|
|
55
|
+
await dispose();
|
|
356
56
|
throw error;
|
|
357
57
|
}
|
|
58
|
+
finally {
|
|
59
|
+
request.signal?.removeEventListener('abort', aborted);
|
|
60
|
+
}
|
|
61
|
+
return { identity: identity(target), cwd: project.path, closeDiagnostics: diagnostics, broker: channel, acpTransport: channel, dispose, detach: dispose };
|
|
358
62
|
};
|
|
359
|
-
return open;
|
|
360
63
|
}
|
package/dist/broker.d.ts
CHANGED
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
import { type CloseCause, type CloseObserver } from './close-diagnostics.ts';
|
|
2
2
|
import { type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
3
3
|
import { NeedError, type Need } from './remote.ts';
|
|
4
|
-
import {
|
|
4
|
+
import type { ConnectTarget } from './provider.ts';
|
|
5
5
|
export type Attachment = ChildProcessWithoutNullStreams;
|
|
6
6
|
export interface BrokerOptions {
|
|
7
7
|
observeClose?: CloseObserver;
|
|
8
8
|
maxFrameBytes: number;
|
|
9
|
-
initialSequence?: number;
|
|
10
9
|
instanceId?: string;
|
|
11
10
|
leased?: boolean;
|
|
12
11
|
controlMaxBytes?: number;
|
|
13
|
-
|
|
14
|
-
beforeInput?: () => Promise<void>;
|
|
15
|
-
recoverySessionId?: string;
|
|
16
|
-
onRecoveryUpdate?: (message: Record<string, unknown>) => void;
|
|
17
|
-
discardOutput?: boolean;
|
|
18
|
-
attach: (after: number) => Promise<Attachment>;
|
|
12
|
+
attach: () => Promise<Attachment>;
|
|
19
13
|
onNeed?: (need: Need) => void;
|
|
20
|
-
onDisconnect?: () => Promise<void>;
|
|
21
14
|
}
|
|
22
|
-
/** One byte stream / SDK app
|
|
15
|
+
/** One byte stream / SDK app over one SSH attachment of a new agent. Input is never replayed. */
|
|
23
16
|
export declare class BrokerTransport {
|
|
24
17
|
readonly readable: ReadableStream<Uint8Array>;
|
|
25
18
|
readonly writable: WritableStream<Uint8Array>;
|
|
@@ -34,41 +27,24 @@ export declare class BrokerTransport {
|
|
|
34
27
|
private lease;
|
|
35
28
|
private listeners;
|
|
36
29
|
private permits;
|
|
37
|
-
private
|
|
38
|
-
private unconfirmed;
|
|
30
|
+
private promptPermit?;
|
|
39
31
|
private failure?;
|
|
40
32
|
private heartbeat?;
|
|
41
|
-
private boundary?;
|
|
42
|
-
private recovering;
|
|
43
|
-
private promptSent;
|
|
44
|
-
private preparingInputs;
|
|
45
33
|
private inputTail;
|
|
46
34
|
private exclusive;
|
|
47
35
|
constructor(options: BrokerOptions);
|
|
48
36
|
get online(): boolean;
|
|
49
37
|
get generation(): number;
|
|
50
38
|
get signal(): AbortSignal;
|
|
51
|
-
get lastSequence(): number;
|
|
52
39
|
get error(): NeedError | undefined;
|
|
53
|
-
get promptInFlight(): boolean;
|
|
54
|
-
processedMessage(message: Record<string, unknown>): void;
|
|
55
|
-
checkpointSequence(): number | null;
|
|
56
|
-
commitCheckpoint(commit: (cursor: number) => Promise<void>): Promise<boolean>;
|
|
57
|
-
finishRecovery(): void;
|
|
58
|
-
replayThrough(sequence: number, signal?: AbortSignal): Promise<void>;
|
|
59
40
|
private stopHeartbeat;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
/** Called only by the ACP protocol layer after a matching result or current-turn output. */
|
|
63
|
-
acknowledge(method: string): void;
|
|
64
|
-
/** A matching terminal prompt response settles all approval replies in that turn. */
|
|
65
|
-
completePrompt(): void;
|
|
41
|
+
authorizeResponse(id: string | number | null, generation: number, valid?: () => boolean): void;
|
|
42
|
+
authorizePrompt(valid: () => boolean): void;
|
|
66
43
|
onChange(fn: () => void): () => void;
|
|
67
44
|
private changed;
|
|
68
45
|
private notice;
|
|
69
46
|
fail(error: NeedError, cause?: CloseCause): void;
|
|
70
47
|
close(cause?: CloseCause): void;
|
|
71
|
-
waitOnline(signal?: AbortSignal): Promise<void>;
|
|
72
48
|
connect(): Promise<void>;
|
|
73
49
|
private attach;
|
|
74
50
|
private write;
|
|
@@ -82,7 +58,6 @@ export interface BrokerIo {
|
|
|
82
58
|
export declare function controlNeed(stderr: string): NeedError;
|
|
83
59
|
export declare function sshIo(target: ConnectTarget, argv: SshArgv, maxControlBytes: number): BrokerIo;
|
|
84
60
|
export declare function brokerCommand(t: ConnectTarget, args: string[]): string;
|
|
85
|
-
export declare function legacyBrokerId(sessionId: string, agentId?: string): string;
|
|
86
61
|
export declare function brokerId(sessionId: string, agentId?: string): string;
|
|
87
62
|
export declare function controlJson(text: string): unknown;
|
|
88
63
|
export interface BrokerSettings {
|