@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3
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/LICENSE +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +636 -613
- package/apps/agent-terminal/app.js +125 -65
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +171 -170
- package/apps/agent-terminal/panel-host-proof.html +152 -152
- package/apps/agent-terminal/panel-proof-content.html +41 -41
- package/apps/agent-terminal/right-workspace-dock.css +187 -159
- package/apps/agent-terminal/right-workspace-dock.js +623 -519
- package/apps/ai-profile-settings-host/index.html +25 -25
- package/apps/ai-profile-settings-host/page.css +1 -1
- package/apps/ai-profile-settings-host/page.js +113 -113
- package/apps/minimal-test-host/README.md +15 -15
- package/apps/minimal-test-host/host.css +2 -2
- package/apps/minimal-test-host/host.js +35 -35
- package/apps/minimal-test-host/index.html +58 -58
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
- package/docs/sdk/FILE_LAYOUT.md +31 -31
- package/docs/sdk/INTEGRATION.md +126 -126
- package/docs/sdk/README.md +21 -21
- package/docs/sdk/SESSION_EVENTS.md +13 -13
- package/docs/sdk/openapi.yaml +8 -1
- package/package.json +1 -1
- package/packages/client/README.md +29 -29
- package/packages/client/index.d.ts +122 -122
- package/packages/client/index.js +260 -260
- package/packages/client/package.json +2 -2
- package/packages/contracts/README.md +9 -9
- package/packages/contracts/index.d.ts +519 -517
- package/packages/contracts/index.js +312 -312
- package/packages/contracts/package.json +1 -1
- package/packages/core/README.md +8 -8
- package/packages/core/index.d.ts +76 -76
- package/packages/core/index.js +222 -222
- package/packages/core/package.json +2 -2
- package/packages/server/README.md +17 -17
- package/packages/server/bin/agent-terminal-server.js +42 -42
- package/packages/server/bin/claude-workbench.js +225 -225
- package/packages/server/index.d.ts +171 -171
- package/packages/server/index.js +230 -230
- package/packages/server/lib/agent-discovery.js +133 -133
- package/packages/server/lib/agent-registry.js +143 -143
- package/packages/server/lib/agent-spawn-env.js +17 -17
- package/packages/server/lib/attachment-store.js +78 -78
- package/packages/server/lib/bridges/app-server-bridge.js +373 -373
- package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
- package/packages/server/lib/capture-settings-store.js +35 -35
- package/packages/server/lib/claude-history-recovery.js +87 -87
- package/packages/server/lib/cli-session-recovery.js +28 -28
- package/packages/server/lib/context-resolver.js +55 -55
- package/packages/server/lib/conversation-capture.js +51 -51
- package/packages/server/lib/delegation-phase-store.js +357 -357
- package/packages/server/lib/delegation-store.js +474 -468
- package/packages/server/lib/diagnostics.js +32 -32
- package/packages/server/lib/drivers/cli-json-runner.js +974 -974
- package/packages/server/lib/drivers/codex-driver.js +1442 -1442
- package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
- package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
- package/packages/server/lib/event-stream.js +138 -138
- package/packages/server/lib/folder-picker.js +623 -623
- package/packages/server/lib/http-handler.js +339 -339
- package/packages/server/lib/json-file.js +26 -26
- package/packages/server/lib/knowledge-context.js +33 -33
- package/packages/server/lib/model-detection.js +295 -295
- package/packages/server/lib/parent-context.js +44 -0
- package/packages/server/lib/profile-store.js +283 -283
- package/packages/server/lib/project-store.js +102 -102
- package/packages/server/lib/runtime-loader.js +120 -120
- package/packages/server/lib/session-archive-store.js +53 -53
- package/packages/server/lib/session-history.js +612 -612
- package/packages/server/lib/system-doctor.js +125 -125
- package/packages/server/lib/ui-preferences-store.js +48 -48
- package/packages/server/lib/workbench-service.js +1696 -1675
- package/packages/server/package.json +2 -2
- package/packages/ui/README.md +30 -30
- package/packages/ui/ai-profile-settings.d.ts +45 -45
- package/packages/ui/ai-profile-settings.js +283 -283
- package/packages/ui/claude-terminal.d.ts +31 -31
- package/packages/ui/claude-terminal.js +343 -343
- package/packages/ui/index.d.ts +18 -18
- package/packages/ui/index.js +51 -51
- package/packages/ui/package.json +2 -2
- package/packages/ui/theme.css +13 -13
- package/src/assets/claude-prompts.json +21 -21
- package/src/backend/http/ai-profile-routes.js +61 -61
- package/src/backend/http/claude-routes.js +170 -170
- package/src/backend/http/node-helpers.js +45 -45
- package/src/backend/index.js +21 -21
- package/src/backend/lib/ai-adapter.js +144 -144
- package/src/backend/lib/ai-profile-store.js +259 -259
- package/src/backend/lib/ai-vendor-presets.js +83 -83
- package/src/backend/lib/ai-workspace.js +140 -140
- package/src/backend/lib/automation-config.js +253 -253
- package/src/backend/lib/claude-cli-runner.js +2392 -2392
- package/src/backend/lib/data-dir.js +169 -169
- package/src/backend/lib/llm-client.js +176 -176
- package/src/backend/lib/model-context-windows.js +82 -82
- package/src/backend/lib/native-task-control.js +217 -217
- package/src/backend/lib/native-task-history-index.js +475 -475
- package/src/backend/lib/native-task-history-reader.js +526 -526
- package/src/backend/lib/native-task-messages.js +457 -457
- package/src/backend/lib/native-task-registry.js +325 -325
- package/src/backend/lib/native-task-runtime.js +242 -242
- package/src/backend/lib/native-task-trace.js +440 -440
- package/src/backend/lib/native-task-transcript.js +393 -393
- package/src/backend/lib/prompt-registry.js +166 -166
- package/src/backend/lib/runtime-config.js +121 -121
- package/src/backend/lib/sdk-session-stream.js +143 -143
- package/src/backend/lib/sdk-turn-stream.js +16 -16
- package/src/backend/lib/store-scope.js +131 -131
- package/src/backend/lib/task-identity.js +104 -104
- package/src/backend/lib/task-state-projection.js +58 -58
- package/src/index.js +7 -7
- package/src/sdk-wave-a.ts +1 -1
- package/src/sdk-wave-b.ts +1 -1
- package/src/sdk-wave-c.ts +1 -1
- package/src/sdk-wave-d.ts +1 -1
- package/src/sdk-wave-e.ts +1 -1
|
@@ -1,373 +1,373 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { spawn: realSpawn } = require('child_process');
|
|
4
|
-
const { knowledgeRootEnv } = require('../agent-spawn-env');
|
|
5
|
-
|
|
6
|
-
// Generic persistent app-server bridge: manages one long-lived child process
|
|
7
|
-
// speaking line-delimited JSON-RPC over stdio (the dialect `codex app-server`
|
|
8
|
-
// uses — messages may omit the `jsonrpc` field in both directions).
|
|
9
|
-
//
|
|
10
|
-
// Responsibilities:
|
|
11
|
-
// - process lifecycle: lazy single-flight start, crash auto-restart with
|
|
12
|
-
// backoff (bounded), idle shutdown after a configurable quiet period,
|
|
13
|
-
// terminal close();
|
|
14
|
-
// - JSON-RPC client: id-correlated request/response promises, client
|
|
15
|
-
// notifications, server notification fan-out (onEvent) and server-initiated
|
|
16
|
-
// requests answered through the onServerRequest callback;
|
|
17
|
-
// - line-buffered stdio parsing (same framing as the CLI JSON runners).
|
|
18
|
-
//
|
|
19
|
-
// Everything is injectable for tests: pass `spawn` to substitute the child.
|
|
20
|
-
//
|
|
21
|
-
// config:
|
|
22
|
-
// command, args, env, cwd child invocation (env/cwd are merged/defaults)
|
|
23
|
-
// spawn injectable spawn(command, args, options)
|
|
24
|
-
// onEvent ({ method, params }) => void — notifications
|
|
25
|
-
// onServerRequest async (method, params) => result — throws to send an error response
|
|
26
|
-
// onUnexpectedExit (error) => void — child died outside idle/close (turns may hang)
|
|
27
|
-
// requestTimeoutMs per-request default timeout (0 disables; default 120000)
|
|
28
|
-
// idleTimeoutMs quiet period before shutdown (0 disables; default 600000)
|
|
29
|
-
// maxRestarts crash restart budget before giving up (default 3)
|
|
30
|
-
// restartBackoffMs array of escalating delays (default [250, 500, 1000])
|
|
31
|
-
// log (message, meta) => void diagnostics hook
|
|
32
|
-
//
|
|
33
|
-
// States: idle -> starting -> ready -> (restarting -> ready)* -> idle|crashed|closed
|
|
34
|
-
|
|
35
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 120000;
|
|
36
|
-
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
37
|
-
const DEFAULT_MAX_RESTARTS = 3;
|
|
38
|
-
const DEFAULT_RESTART_BACKOFF_MS = [250, 500, 1000];
|
|
39
|
-
|
|
40
|
-
function createAppServerBridge(config = {}) {
|
|
41
|
-
const spawnFn = config.spawn || realSpawn;
|
|
42
|
-
const onEvent = typeof config.onEvent === 'function' ? config.onEvent : () => {};
|
|
43
|
-
const onUnexpectedExit = typeof config.onUnexpectedExit === 'function' ? config.onUnexpectedExit : () => {};
|
|
44
|
-
const onReady = typeof config.onReady === 'function' ? config.onReady : () => {};
|
|
45
|
-
const onServerRequest = config.onServerRequest;
|
|
46
|
-
const log = typeof config.log === 'function' ? config.log : () => {};
|
|
47
|
-
const requestTimeoutMs = config.requestTimeoutMs !== undefined ? config.requestTimeoutMs : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
48
|
-
const idleTimeoutMs = config.idleTimeoutMs !== undefined ? config.idleTimeoutMs : DEFAULT_IDLE_TIMEOUT_MS;
|
|
49
|
-
const maxRestarts = config.maxRestarts !== undefined ? config.maxRestarts : DEFAULT_MAX_RESTARTS;
|
|
50
|
-
const restartBackoffMs = config.restartBackoffMs || DEFAULT_RESTART_BACKOFF_MS;
|
|
51
|
-
|
|
52
|
-
let state = 'idle';
|
|
53
|
-
let child = null;
|
|
54
|
-
let generation = 0; // bumped on every successful (re)launch of the child
|
|
55
|
-
let bootPromise = null;
|
|
56
|
-
let restartTimer = null;
|
|
57
|
-
let idleTimer = null;
|
|
58
|
-
let restartsUsed = 0;
|
|
59
|
-
let intentionalStop = false;
|
|
60
|
-
let closed = false;
|
|
61
|
-
let nextRequestId = 1;
|
|
62
|
-
let bootSettle = null; // { resolve, reject } of the in-flight launch promise
|
|
63
|
-
let inFlightServerRequests = 0; // unanswered server requests are activity
|
|
64
|
-
const pending = new Map(); // id -> { resolve, reject, timer }
|
|
65
|
-
|
|
66
|
-
function safeWrite(message) {
|
|
67
|
-
const line = `${JSON.stringify(message)}\n`;
|
|
68
|
-
if (!child || !child.stdin || child.stdin.destroyed) throw bridgeError('bridge child stdin is not writable', 503, 'BRIDGE_NOT_READY');
|
|
69
|
-
child.stdin.write(line);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function touchActivity() {
|
|
73
|
-
if (idleTimeoutMs > 0 && state === 'ready') {
|
|
74
|
-
clearTimeout(idleTimer);
|
|
75
|
-
idleTimer = setTimeout(idleShutdown, idleTimeoutMs);
|
|
76
|
-
idleTimer.unref && idleTimer.unref();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function idleShutdown() {
|
|
81
|
-
if (closed || pending.size > 0 || inFlightServerRequests > 0) { touchActivity(); return; }
|
|
82
|
-
log('bridge idle shutdown', { idleTimeoutMs });
|
|
83
|
-
intentionalStop = true;
|
|
84
|
-
killChild('idle-timeout');
|
|
85
|
-
state = 'idle';
|
|
86
|
-
bootPromise = null;
|
|
87
|
-
restartsUsed = 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function killChild() {
|
|
91
|
-
if (!child) return;
|
|
92
|
-
const target = child;
|
|
93
|
-
child = null;
|
|
94
|
-
try {
|
|
95
|
-
// npm shims spawn a node child on Windows; killing the shell alone
|
|
96
|
-
// orphans it. Fakes injected by tests have no pid and skip taskkill.
|
|
97
|
-
if (process.platform === 'win32' && Number.isInteger(target.pid)) {
|
|
98
|
-
realSpawn('taskkill', ['/pid', String(target.pid), '/T', '/F'], { windowsHide: true });
|
|
99
|
-
}
|
|
100
|
-
} catch { /* already gone */ }
|
|
101
|
-
try { target.kill(); } catch { /* already gone */ }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function failPending(error) {
|
|
105
|
-
for (const entry of pending.values()) {
|
|
106
|
-
clearTimeout(entry.timer);
|
|
107
|
-
entry.reject(error);
|
|
108
|
-
}
|
|
109
|
-
pending.clear();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function wire(childInstance) {
|
|
113
|
-
child = childInstance;
|
|
114
|
-
let buffer = '';
|
|
115
|
-
childInstance.stdout.setEncoding('utf8');
|
|
116
|
-
childInstance.stdout.on('data', chunk => {
|
|
117
|
-
buffer += chunk;
|
|
118
|
-
let index;
|
|
119
|
-
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
120
|
-
const line = buffer.slice(0, index).trim();
|
|
121
|
-
buffer = buffer.slice(index + 1);
|
|
122
|
-
if (line) handleMessage(line);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
childInstance.stderr.setEncoding && childInstance.stderr.setEncoding('utf8');
|
|
126
|
-
childInstance.stderr.on('data', chunk => {
|
|
127
|
-
const text = String(chunk).trim();
|
|
128
|
-
if (text) log(`bridge stderr: ${text.slice(0, 400)}`);
|
|
129
|
-
});
|
|
130
|
-
childInstance.on('error', error => {
|
|
131
|
-
log(`bridge child error: ${error.message}`);
|
|
132
|
-
handleExit(error);
|
|
133
|
-
});
|
|
134
|
-
childInstance.on('close', code => {
|
|
135
|
-
log(`bridge child exited code=${code}`);
|
|
136
|
-
handleExit(null);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function handleMessage(line) {
|
|
141
|
-
touchActivity();
|
|
142
|
-
let message;
|
|
143
|
-
try { message = JSON.parse(line); } catch {
|
|
144
|
-
log(`bridge discarded unparseable line: ${line.slice(0, 200)}`);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (!message || typeof message !== 'object') return;
|
|
148
|
-
if (message.method && message.id !== undefined && message.id !== null) {
|
|
149
|
-
handleServerRequest(message);
|
|
150
|
-
} else if (message.method) {
|
|
151
|
-
try { onEvent({ method: message.method, params: message.params }); } catch (error) {
|
|
152
|
-
log(`bridge onEvent handler failed for ${message.method}: ${error.message}`);
|
|
153
|
-
}
|
|
154
|
-
} else if (message.id !== undefined && message.id !== null) {
|
|
155
|
-
const entry = pending.get(message.id);
|
|
156
|
-
if (!entry) return; // response to an unknown/aborted request
|
|
157
|
-
pending.delete(message.id);
|
|
158
|
-
clearTimeout(entry.timer);
|
|
159
|
-
if (restartsUsed > 0) restartsUsed = 0;
|
|
160
|
-
if (message.error) entry.reject(rpcError(message.error));
|
|
161
|
-
else entry.resolve(message.result !== undefined ? message.result : null);
|
|
162
|
-
}
|
|
163
|
-
// anything else is ignored
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function handleServerRequest(message) {
|
|
167
|
-
const id = message.id;
|
|
168
|
-
// An unanswered server request (e.g. an approval) is bridge activity:
|
|
169
|
-
// it must keep the child alive and never be silently idled away.
|
|
170
|
-
inFlightServerRequests += 1;
|
|
171
|
-
try {
|
|
172
|
-
if (!onServerRequest) throw bridgeError(`no onServerRequest handler for ${message.method}`, 501, 'BRIDGE_UNSUPPORTED');
|
|
173
|
-
const result = await onServerRequest(message.method, message.params);
|
|
174
|
-
safeWrite({ id, result: result === undefined ? null : result });
|
|
175
|
-
} catch (error) {
|
|
176
|
-
try {
|
|
177
|
-
safeWrite({ id, error: { code: error.rpcCode || -32000, message: String(error && error.message || error).slice(0, 1000) } });
|
|
178
|
-
} catch { /* child already gone; nothing to answer */ }
|
|
179
|
-
} finally {
|
|
180
|
-
inFlightServerRequests -= 1;
|
|
181
|
-
touchActivity();
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function handleExit(spawnError) {
|
|
186
|
-
if (!child) return;
|
|
187
|
-
const wasReady = state === 'ready';
|
|
188
|
-
child = null;
|
|
189
|
-
bootPromise = null;
|
|
190
|
-
clearTimeout(idleTimer);
|
|
191
|
-
if (closed || intentionalStop) return;
|
|
192
|
-
const error = spawnError
|
|
193
|
-
? bridgeError(`bridge child failed: ${spawnError.message}`, 503, 'BRIDGE_SPAWN_FAILED', { cause: spawnError })
|
|
194
|
-
: bridgeError('bridge child exited unexpectedly', 503, 'BRIDGE_CRASHED');
|
|
195
|
-
failPending(error);
|
|
196
|
-
try { onUnexpectedExit(error); } catch (hookError) { log(`bridge onUnexpectedExit hook failed: ${hookError.message}`); }
|
|
197
|
-
if (!wasReady) {
|
|
198
|
-
// The boot promise never resolved; fail it so start() callers see why.
|
|
199
|
-
state = 'idle';
|
|
200
|
-
if (bootSettle) {
|
|
201
|
-
const settle = bootSettle;
|
|
202
|
-
bootSettle = null;
|
|
203
|
-
settle.reject(spawnError
|
|
204
|
-
? bridgeError(`bridge child failed to start: ${spawnError.message}`, 503, 'BRIDGE_SPAWN_FAILED', { cause: spawnError })
|
|
205
|
-
: bridgeError('bridge child exited before becoming ready', 503, 'BRIDGE_SPAWN_FAILED'));
|
|
206
|
-
}
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
if (restartsUsed < maxRestarts) {
|
|
210
|
-
const backoff = restartBackoffMs[Math.min(restartsUsed, restartBackoffMs.length - 1)];
|
|
211
|
-
restartsUsed += 1;
|
|
212
|
-
state = 'restarting';
|
|
213
|
-
log(`bridge crashed; restarting (${restartsUsed}/${maxRestarts}) in ${backoff}ms`);
|
|
214
|
-
bootPromise = new Promise((resolve, reject) => {
|
|
215
|
-
// Register the restart promise's settle so a close() during the
|
|
216
|
-
// backoff window cannot leave start() callers hanging forever.
|
|
217
|
-
bootSettle = { resolve, reject };
|
|
218
|
-
restartTimer = setTimeout(() => {
|
|
219
|
-
launchChild().then(resolve, reject);
|
|
220
|
-
}, backoff);
|
|
221
|
-
});
|
|
222
|
-
// This promise is held internally until someone calls start(); without
|
|
223
|
-
// a handler a close()-time rejection would surface as unhandled.
|
|
224
|
-
bootPromise.catch(() => {});
|
|
225
|
-
} else {
|
|
226
|
-
state = 'crashed';
|
|
227
|
-
log('bridge exhausted its restart budget');
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function launchChild() {
|
|
232
|
-
state = 'starting';
|
|
233
|
-
return new Promise((resolve, reject) => {
|
|
234
|
-
bootSettle = { resolve, reject };
|
|
235
|
-
let instance;
|
|
236
|
-
let command = config.command;
|
|
237
|
-
let args = config.args || [];
|
|
238
|
-
try {
|
|
239
|
-
// Windows bare commands (npm shims, PATH lookups) don't spawn without
|
|
240
|
-
// shell:true; resolve to the exact executable like the CLI runner
|
|
241
|
-
// does. ENOENT here would permanently demote the driver to
|
|
242
|
-
// per-turn-spawn CLI mode, which is exactly the "every turn is slow"
|
|
243
|
-
// failure mode.
|
|
244
|
-
if (process.platform === 'win32' && config.resolveInvocation) {
|
|
245
|
-
const invocation = config.resolveInvocation(command, args);
|
|
246
|
-
command = invocation.command;
|
|
247
|
-
args = invocation.args;
|
|
248
|
-
}
|
|
249
|
-
instance = spawnFn(command, args, {
|
|
250
|
-
cwd: config.cwd,
|
|
251
|
-
windowsHide: true,
|
|
252
|
-
env: { ...process.env, ...(config.env || {}), ...knowledgeRootEnv() },
|
|
253
|
-
});
|
|
254
|
-
} catch (error) {
|
|
255
|
-
state = 'idle';
|
|
256
|
-
bootSettle = null;
|
|
257
|
-
reject(bridgeError(`failed to start bridge child: ${error.message}`, 503, 'BRIDGE_SPAWN_FAILED', { cause: error }));
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
wire(instance);
|
|
261
|
-
const markReady = () => {
|
|
262
|
-
if (!child || closed) return;
|
|
263
|
-
bootSettle = null;
|
|
264
|
-
generation += 1;
|
|
265
|
-
state = 'ready';
|
|
266
|
-
intentionalStop = false;
|
|
267
|
-
touchActivity();
|
|
268
|
-
try { onReady(generation); } catch (error) { log(`bridge onReady hook failed: ${error.message}`); }
|
|
269
|
-
resolve();
|
|
270
|
-
};
|
|
271
|
-
if (instance.pid === undefined) {
|
|
272
|
-
// Fake children (EventEmitter stand-ins) never emit 'spawn'.
|
|
273
|
-
setImmediate(markReady);
|
|
274
|
-
} else {
|
|
275
|
-
// Real children signal readiness through 'spawn'; a fallback timer
|
|
276
|
-
// covers exotic platforms that are slow to emit it.
|
|
277
|
-
if (typeof instance.once === 'function') instance.once('spawn', markReady);
|
|
278
|
-
const fallback = setTimeout(markReady, 500);
|
|
279
|
-
fallback.unref && fallback.unref();
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function start() {
|
|
285
|
-
if (closed) return Promise.reject(bridgeError('bridge is closed', 503, 'BRIDGE_CLOSED'));
|
|
286
|
-
if (state === 'ready' && bootPromise) return bootPromise;
|
|
287
|
-
if (state === 'crashed') return Promise.reject(bridgeError('bridge exhausted its restart budget', 503, 'BRIDGE_CRASHED'));
|
|
288
|
-
if (!bootPromise) bootPromise = launchChild();
|
|
289
|
-
return bootPromise;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function request(method, params, options = {}) {
|
|
293
|
-
if (closed) return Promise.reject(bridgeError('bridge is closed', 503, 'BRIDGE_CLOSED'));
|
|
294
|
-
if (state === 'crashed') return Promise.reject(bridgeError('bridge exhausted its restart budget', 503, 'BRIDGE_CRASHED'));
|
|
295
|
-
const timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs : requestTimeoutMs;
|
|
296
|
-
return start().then(() => new Promise((resolve, reject) => {
|
|
297
|
-
const id = nextRequestId++;
|
|
298
|
-
const entry = { resolve, reject, timer: null };
|
|
299
|
-
if (timeoutMs > 0) {
|
|
300
|
-
entry.timer = setTimeout(() => {
|
|
301
|
-
pending.delete(id);
|
|
302
|
-
reject(bridgeError(`bridge request ${method} timed out after ${timeoutMs}ms`, 504, 'BRIDGE_TIMEOUT'));
|
|
303
|
-
}, timeoutMs);
|
|
304
|
-
entry.timer.unref && entry.timer.unref();
|
|
305
|
-
}
|
|
306
|
-
pending.set(id, entry);
|
|
307
|
-
try {
|
|
308
|
-
// The app-server dialect omits the jsonrpc field; we mirror that.
|
|
309
|
-
safeWrite(params !== undefined ? { id, method, params } : { id, method });
|
|
310
|
-
touchActivity();
|
|
311
|
-
} catch (error) {
|
|
312
|
-
pending.delete(id);
|
|
313
|
-
clearTimeout(entry.timer);
|
|
314
|
-
reject(error);
|
|
315
|
-
}
|
|
316
|
-
}));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function notify(method, params) {
|
|
320
|
-
if (closed) throw bridgeError('bridge is closed', 503, 'BRIDGE_CLOSED');
|
|
321
|
-
return start().then(() => {
|
|
322
|
-
safeWrite(params !== undefined ? { method, params } : { method });
|
|
323
|
-
touchActivity();
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function close() {
|
|
328
|
-
if (closed) return Promise.resolve();
|
|
329
|
-
closed = true;
|
|
330
|
-
state = 'closed';
|
|
331
|
-
clearTimeout(restartTimer);
|
|
332
|
-
clearTimeout(idleTimer);
|
|
333
|
-
intentionalStop = true;
|
|
334
|
-
killChild();
|
|
335
|
-
failPending(bridgeError('bridge closed', 503, 'BRIDGE_CLOSED'));
|
|
336
|
-
// A close() while still starting (or in a restart backoff) must settle
|
|
337
|
-
// the boot promise, otherwise every awaiting start() caller hangs.
|
|
338
|
-
if (bootSettle) {
|
|
339
|
-
const settle = bootSettle;
|
|
340
|
-
bootSettle = null;
|
|
341
|
-
settle.reject(bridgeError('bridge closed', 503, 'BRIDGE_CLOSED'));
|
|
342
|
-
}
|
|
343
|
-
return Promise.resolve();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
command: config.command,
|
|
348
|
-
args: config.args || [],
|
|
349
|
-
env: config.env || {},
|
|
350
|
-
get state() { return state; },
|
|
351
|
-
get pendingCount() { return pending.size; },
|
|
352
|
-
get restartsUsed() { return restartsUsed; },
|
|
353
|
-
get generation() { return generation; },
|
|
354
|
-
start,
|
|
355
|
-
request,
|
|
356
|
-
notify,
|
|
357
|
-
close,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function bridgeError(message, status, code, extra = {}) {
|
|
362
|
-
return Object.assign(new Error(message), { status, code, ...extra });
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function rpcError(rpc) {
|
|
366
|
-
return Object.assign(new Error(String(rpc && rpc.message || 'bridge rpc error').slice(0, 1000)), {
|
|
367
|
-
status: 502,
|
|
368
|
-
code: 'BRIDGE_RPC_ERROR',
|
|
369
|
-
rpcCode: rpc && rpc.code,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
module.exports = { createAppServerBridge };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn: realSpawn } = require('child_process');
|
|
4
|
+
const { knowledgeRootEnv } = require('../agent-spawn-env');
|
|
5
|
+
|
|
6
|
+
// Generic persistent app-server bridge: manages one long-lived child process
|
|
7
|
+
// speaking line-delimited JSON-RPC over stdio (the dialect `codex app-server`
|
|
8
|
+
// uses — messages may omit the `jsonrpc` field in both directions).
|
|
9
|
+
//
|
|
10
|
+
// Responsibilities:
|
|
11
|
+
// - process lifecycle: lazy single-flight start, crash auto-restart with
|
|
12
|
+
// backoff (bounded), idle shutdown after a configurable quiet period,
|
|
13
|
+
// terminal close();
|
|
14
|
+
// - JSON-RPC client: id-correlated request/response promises, client
|
|
15
|
+
// notifications, server notification fan-out (onEvent) and server-initiated
|
|
16
|
+
// requests answered through the onServerRequest callback;
|
|
17
|
+
// - line-buffered stdio parsing (same framing as the CLI JSON runners).
|
|
18
|
+
//
|
|
19
|
+
// Everything is injectable for tests: pass `spawn` to substitute the child.
|
|
20
|
+
//
|
|
21
|
+
// config:
|
|
22
|
+
// command, args, env, cwd child invocation (env/cwd are merged/defaults)
|
|
23
|
+
// spawn injectable spawn(command, args, options)
|
|
24
|
+
// onEvent ({ method, params }) => void — notifications
|
|
25
|
+
// onServerRequest async (method, params) => result — throws to send an error response
|
|
26
|
+
// onUnexpectedExit (error) => void — child died outside idle/close (turns may hang)
|
|
27
|
+
// requestTimeoutMs per-request default timeout (0 disables; default 120000)
|
|
28
|
+
// idleTimeoutMs quiet period before shutdown (0 disables; default 600000)
|
|
29
|
+
// maxRestarts crash restart budget before giving up (default 3)
|
|
30
|
+
// restartBackoffMs array of escalating delays (default [250, 500, 1000])
|
|
31
|
+
// log (message, meta) => void diagnostics hook
|
|
32
|
+
//
|
|
33
|
+
// States: idle -> starting -> ready -> (restarting -> ready)* -> idle|crashed|closed
|
|
34
|
+
|
|
35
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 120000;
|
|
36
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
37
|
+
const DEFAULT_MAX_RESTARTS = 3;
|
|
38
|
+
const DEFAULT_RESTART_BACKOFF_MS = [250, 500, 1000];
|
|
39
|
+
|
|
40
|
+
function createAppServerBridge(config = {}) {
|
|
41
|
+
const spawnFn = config.spawn || realSpawn;
|
|
42
|
+
const onEvent = typeof config.onEvent === 'function' ? config.onEvent : () => {};
|
|
43
|
+
const onUnexpectedExit = typeof config.onUnexpectedExit === 'function' ? config.onUnexpectedExit : () => {};
|
|
44
|
+
const onReady = typeof config.onReady === 'function' ? config.onReady : () => {};
|
|
45
|
+
const onServerRequest = config.onServerRequest;
|
|
46
|
+
const log = typeof config.log === 'function' ? config.log : () => {};
|
|
47
|
+
const requestTimeoutMs = config.requestTimeoutMs !== undefined ? config.requestTimeoutMs : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
48
|
+
const idleTimeoutMs = config.idleTimeoutMs !== undefined ? config.idleTimeoutMs : DEFAULT_IDLE_TIMEOUT_MS;
|
|
49
|
+
const maxRestarts = config.maxRestarts !== undefined ? config.maxRestarts : DEFAULT_MAX_RESTARTS;
|
|
50
|
+
const restartBackoffMs = config.restartBackoffMs || DEFAULT_RESTART_BACKOFF_MS;
|
|
51
|
+
|
|
52
|
+
let state = 'idle';
|
|
53
|
+
let child = null;
|
|
54
|
+
let generation = 0; // bumped on every successful (re)launch of the child
|
|
55
|
+
let bootPromise = null;
|
|
56
|
+
let restartTimer = null;
|
|
57
|
+
let idleTimer = null;
|
|
58
|
+
let restartsUsed = 0;
|
|
59
|
+
let intentionalStop = false;
|
|
60
|
+
let closed = false;
|
|
61
|
+
let nextRequestId = 1;
|
|
62
|
+
let bootSettle = null; // { resolve, reject } of the in-flight launch promise
|
|
63
|
+
let inFlightServerRequests = 0; // unanswered server requests are activity
|
|
64
|
+
const pending = new Map(); // id -> { resolve, reject, timer }
|
|
65
|
+
|
|
66
|
+
function safeWrite(message) {
|
|
67
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
68
|
+
if (!child || !child.stdin || child.stdin.destroyed) throw bridgeError('bridge child stdin is not writable', 503, 'BRIDGE_NOT_READY');
|
|
69
|
+
child.stdin.write(line);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function touchActivity() {
|
|
73
|
+
if (idleTimeoutMs > 0 && state === 'ready') {
|
|
74
|
+
clearTimeout(idleTimer);
|
|
75
|
+
idleTimer = setTimeout(idleShutdown, idleTimeoutMs);
|
|
76
|
+
idleTimer.unref && idleTimer.unref();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function idleShutdown() {
|
|
81
|
+
if (closed || pending.size > 0 || inFlightServerRequests > 0) { touchActivity(); return; }
|
|
82
|
+
log('bridge idle shutdown', { idleTimeoutMs });
|
|
83
|
+
intentionalStop = true;
|
|
84
|
+
killChild('idle-timeout');
|
|
85
|
+
state = 'idle';
|
|
86
|
+
bootPromise = null;
|
|
87
|
+
restartsUsed = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function killChild() {
|
|
91
|
+
if (!child) return;
|
|
92
|
+
const target = child;
|
|
93
|
+
child = null;
|
|
94
|
+
try {
|
|
95
|
+
// npm shims spawn a node child on Windows; killing the shell alone
|
|
96
|
+
// orphans it. Fakes injected by tests have no pid and skip taskkill.
|
|
97
|
+
if (process.platform === 'win32' && Number.isInteger(target.pid)) {
|
|
98
|
+
realSpawn('taskkill', ['/pid', String(target.pid), '/T', '/F'], { windowsHide: true });
|
|
99
|
+
}
|
|
100
|
+
} catch { /* already gone */ }
|
|
101
|
+
try { target.kill(); } catch { /* already gone */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function failPending(error) {
|
|
105
|
+
for (const entry of pending.values()) {
|
|
106
|
+
clearTimeout(entry.timer);
|
|
107
|
+
entry.reject(error);
|
|
108
|
+
}
|
|
109
|
+
pending.clear();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function wire(childInstance) {
|
|
113
|
+
child = childInstance;
|
|
114
|
+
let buffer = '';
|
|
115
|
+
childInstance.stdout.setEncoding('utf8');
|
|
116
|
+
childInstance.stdout.on('data', chunk => {
|
|
117
|
+
buffer += chunk;
|
|
118
|
+
let index;
|
|
119
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
120
|
+
const line = buffer.slice(0, index).trim();
|
|
121
|
+
buffer = buffer.slice(index + 1);
|
|
122
|
+
if (line) handleMessage(line);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
childInstance.stderr.setEncoding && childInstance.stderr.setEncoding('utf8');
|
|
126
|
+
childInstance.stderr.on('data', chunk => {
|
|
127
|
+
const text = String(chunk).trim();
|
|
128
|
+
if (text) log(`bridge stderr: ${text.slice(0, 400)}`);
|
|
129
|
+
});
|
|
130
|
+
childInstance.on('error', error => {
|
|
131
|
+
log(`bridge child error: ${error.message}`);
|
|
132
|
+
handleExit(error);
|
|
133
|
+
});
|
|
134
|
+
childInstance.on('close', code => {
|
|
135
|
+
log(`bridge child exited code=${code}`);
|
|
136
|
+
handleExit(null);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function handleMessage(line) {
|
|
141
|
+
touchActivity();
|
|
142
|
+
let message;
|
|
143
|
+
try { message = JSON.parse(line); } catch {
|
|
144
|
+
log(`bridge discarded unparseable line: ${line.slice(0, 200)}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (!message || typeof message !== 'object') return;
|
|
148
|
+
if (message.method && message.id !== undefined && message.id !== null) {
|
|
149
|
+
handleServerRequest(message);
|
|
150
|
+
} else if (message.method) {
|
|
151
|
+
try { onEvent({ method: message.method, params: message.params }); } catch (error) {
|
|
152
|
+
log(`bridge onEvent handler failed for ${message.method}: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
} else if (message.id !== undefined && message.id !== null) {
|
|
155
|
+
const entry = pending.get(message.id);
|
|
156
|
+
if (!entry) return; // response to an unknown/aborted request
|
|
157
|
+
pending.delete(message.id);
|
|
158
|
+
clearTimeout(entry.timer);
|
|
159
|
+
if (restartsUsed > 0) restartsUsed = 0;
|
|
160
|
+
if (message.error) entry.reject(rpcError(message.error));
|
|
161
|
+
else entry.resolve(message.result !== undefined ? message.result : null);
|
|
162
|
+
}
|
|
163
|
+
// anything else is ignored
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handleServerRequest(message) {
|
|
167
|
+
const id = message.id;
|
|
168
|
+
// An unanswered server request (e.g. an approval) is bridge activity:
|
|
169
|
+
// it must keep the child alive and never be silently idled away.
|
|
170
|
+
inFlightServerRequests += 1;
|
|
171
|
+
try {
|
|
172
|
+
if (!onServerRequest) throw bridgeError(`no onServerRequest handler for ${message.method}`, 501, 'BRIDGE_UNSUPPORTED');
|
|
173
|
+
const result = await onServerRequest(message.method, message.params);
|
|
174
|
+
safeWrite({ id, result: result === undefined ? null : result });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
try {
|
|
177
|
+
safeWrite({ id, error: { code: error.rpcCode || -32000, message: String(error && error.message || error).slice(0, 1000) } });
|
|
178
|
+
} catch { /* child already gone; nothing to answer */ }
|
|
179
|
+
} finally {
|
|
180
|
+
inFlightServerRequests -= 1;
|
|
181
|
+
touchActivity();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function handleExit(spawnError) {
|
|
186
|
+
if (!child) return;
|
|
187
|
+
const wasReady = state === 'ready';
|
|
188
|
+
child = null;
|
|
189
|
+
bootPromise = null;
|
|
190
|
+
clearTimeout(idleTimer);
|
|
191
|
+
if (closed || intentionalStop) return;
|
|
192
|
+
const error = spawnError
|
|
193
|
+
? bridgeError(`bridge child failed: ${spawnError.message}`, 503, 'BRIDGE_SPAWN_FAILED', { cause: spawnError })
|
|
194
|
+
: bridgeError('bridge child exited unexpectedly', 503, 'BRIDGE_CRASHED');
|
|
195
|
+
failPending(error);
|
|
196
|
+
try { onUnexpectedExit(error); } catch (hookError) { log(`bridge onUnexpectedExit hook failed: ${hookError.message}`); }
|
|
197
|
+
if (!wasReady) {
|
|
198
|
+
// The boot promise never resolved; fail it so start() callers see why.
|
|
199
|
+
state = 'idle';
|
|
200
|
+
if (bootSettle) {
|
|
201
|
+
const settle = bootSettle;
|
|
202
|
+
bootSettle = null;
|
|
203
|
+
settle.reject(spawnError
|
|
204
|
+
? bridgeError(`bridge child failed to start: ${spawnError.message}`, 503, 'BRIDGE_SPAWN_FAILED', { cause: spawnError })
|
|
205
|
+
: bridgeError('bridge child exited before becoming ready', 503, 'BRIDGE_SPAWN_FAILED'));
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (restartsUsed < maxRestarts) {
|
|
210
|
+
const backoff = restartBackoffMs[Math.min(restartsUsed, restartBackoffMs.length - 1)];
|
|
211
|
+
restartsUsed += 1;
|
|
212
|
+
state = 'restarting';
|
|
213
|
+
log(`bridge crashed; restarting (${restartsUsed}/${maxRestarts}) in ${backoff}ms`);
|
|
214
|
+
bootPromise = new Promise((resolve, reject) => {
|
|
215
|
+
// Register the restart promise's settle so a close() during the
|
|
216
|
+
// backoff window cannot leave start() callers hanging forever.
|
|
217
|
+
bootSettle = { resolve, reject };
|
|
218
|
+
restartTimer = setTimeout(() => {
|
|
219
|
+
launchChild().then(resolve, reject);
|
|
220
|
+
}, backoff);
|
|
221
|
+
});
|
|
222
|
+
// This promise is held internally until someone calls start(); without
|
|
223
|
+
// a handler a close()-time rejection would surface as unhandled.
|
|
224
|
+
bootPromise.catch(() => {});
|
|
225
|
+
} else {
|
|
226
|
+
state = 'crashed';
|
|
227
|
+
log('bridge exhausted its restart budget');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function launchChild() {
|
|
232
|
+
state = 'starting';
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
bootSettle = { resolve, reject };
|
|
235
|
+
let instance;
|
|
236
|
+
let command = config.command;
|
|
237
|
+
let args = config.args || [];
|
|
238
|
+
try {
|
|
239
|
+
// Windows bare commands (npm shims, PATH lookups) don't spawn without
|
|
240
|
+
// shell:true; resolve to the exact executable like the CLI runner
|
|
241
|
+
// does. ENOENT here would permanently demote the driver to
|
|
242
|
+
// per-turn-spawn CLI mode, which is exactly the "every turn is slow"
|
|
243
|
+
// failure mode.
|
|
244
|
+
if (process.platform === 'win32' && config.resolveInvocation) {
|
|
245
|
+
const invocation = config.resolveInvocation(command, args);
|
|
246
|
+
command = invocation.command;
|
|
247
|
+
args = invocation.args;
|
|
248
|
+
}
|
|
249
|
+
instance = spawnFn(command, args, {
|
|
250
|
+
cwd: config.cwd,
|
|
251
|
+
windowsHide: true,
|
|
252
|
+
env: { ...process.env, ...(config.env || {}), ...knowledgeRootEnv() },
|
|
253
|
+
});
|
|
254
|
+
} catch (error) {
|
|
255
|
+
state = 'idle';
|
|
256
|
+
bootSettle = null;
|
|
257
|
+
reject(bridgeError(`failed to start bridge child: ${error.message}`, 503, 'BRIDGE_SPAWN_FAILED', { cause: error }));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
wire(instance);
|
|
261
|
+
const markReady = () => {
|
|
262
|
+
if (!child || closed) return;
|
|
263
|
+
bootSettle = null;
|
|
264
|
+
generation += 1;
|
|
265
|
+
state = 'ready';
|
|
266
|
+
intentionalStop = false;
|
|
267
|
+
touchActivity();
|
|
268
|
+
try { onReady(generation); } catch (error) { log(`bridge onReady hook failed: ${error.message}`); }
|
|
269
|
+
resolve();
|
|
270
|
+
};
|
|
271
|
+
if (instance.pid === undefined) {
|
|
272
|
+
// Fake children (EventEmitter stand-ins) never emit 'spawn'.
|
|
273
|
+
setImmediate(markReady);
|
|
274
|
+
} else {
|
|
275
|
+
// Real children signal readiness through 'spawn'; a fallback timer
|
|
276
|
+
// covers exotic platforms that are slow to emit it.
|
|
277
|
+
if (typeof instance.once === 'function') instance.once('spawn', markReady);
|
|
278
|
+
const fallback = setTimeout(markReady, 500);
|
|
279
|
+
fallback.unref && fallback.unref();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function start() {
|
|
285
|
+
if (closed) return Promise.reject(bridgeError('bridge is closed', 503, 'BRIDGE_CLOSED'));
|
|
286
|
+
if (state === 'ready' && bootPromise) return bootPromise;
|
|
287
|
+
if (state === 'crashed') return Promise.reject(bridgeError('bridge exhausted its restart budget', 503, 'BRIDGE_CRASHED'));
|
|
288
|
+
if (!bootPromise) bootPromise = launchChild();
|
|
289
|
+
return bootPromise;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function request(method, params, options = {}) {
|
|
293
|
+
if (closed) return Promise.reject(bridgeError('bridge is closed', 503, 'BRIDGE_CLOSED'));
|
|
294
|
+
if (state === 'crashed') return Promise.reject(bridgeError('bridge exhausted its restart budget', 503, 'BRIDGE_CRASHED'));
|
|
295
|
+
const timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs : requestTimeoutMs;
|
|
296
|
+
return start().then(() => new Promise((resolve, reject) => {
|
|
297
|
+
const id = nextRequestId++;
|
|
298
|
+
const entry = { resolve, reject, timer: null };
|
|
299
|
+
if (timeoutMs > 0) {
|
|
300
|
+
entry.timer = setTimeout(() => {
|
|
301
|
+
pending.delete(id);
|
|
302
|
+
reject(bridgeError(`bridge request ${method} timed out after ${timeoutMs}ms`, 504, 'BRIDGE_TIMEOUT'));
|
|
303
|
+
}, timeoutMs);
|
|
304
|
+
entry.timer.unref && entry.timer.unref();
|
|
305
|
+
}
|
|
306
|
+
pending.set(id, entry);
|
|
307
|
+
try {
|
|
308
|
+
// The app-server dialect omits the jsonrpc field; we mirror that.
|
|
309
|
+
safeWrite(params !== undefined ? { id, method, params } : { id, method });
|
|
310
|
+
touchActivity();
|
|
311
|
+
} catch (error) {
|
|
312
|
+
pending.delete(id);
|
|
313
|
+
clearTimeout(entry.timer);
|
|
314
|
+
reject(error);
|
|
315
|
+
}
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function notify(method, params) {
|
|
320
|
+
if (closed) throw bridgeError('bridge is closed', 503, 'BRIDGE_CLOSED');
|
|
321
|
+
return start().then(() => {
|
|
322
|
+
safeWrite(params !== undefined ? { method, params } : { method });
|
|
323
|
+
touchActivity();
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function close() {
|
|
328
|
+
if (closed) return Promise.resolve();
|
|
329
|
+
closed = true;
|
|
330
|
+
state = 'closed';
|
|
331
|
+
clearTimeout(restartTimer);
|
|
332
|
+
clearTimeout(idleTimer);
|
|
333
|
+
intentionalStop = true;
|
|
334
|
+
killChild();
|
|
335
|
+
failPending(bridgeError('bridge closed', 503, 'BRIDGE_CLOSED'));
|
|
336
|
+
// A close() while still starting (or in a restart backoff) must settle
|
|
337
|
+
// the boot promise, otherwise every awaiting start() caller hangs.
|
|
338
|
+
if (bootSettle) {
|
|
339
|
+
const settle = bootSettle;
|
|
340
|
+
bootSettle = null;
|
|
341
|
+
settle.reject(bridgeError('bridge closed', 503, 'BRIDGE_CLOSED'));
|
|
342
|
+
}
|
|
343
|
+
return Promise.resolve();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
command: config.command,
|
|
348
|
+
args: config.args || [],
|
|
349
|
+
env: config.env || {},
|
|
350
|
+
get state() { return state; },
|
|
351
|
+
get pendingCount() { return pending.size; },
|
|
352
|
+
get restartsUsed() { return restartsUsed; },
|
|
353
|
+
get generation() { return generation; },
|
|
354
|
+
start,
|
|
355
|
+
request,
|
|
356
|
+
notify,
|
|
357
|
+
close,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function bridgeError(message, status, code, extra = {}) {
|
|
362
|
+
return Object.assign(new Error(message), { status, code, ...extra });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function rpcError(rpc) {
|
|
366
|
+
return Object.assign(new Error(String(rpc && rpc.message || 'bridge rpc error').slice(0, 1000)), {
|
|
367
|
+
status: 502,
|
|
368
|
+
code: 'BRIDGE_RPC_ERROR',
|
|
369
|
+
rpcCode: rpc && rpc.code,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
module.exports = { createAppServerBridge };
|