dsh-agy-cli 0.0.0-stage → 0.12.0
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 +30 -0
- package/README.en.md +27 -0
- package/README.md +94 -2
- package/agents/dsh-agy-image-view.md +17 -0
- package/agents/dsh-agy-mcp-executor.md +32 -0
- package/agents/dsh-agy-read-only.md +20 -0
- package/agents/dsh-agy-tool-free.md +16 -0
- package/agents/dsh-agy-workspace-write.md +23 -0
- package/bin/dsh-agy-cli.js +9 -0
- package/bin/win32-x64/agy-launcher.exe +0 -0
- package/cordis.patch.yml +16 -0
- package/dist/client.js +4259 -0
- package/lib/agent-installer.d.ts +66 -0
- package/lib/agent-installer.js +147 -0
- package/lib/agent-presets.d.ts +42 -0
- package/lib/agent-presets.js +130 -0
- package/lib/agents-cli.d.ts +3 -0
- package/lib/agents-cli.js +110 -0
- package/lib/agy/diagnostics.d.ts +29 -0
- package/lib/agy/diagnostics.js +109 -0
- package/lib/agy/errors.d.ts +20 -0
- package/lib/agy/errors.js +77 -0
- package/lib/agy/experimental-transport.d.ts +79 -0
- package/lib/agy/experimental-transport.js +612 -0
- package/lib/agy/keepalive.d.ts +140 -0
- package/lib/agy/keepalive.js +424 -0
- package/lib/agy/limiter.d.ts +32 -0
- package/lib/agy/limiter.js +111 -0
- package/lib/agy/log.d.ts +214 -0
- package/lib/agy/log.js +503 -0
- package/lib/agy/models.d.ts +49 -0
- package/lib/agy/models.js +203 -0
- package/lib/agy/overrides.d.ts +17 -0
- package/lib/agy/overrides.js +73 -0
- package/lib/agy/parser.d.ts +102 -0
- package/lib/agy/parser.js +492 -0
- package/lib/agy/persistent-transport.d.ts +101 -0
- package/lib/agy/persistent-transport.js +753 -0
- package/lib/agy/process.d.ts +144 -0
- package/lib/agy/process.js +452 -0
- package/lib/agy/redact.d.ts +3 -0
- package/lib/agy/redact.js +24 -0
- package/lib/agy/stream-protocol.d.ts +50 -0
- package/lib/agy/stream-protocol.js +156 -0
- package/lib/agy/windows-launcher.d.ts +21 -0
- package/lib/agy/windows-launcher.js +76 -0
- package/lib/diagnostics.d.ts +78 -0
- package/lib/diagnostics.js +162 -0
- package/lib/doctor.d.ts +138 -0
- package/lib/doctor.js +923 -0
- package/lib/dsh/context.d.ts +133 -0
- package/lib/dsh/context.js +352 -0
- package/lib/dsh-home.d.ts +6 -0
- package/lib/dsh-home.js +11 -0
- package/lib/index.d.ts +43 -0
- package/lib/index.js +256 -0
- package/lib/mcp/agy-registration.d.ts +70 -0
- package/lib/mcp/agy-registration.js +245 -0
- package/lib/mcp/bridge.d.ts +73 -0
- package/lib/mcp/bridge.js +295 -0
- package/lib/mcp/endpoint.d.ts +41 -0
- package/lib/mcp/endpoint.js +142 -0
- package/lib/mcp/protocol.d.ts +107 -0
- package/lib/mcp/protocol.js +197 -0
- package/lib/mcp/tool-projection.d.ts +64 -0
- package/lib/mcp/tool-projection.js +168 -0
- package/lib/provider/agy.d.ts +153 -0
- package/lib/provider/agy.js +2159 -0
- package/lib/provider/config.d.ts +226 -0
- package/lib/provider/config.js +294 -0
- package/lib/provider/error-codes.d.ts +22 -0
- package/lib/provider/error-codes.js +29 -0
- package/lib/provider/image-bridge.d.ts +60 -0
- package/lib/provider/image-bridge.js +290 -0
- package/lib/provider/mock.d.ts +15 -0
- package/lib/provider/mock.js +78 -0
- package/lib/provider/prompt-budget.d.ts +45 -0
- package/lib/provider/prompt-budget.js +596 -0
- package/lib/provider/serialize.d.ts +15 -0
- package/lib/provider/serialize.js +82 -0
- package/lib/provider/tool-protocol.d.ts +80 -0
- package/lib/provider/tool-protocol.js +848 -0
- package/lib/provider/tool-schema-file.d.ts +11 -0
- package/lib/provider/tool-schema-file.js +27 -0
- package/lib/session/file-store.d.ts +56 -0
- package/lib/session/file-store.js +186 -0
- package/lib/session/store.d.ts +49 -0
- package/lib/session/store.js +62 -0
- package/lib/web/router.d.ts +19 -0
- package/lib/web/router.js +906 -0
- package/package.json +112 -4
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Productized persistent transport for AGY 1.1.15 stream-json.
|
|
3
|
+
* Input: {event:"user",message:{role:"user",content:[{type:"text",text}]}} per NDJSON line.
|
|
4
|
+
* Output: {event:"init"} -> {event:"step_update"}* -> {event:"result"} per turn.
|
|
5
|
+
* One AGY process per DSH session, single active turn, idle TTL, before-accept fallback.
|
|
6
|
+
* See findings.md 2026-08-20 V8-M1 and .tmp/v8-m1/three-turn.log.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { encodeAgyUserMessage } from './stream-protocol.js';
|
|
11
|
+
import { buildWindowsNoConsoleLaunch } from './windows-launcher.js';
|
|
12
|
+
import { isAgyReasoningEffort } from './process.js';
|
|
13
|
+
export class PersistentTransportError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
constructor(message, code, options) {
|
|
16
|
+
super(message, options);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.name = 'PersistentTransportError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const DEFAULT_MAX_WORKERS = 8;
|
|
22
|
+
const DEFAULT_MAX_FRAME_BYTES = 256 * 1024;
|
|
23
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
24
|
+
const DEFAULT_MAX_STDERR_BYTES = 64 * 1024;
|
|
25
|
+
const DEFAULT_IDLE_TTL_MS = 30_000;
|
|
26
|
+
const DEFAULT_READY_TIMEOUT_MS = 5_000;
|
|
27
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 1_000;
|
|
28
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
29
|
+
function delay(milliseconds) {
|
|
30
|
+
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
31
|
+
}
|
|
32
|
+
function validateInteger(name, value, minimum, maximum) {
|
|
33
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
34
|
+
throw new RangeError(`${name} must be an integer between ${minimum} and ${maximum}`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function normalizeOptions(options) {
|
|
39
|
+
if (options.executable.trim().length === 0) {
|
|
40
|
+
throw new RangeError('executable must not be empty');
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
executable: options.executable,
|
|
44
|
+
args: options.args ?? [],
|
|
45
|
+
cwd: options.cwd,
|
|
46
|
+
env: options.env,
|
|
47
|
+
maxWorkers: validateInteger('maxWorkers', options.maxWorkers ?? DEFAULT_MAX_WORKERS, 1, 64),
|
|
48
|
+
maxFrameBytes: validateInteger('maxFrameBytes', options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES, 128, 16 * 1024 * 1024),
|
|
49
|
+
maxOutputBytes: validateInteger('maxOutputBytes', options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES, 1, 64 * 1024 * 1024),
|
|
50
|
+
maxStderrBytes: validateInteger('maxStderrBytes', options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES, 1, 8 * 1024 * 1024),
|
|
51
|
+
idleTtlMs: validateInteger('idleTtlMs', options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS, 0, 3_600_000),
|
|
52
|
+
readyTimeoutMs: validateInteger('readyTimeoutMs', options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS, 1, 60_000),
|
|
53
|
+
shutdownTimeoutMs: validateInteger('shutdownTimeoutMs', options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS, 1, 60_000),
|
|
54
|
+
defaultRequestTimeoutMs: validateInteger('defaultRequestTimeoutMs', options.defaultRequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, 1, 3_600_000),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function isRecord(value) {
|
|
58
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
function byteLength(value) {
|
|
61
|
+
return Buffer.byteLength(value, 'utf8');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Route args for one worker. AGY reads --agent/--model/--effort/--conversation
|
|
65
|
+
* once at process start, so these must be known before spawning; the rest of
|
|
66
|
+
* the argv comes from the transport options.
|
|
67
|
+
*/
|
|
68
|
+
function routeArgs(input) {
|
|
69
|
+
if (input.reasoningEffort !== undefined && !isAgyReasoningEffort(input.reasoningEffort)) {
|
|
70
|
+
throw new TypeError('Persistent AGY reasoning effort must be one of: low, medium, high');
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
...(input.agent === undefined ? [] : ['--agent', input.agent]),
|
|
74
|
+
...(input.model === undefined ? [] : ['--model', input.model]),
|
|
75
|
+
...(input.conversation === undefined ? [] : ['--conversation', input.conversation]),
|
|
76
|
+
...(input.reasoningEffort === undefined ? [] : ['--effort', input.reasoningEffort]),
|
|
77
|
+
...(input.extraArgs ?? []),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Worker pool key. The session alone is not enough: a session that switches
|
|
82
|
+
* model, or that must continue a different AGY conversation, needs its own
|
|
83
|
+
* process. See PersistentTransportRequest.agent/model/reasoningEffort.
|
|
84
|
+
*/
|
|
85
|
+
function workerKeyOf(input) {
|
|
86
|
+
const route = [
|
|
87
|
+
input.agent ?? '',
|
|
88
|
+
input.model ?? '',
|
|
89
|
+
input.reasoningEffort ?? '',
|
|
90
|
+
input.conversation ?? '',
|
|
91
|
+
(input.extraArgs ?? []).join('\u0000'),
|
|
92
|
+
].join('\u0001');
|
|
93
|
+
return `${input.sessionId}\u0002${route}`;
|
|
94
|
+
}
|
|
95
|
+
/** Encode one bounded NDJSON frame, including its trailing newline. */
|
|
96
|
+
export function encodePersistentFrame(frame, maxFrameBytes) {
|
|
97
|
+
const line = JSON.stringify(frame);
|
|
98
|
+
if (line === undefined) {
|
|
99
|
+
throw new PersistentTransportError('Persistent transport frame could not be encoded', 'PROTOCOL_ERROR');
|
|
100
|
+
}
|
|
101
|
+
const encoded = `${line}\n`;
|
|
102
|
+
if (byteLength(encoded) > maxFrameBytes) {
|
|
103
|
+
throw new PersistentTransportError('Persistent transport input frame exceeds the configured limit', 'FRAME_TOO_LARGE');
|
|
104
|
+
}
|
|
105
|
+
return encoded;
|
|
106
|
+
}
|
|
107
|
+
/** Parse and validate the small protocol envelope; payload remains opaque. */
|
|
108
|
+
export function parsePersistentFrame(line) {
|
|
109
|
+
if (line.trim().length === 0) {
|
|
110
|
+
throw new PersistentTransportError('Persistent transport emitted an empty frame', 'PROTOCOL_ERROR');
|
|
111
|
+
}
|
|
112
|
+
let value;
|
|
113
|
+
try {
|
|
114
|
+
value = JSON.parse(line);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throw new PersistentTransportError('Persistent transport emitted malformed JSON', 'PROTOCOL_ERROR', {
|
|
118
|
+
cause: error,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (!isRecord(value) || typeof value.kind !== 'string'
|
|
122
|
+
|| !['ready', 'event', 'complete', 'error'].includes(value.kind)) {
|
|
123
|
+
throw new PersistentTransportError('Persistent transport emitted an invalid frame envelope', 'PROTOCOL_ERROR');
|
|
124
|
+
}
|
|
125
|
+
if (value.requestId !== undefined && typeof value.requestId !== 'string') {
|
|
126
|
+
throw new PersistentTransportError('Persistent transport frame requestId is invalid', 'PROTOCOL_ERROR');
|
|
127
|
+
}
|
|
128
|
+
if (value.sessionId !== undefined && typeof value.sessionId !== 'string') {
|
|
129
|
+
throw new PersistentTransportError('Persistent transport frame sessionId is invalid', 'PROTOCOL_ERROR');
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
kind: value.kind,
|
|
133
|
+
...(value.requestId === undefined ? {} : { requestId: value.requestId }),
|
|
134
|
+
...(value.sessionId === undefined ? {} : { sessionId: value.sessionId }),
|
|
135
|
+
...(Object.hasOwn(value, 'payload') ? { payload: value.payload } : {}),
|
|
136
|
+
...(value.message === undefined ? {} : { message: String(value.message).slice(0, 256) }),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Real AGY 1.1.15 output envelope: init | step_update | result */
|
|
140
|
+
function parseAgyOutputLine(line) {
|
|
141
|
+
if (line.trim().length === 0) {
|
|
142
|
+
throw new PersistentTransportError('Persistent AGY emitted an empty line', 'PROTOCOL_ERROR');
|
|
143
|
+
}
|
|
144
|
+
let value;
|
|
145
|
+
try {
|
|
146
|
+
value = JSON.parse(line);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
throw new PersistentTransportError('Persistent AGY emitted malformed JSON', 'PROTOCOL_ERROR', { cause: error });
|
|
150
|
+
}
|
|
151
|
+
if (!isRecord(value) || typeof value.event !== 'string') {
|
|
152
|
+
throw new PersistentTransportError('Persistent AGY output missing event field', 'PROTOCOL_ERROR');
|
|
153
|
+
}
|
|
154
|
+
return { event: value.event, raw: value };
|
|
155
|
+
}
|
|
156
|
+
function terminateWorkerTree(child) {
|
|
157
|
+
const pid = child.pid;
|
|
158
|
+
if (pid === undefined) {
|
|
159
|
+
child.kill();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (process.platform === 'win32') {
|
|
163
|
+
const killer = spawn('taskkill', ['/pid', String(pid), '/t', '/f'], {
|
|
164
|
+
shell: false,
|
|
165
|
+
windowsHide: true,
|
|
166
|
+
stdio: 'ignore',
|
|
167
|
+
});
|
|
168
|
+
const fallback = setTimeout(() => {
|
|
169
|
+
try {
|
|
170
|
+
child.kill();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// The child may have exited while taskkill was traversing the tree.
|
|
174
|
+
}
|
|
175
|
+
}, 250);
|
|
176
|
+
try {
|
|
177
|
+
// Ask the direct child to close immediately so the parent can finish
|
|
178
|
+
// its lifecycle without waiting for taskkill's process to exit.
|
|
179
|
+
child.kill();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// taskkill remains responsible for descendants and the close handler
|
|
183
|
+
// still completes the worker state transition.
|
|
184
|
+
}
|
|
185
|
+
killer.once('error', () => {
|
|
186
|
+
clearTimeout(fallback);
|
|
187
|
+
try {
|
|
188
|
+
child.kill();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// The child may already be closed.
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
killer.once('close', () => clearTimeout(fallback));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
process.kill(-pid, 'SIGKILL');
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
child.kill('SIGKILL');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function errorFromUnknown(error, fallbackCode, fallbackMessage) {
|
|
205
|
+
if (error instanceof PersistentTransportError)
|
|
206
|
+
return error;
|
|
207
|
+
return new PersistentTransportError(fallbackMessage, fallbackCode, {
|
|
208
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
class PersistentAgyWorker {
|
|
212
|
+
options;
|
|
213
|
+
sessionId;
|
|
214
|
+
workerKey;
|
|
215
|
+
/** Route args fixed at spawn time; a different route needs a new process. */
|
|
216
|
+
routeArgs;
|
|
217
|
+
/** Conversation this worker was asked to continue, if any. */
|
|
218
|
+
seedConversation;
|
|
219
|
+
onStopped;
|
|
220
|
+
state = 'starting';
|
|
221
|
+
child;
|
|
222
|
+
active;
|
|
223
|
+
conversationId;
|
|
224
|
+
idleTimer;
|
|
225
|
+
startPromise;
|
|
226
|
+
closePromise = Promise.resolve();
|
|
227
|
+
closeResolve;
|
|
228
|
+
closed = true;
|
|
229
|
+
notifiedStopped = false;
|
|
230
|
+
stdoutBuffer = '';
|
|
231
|
+
stderrBytes = 0;
|
|
232
|
+
readyResolve;
|
|
233
|
+
readyReject;
|
|
234
|
+
readySettled = false;
|
|
235
|
+
constructor(sessionId, workerKey, input, options, onStopped) {
|
|
236
|
+
this.sessionId = sessionId;
|
|
237
|
+
this.workerKey = workerKey;
|
|
238
|
+
this.routeArgs = routeArgs(input);
|
|
239
|
+
this.seedConversation = input.conversation;
|
|
240
|
+
this.options = options;
|
|
241
|
+
this.onStopped = onStopped;
|
|
242
|
+
}
|
|
243
|
+
/** The pool key this worker was created for; see workerKeyOf(). */
|
|
244
|
+
getWorkerKey() {
|
|
245
|
+
return this.workerKey;
|
|
246
|
+
}
|
|
247
|
+
/** True when this worker already exists and can serve the given route. */
|
|
248
|
+
matchesRoute(input) {
|
|
249
|
+
return workerKeyOf(input) === this.workerKey;
|
|
250
|
+
}
|
|
251
|
+
getState() {
|
|
252
|
+
return this.state;
|
|
253
|
+
}
|
|
254
|
+
async start() {
|
|
255
|
+
if (this.state === 'stopped' || this.state === 'stopping') {
|
|
256
|
+
throw new PersistentTransportError('Persistent AGY worker is stopped', 'WORKER_STOPPED');
|
|
257
|
+
}
|
|
258
|
+
if (this.state === 'idle' || this.state === 'busy')
|
|
259
|
+
return;
|
|
260
|
+
if (this.startPromise !== undefined)
|
|
261
|
+
return this.startPromise;
|
|
262
|
+
this.startPromise = this.spawnAndWaitForReady();
|
|
263
|
+
try {
|
|
264
|
+
await this.startPromise;
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
this.startPromise = undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async request(input) {
|
|
271
|
+
await this.start();
|
|
272
|
+
if (this.state !== 'idle') {
|
|
273
|
+
throw new PersistentTransportError('Persistent AGY worker already has an active request', 'WORKER_BUSY');
|
|
274
|
+
}
|
|
275
|
+
if (input.signal?.aborted) {
|
|
276
|
+
await this.stop('reset');
|
|
277
|
+
throw new PersistentTransportError('Persistent AGY request aborted before write', 'ABORTED');
|
|
278
|
+
}
|
|
279
|
+
const timeoutMs = input.timeoutMs ?? this.options.defaultRequestTimeoutMs;
|
|
280
|
+
validateInteger('timeoutMs', timeoutMs, 1, 3_600_000);
|
|
281
|
+
const idleTimeoutMs = input.idleTimeoutMs ?? timeoutMs;
|
|
282
|
+
validateInteger('idleTimeoutMs', idleTimeoutMs, 1, 3_600_000);
|
|
283
|
+
const maxOutputBytes = Math.min(input.maxOutputBytes ?? this.options.maxOutputBytes, this.options.maxOutputBytes);
|
|
284
|
+
validateInteger('maxOutputBytes', maxOutputBytes, 1, this.options.maxOutputBytes);
|
|
285
|
+
this.clearIdleTimer();
|
|
286
|
+
this.state = 'busy';
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
288
|
+
const active = {
|
|
289
|
+
requestId: randomUUID(),
|
|
290
|
+
sessionId: this.sessionId,
|
|
291
|
+
startedAt: Date.now(),
|
|
292
|
+
maxOutputBytes,
|
|
293
|
+
events: [],
|
|
294
|
+
onEvent: input.onEvent,
|
|
295
|
+
signal: input.signal,
|
|
296
|
+
resolve,
|
|
297
|
+
reject,
|
|
298
|
+
outputBytes: 0,
|
|
299
|
+
firstEventAt: undefined,
|
|
300
|
+
timer: undefined,
|
|
301
|
+
idleTimer: undefined,
|
|
302
|
+
idleTimeoutMs,
|
|
303
|
+
abortListener: undefined,
|
|
304
|
+
};
|
|
305
|
+
this.active = active;
|
|
306
|
+
active.abortListener = () => this.failActiveAndReset(new PersistentTransportError('Persistent AGY request aborted', 'ABORTED'));
|
|
307
|
+
input.signal?.addEventListener('abort', active.abortListener, { once: true });
|
|
308
|
+
// Two independent clocks: the wall clock is a hard ceiling, the idle clock
|
|
309
|
+
// is re-armed by every frame so a long thinking or tool pause is not a kill.
|
|
310
|
+
const wallClock = setTimeout(() => this.failActiveAndReset(new PersistentTransportError('Persistent AGY request exceeded its wall-clock timeout', 'TIMEOUT')), timeoutMs);
|
|
311
|
+
active.timer = wallClock;
|
|
312
|
+
this.touchActive(active);
|
|
313
|
+
void this.sendRequest(active, input.text);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async stop(reason = 'dispose') {
|
|
317
|
+
if (this.state === 'stopped')
|
|
318
|
+
return;
|
|
319
|
+
if (this.state === 'stopping') {
|
|
320
|
+
await this.closePromise;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.clearIdleTimer();
|
|
324
|
+
if (this.state === 'starting') {
|
|
325
|
+
this.rejectReady(reason === 'dispose'
|
|
326
|
+
? new PersistentTransportError('Persistent AGY transport was disposed during worker startup', 'DISPOSED')
|
|
327
|
+
: new PersistentTransportError('Persistent AGY worker stopped before startup completed', 'WORKER_STOPPED'));
|
|
328
|
+
}
|
|
329
|
+
if (this.active !== undefined) {
|
|
330
|
+
this.settleActiveError(new PersistentTransportError(reason === 'dispose'
|
|
331
|
+
? 'Persistent AGY transport was disposed'
|
|
332
|
+
: 'Persistent AGY worker was stopped before request completion', reason === 'dispose' ? 'DISPOSED' : 'WORKER_STOPPED'));
|
|
333
|
+
}
|
|
334
|
+
this.state = 'stopping';
|
|
335
|
+
const child = this.child;
|
|
336
|
+
if (child === undefined || this.closed) {
|
|
337
|
+
this.state = 'stopped';
|
|
338
|
+
this.notifyStopped();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (reason === 'dispose' || reason === 'idle') {
|
|
342
|
+
try {
|
|
343
|
+
child.stdin.end();
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// The close handler below still performs the final state transition.
|
|
347
|
+
}
|
|
348
|
+
await Promise.race([this.closePromise, delay(this.options.shutdownTimeoutMs)]);
|
|
349
|
+
}
|
|
350
|
+
if (!this.closed)
|
|
351
|
+
terminateWorkerTree(child);
|
|
352
|
+
await this.closePromise;
|
|
353
|
+
}
|
|
354
|
+
async spawnAndWaitForReady() {
|
|
355
|
+
this.state = 'starting';
|
|
356
|
+
this.closed = false;
|
|
357
|
+
this.stdoutBuffer = '';
|
|
358
|
+
this.stderrBytes = 0;
|
|
359
|
+
this.readySettled = false;
|
|
360
|
+
this.closePromise = new Promise(resolve => { this.closeResolve = resolve; });
|
|
361
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
362
|
+
this.readyResolve = resolve;
|
|
363
|
+
this.readyReject = reject;
|
|
364
|
+
});
|
|
365
|
+
try {
|
|
366
|
+
const args = [...this.options.args, ...this.routeArgs];
|
|
367
|
+
const launch = process.platform === 'win32'
|
|
368
|
+
? buildWindowsNoConsoleLaunch(this.options.executable, args)
|
|
369
|
+
: { executable: this.options.executable, args };
|
|
370
|
+
const child = spawn(launch.executable, [...launch.args], {
|
|
371
|
+
cwd: this.options.cwd,
|
|
372
|
+
env: {
|
|
373
|
+
...process.env,
|
|
374
|
+
...this.options.env,
|
|
375
|
+
DSH_AGY_EXPERIMENTAL_SESSION: this.sessionId,
|
|
376
|
+
},
|
|
377
|
+
shell: false,
|
|
378
|
+
detached: process.platform !== 'win32',
|
|
379
|
+
windowsHide: true,
|
|
380
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
381
|
+
});
|
|
382
|
+
this.child = child;
|
|
383
|
+
this.attachChild(child);
|
|
384
|
+
const timeout = setTimeout(() => {
|
|
385
|
+
this.rejectReady(new PersistentTransportError('Persistent AGY worker did not become ready in time', 'WORKER_START_FAILED'));
|
|
386
|
+
}, this.options.readyTimeoutMs);
|
|
387
|
+
try {
|
|
388
|
+
await readyPromise;
|
|
389
|
+
}
|
|
390
|
+
finally {
|
|
391
|
+
clearTimeout(timeout);
|
|
392
|
+
}
|
|
393
|
+
const stateAfterReady = this.getState();
|
|
394
|
+
if (stateAfterReady === 'stopped' || stateAfterReady === 'stopping') {
|
|
395
|
+
throw new PersistentTransportError('Persistent AGY worker stopped during startup', 'WORKER_START_FAILED');
|
|
396
|
+
}
|
|
397
|
+
this.state = 'idle';
|
|
398
|
+
this.armIdleTimer();
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
const mapped = errorFromUnknown(error, 'WORKER_START_FAILED', 'Persistent AGY worker failed to start');
|
|
402
|
+
await this.stop('reset');
|
|
403
|
+
throw mapped;
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
this.readyResolve = undefined;
|
|
407
|
+
this.readyReject = undefined;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
attachChild(child) {
|
|
411
|
+
child.stdout.setEncoding('utf8');
|
|
412
|
+
child.stderr.setEncoding('utf8');
|
|
413
|
+
child.stdout.on('data', (chunk) => this.handleStdout(chunk));
|
|
414
|
+
child.stderr.on('data', (chunk) => {
|
|
415
|
+
this.stderrBytes += byteLength(chunk);
|
|
416
|
+
if (this.stderrBytes > this.options.maxStderrBytes) {
|
|
417
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker stderr exceeded its capture limit', 'OUTPUT_LIMIT'));
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
child.once('error', error => {
|
|
421
|
+
const code = this.state === 'starting'
|
|
422
|
+
? 'WORKER_START_FAILED'
|
|
423
|
+
: 'WORKER_CRASHED';
|
|
424
|
+
const mapped = new PersistentTransportError(code === 'WORKER_START_FAILED'
|
|
425
|
+
? 'Persistent AGY worker could not be started'
|
|
426
|
+
: 'Persistent AGY worker process failed', code, { cause: error });
|
|
427
|
+
this.rejectReady(mapped);
|
|
428
|
+
if (this.active !== undefined)
|
|
429
|
+
this.settleActiveError(mapped);
|
|
430
|
+
});
|
|
431
|
+
child.once('close', (exitCode, signal) => {
|
|
432
|
+
this.closed = true;
|
|
433
|
+
this.clearIdleTimer();
|
|
434
|
+
if (!this.readySettled) {
|
|
435
|
+
this.rejectReady(new PersistentTransportError('Persistent AGY worker exited before ready', 'WORKER_START_FAILED'));
|
|
436
|
+
}
|
|
437
|
+
if (this.active !== undefined) {
|
|
438
|
+
this.settleActiveError(new PersistentTransportError(`Persistent AGY worker exited before request completion (${exitCode ?? 'signal'})`, 'WORKER_CRASHED', { cause: signal === null ? undefined : new Error(signal) }));
|
|
439
|
+
}
|
|
440
|
+
this.state = 'stopped';
|
|
441
|
+
this.closeResolve?.();
|
|
442
|
+
this.closeResolve = undefined;
|
|
443
|
+
this.notifyStopped();
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
handleStdout(chunk) {
|
|
447
|
+
this.stdoutBuffer += chunk;
|
|
448
|
+
// A runaway child can emit one endless line with no newline; without this
|
|
449
|
+
// check the un-split buffer grows past maxOutputBytes unchecked (the byte
|
|
450
|
+
// valve only runs on completed lines).
|
|
451
|
+
if (this.stdoutBuffer.length > this.options.maxFrameBytes) {
|
|
452
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY emitted a stdout line beyond the frame limit', 'OUTPUT_LIMIT'));
|
|
453
|
+
this.stdoutBuffer = '';
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
let newlineIndex = this.stdoutBuffer.indexOf('\n');
|
|
457
|
+
while (newlineIndex >= 0) {
|
|
458
|
+
const rawLine = this.stdoutBuffer.slice(0, newlineIndex);
|
|
459
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
460
|
+
this.handleLine(rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine);
|
|
461
|
+
newlineIndex = this.stdoutBuffer.indexOf('\n');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
handleLine(line) {
|
|
465
|
+
let parsed;
|
|
466
|
+
try {
|
|
467
|
+
parsed = parseAgyOutputLine(line);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
this.failActiveAndReset(errorFromUnknown(error, 'PROTOCOL_ERROR', 'Persistent AGY worker emitted an invalid frame'));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const event = parsed.event;
|
|
474
|
+
const raw = parsed.raw;
|
|
475
|
+
// init is the ready signal (emitted once at worker start)
|
|
476
|
+
if (event === 'init') {
|
|
477
|
+
const convId = typeof raw.conversation_id === 'string' ? raw.conversation_id : undefined;
|
|
478
|
+
if (convId !== undefined)
|
|
479
|
+
this.conversationId = convId;
|
|
480
|
+
// A worker asked to continue conversation X that comes up as Y would
|
|
481
|
+
// answer the turn with no history at all while looking healthy. Fail the
|
|
482
|
+
// request before any delta so the caller can resend full history.
|
|
483
|
+
if (this.seedConversation !== undefined && convId !== undefined && convId !== this.seedConversation) {
|
|
484
|
+
this.rejectReady(new PersistentTransportError(`Persistent AGY worker was asked to continue conversation ${this.seedConversation} but started ${convId}`, 'CONVERSATION_MISMATCH'));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
this.resolveReady();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const active = this.active;
|
|
491
|
+
if (active === undefined) {
|
|
492
|
+
// No active request: ignore stray step_update/result that belong to previous turn already settled
|
|
493
|
+
// But if we get a result without active, treat as protocol error and reset
|
|
494
|
+
if (event === 'result') {
|
|
495
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker emitted a result without an active request', 'PROTOCOL_ERROR'));
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
// Track output size
|
|
500
|
+
active.outputBytes += byteLength(`${line}\n`);
|
|
501
|
+
if (active.outputBytes > active.maxOutputBytes) {
|
|
502
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker response exceeded its capture limit', 'OUTPUT_LIMIT'));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// A line arrived: the worker is alive, so the inactivity clock restarts.
|
|
506
|
+
this.touchActive(active);
|
|
507
|
+
// Verify conversation_id correlation when present
|
|
508
|
+
const resultConv = raw.result?.conversation_id
|
|
509
|
+
?? raw.step_update?.conversation_id;
|
|
510
|
+
if (typeof resultConv === 'string' && this.conversationId !== undefined && resultConv !== this.conversationId) {
|
|
511
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker conversation_id mismatch', 'PROTOCOL_ERROR'));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (event === 'step_update') {
|
|
515
|
+
active.firstEventAt ??= Date.now();
|
|
516
|
+
active.events.push(raw);
|
|
517
|
+
try {
|
|
518
|
+
active.onEvent?.(raw);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker event handler failed', 'EVENT_HANDLER_FAILED', { cause: error instanceof Error ? error : new Error(String(error)) }));
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (event === 'result') {
|
|
526
|
+
const result = raw.result;
|
|
527
|
+
const status = typeof result?.status === 'string' ? result.status : undefined;
|
|
528
|
+
if (status !== 'SUCCESS') {
|
|
529
|
+
const errMsg = typeof result?.error === 'string' ? result.error : 'AGY result status ' + status;
|
|
530
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker returned error result: ' + errMsg.slice(0, 256), 'WORKER_ERROR'));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
active.firstEventAt ??= Date.now();
|
|
534
|
+
active.events.push(raw);
|
|
535
|
+
try {
|
|
536
|
+
active.onEvent?.(raw);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker event handler failed', 'EVENT_HANDLER_FAILED', { cause: error instanceof Error ? error : new Error(String(error)) }));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
this.settleActiveSuccess();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
// Unknown event: treat as protocol error
|
|
546
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker emitted unknown event: ' + event, 'PROTOCOL_ERROR'));
|
|
547
|
+
}
|
|
548
|
+
async sendRequest(active, text) {
|
|
549
|
+
try {
|
|
550
|
+
const frame = encodeAgyUserMessage(text, this.options.maxFrameBytes);
|
|
551
|
+
await this.writeFrame(frame);
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
if (this.active !== active)
|
|
555
|
+
return;
|
|
556
|
+
this.failActiveAndReset(errorFromUnknown(error, 'PROTOCOL_ERROR', 'Persistent AGY request could not be written'));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async writeFrame(frame) {
|
|
560
|
+
const stdin = this.child?.stdin;
|
|
561
|
+
if (stdin === undefined || !stdin.writable) {
|
|
562
|
+
throw new PersistentTransportError('Persistent AGY worker stdin is closed', 'WORKER_STOPPED');
|
|
563
|
+
}
|
|
564
|
+
if (stdin.write(frame))
|
|
565
|
+
return;
|
|
566
|
+
await new Promise((resolve, reject) => {
|
|
567
|
+
const onDrain = () => {
|
|
568
|
+
cleanup();
|
|
569
|
+
resolve();
|
|
570
|
+
};
|
|
571
|
+
const onError = (error) => {
|
|
572
|
+
cleanup();
|
|
573
|
+
reject(error);
|
|
574
|
+
};
|
|
575
|
+
// A worker torn down mid-drain ends stdin; Node never emits 'drain' on
|
|
576
|
+
// an ended stream, so without a close listener this promise would hang
|
|
577
|
+
// forever and leak its callers.
|
|
578
|
+
const onClose = () => {
|
|
579
|
+
cleanup();
|
|
580
|
+
reject(new PersistentTransportError('Persistent AGY worker stdin closed during write', 'WORKER_STOPPED'));
|
|
581
|
+
};
|
|
582
|
+
const cleanup = () => {
|
|
583
|
+
stdin.removeListener('drain', onDrain);
|
|
584
|
+
stdin.removeListener('error', onError);
|
|
585
|
+
stdin.removeListener('close', onClose);
|
|
586
|
+
};
|
|
587
|
+
stdin.once('drain', onDrain);
|
|
588
|
+
stdin.once('error', onError);
|
|
589
|
+
stdin.once('close', onClose);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
settleActiveSuccess() {
|
|
593
|
+
const active = this.active;
|
|
594
|
+
if (active === undefined)
|
|
595
|
+
return;
|
|
596
|
+
this.active = undefined;
|
|
597
|
+
this.clearActive(active);
|
|
598
|
+
this.state = 'idle';
|
|
599
|
+
this.armIdleTimer();
|
|
600
|
+
active.resolve({
|
|
601
|
+
requestId: active.requestId,
|
|
602
|
+
sessionId: active.sessionId,
|
|
603
|
+
events: [...active.events],
|
|
604
|
+
durationMs: Date.now() - active.startedAt,
|
|
605
|
+
...(active.firstEventAt === undefined ? {} : { firstEventMs: active.firstEventAt - active.startedAt }),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
settleActiveError(error) {
|
|
609
|
+
const active = this.active;
|
|
610
|
+
if (active === undefined)
|
|
611
|
+
return;
|
|
612
|
+
this.active = undefined;
|
|
613
|
+
this.clearActive(active);
|
|
614
|
+
active.reject(error);
|
|
615
|
+
}
|
|
616
|
+
clearActive(active) {
|
|
617
|
+
if (active.timer !== undefined)
|
|
618
|
+
clearTimeout(active.timer);
|
|
619
|
+
if (active.idleTimer !== undefined)
|
|
620
|
+
clearTimeout(active.idleTimer);
|
|
621
|
+
if (active.abortListener !== undefined)
|
|
622
|
+
active.signal?.removeEventListener('abort', active.abortListener);
|
|
623
|
+
active.abortListener = undefined;
|
|
624
|
+
}
|
|
625
|
+
/** Re-arm the inactivity clock; every frame proves the worker is alive. */
|
|
626
|
+
touchActive(active) {
|
|
627
|
+
const idleTimeoutMs = active.idleTimeoutMs;
|
|
628
|
+
if (idleTimeoutMs === undefined)
|
|
629
|
+
return;
|
|
630
|
+
if (active.idleTimer !== undefined)
|
|
631
|
+
clearTimeout(active.idleTimer);
|
|
632
|
+
active.idleTimer = setTimeout(() => this.failActiveAndReset(new PersistentTransportError(`Persistent AGY request produced no output for ${idleTimeoutMs} ms`, 'TIMEOUT')), idleTimeoutMs);
|
|
633
|
+
}
|
|
634
|
+
failActiveAndReset(error) {
|
|
635
|
+
this.settleActiveError(error);
|
|
636
|
+
void this.stop('reset');
|
|
637
|
+
}
|
|
638
|
+
resolveReady() {
|
|
639
|
+
if (this.readySettled)
|
|
640
|
+
return;
|
|
641
|
+
this.readySettled = true;
|
|
642
|
+
this.readyResolve?.();
|
|
643
|
+
}
|
|
644
|
+
rejectReady(error) {
|
|
645
|
+
if (this.readySettled)
|
|
646
|
+
return;
|
|
647
|
+
this.readySettled = true;
|
|
648
|
+
this.readyReject?.(error);
|
|
649
|
+
}
|
|
650
|
+
armIdleTimer() {
|
|
651
|
+
this.clearIdleTimer();
|
|
652
|
+
if (this.options.idleTtlMs === 0 || this.state !== 'idle')
|
|
653
|
+
return;
|
|
654
|
+
this.idleTimer = setTimeout(() => { void this.stop('idle'); }, this.options.idleTtlMs);
|
|
655
|
+
}
|
|
656
|
+
clearIdleTimer() {
|
|
657
|
+
if (this.idleTimer !== undefined)
|
|
658
|
+
clearTimeout(this.idleTimer);
|
|
659
|
+
this.idleTimer = undefined;
|
|
660
|
+
}
|
|
661
|
+
notifyStopped() {
|
|
662
|
+
if (this.notifiedStopped)
|
|
663
|
+
return;
|
|
664
|
+
this.notifiedStopped = true;
|
|
665
|
+
this.onStopped(this);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Worker-per-route persistent transport. One AGY process per (session, route);
|
|
670
|
+
* the worker holds a single active turn and is keyed by the route because AGY
|
|
671
|
+
* fixes --agent/--model/--effort at process start.
|
|
672
|
+
*/
|
|
673
|
+
export class ExperimentalAgyTransport {
|
|
674
|
+
options;
|
|
675
|
+
workers = new Map();
|
|
676
|
+
disposed = false;
|
|
677
|
+
constructor(options) {
|
|
678
|
+
this.options = normalizeOptions(options);
|
|
679
|
+
}
|
|
680
|
+
async request(input) {
|
|
681
|
+
if (this.disposed) {
|
|
682
|
+
throw new PersistentTransportError('Persistent AGY transport is disposed', 'DISPOSED');
|
|
683
|
+
}
|
|
684
|
+
if (input.sessionId.trim().length === 0) {
|
|
685
|
+
throw new RangeError('sessionId must not be empty');
|
|
686
|
+
}
|
|
687
|
+
const key = workerKeyOf(input);
|
|
688
|
+
let worker = this.workers.get(key);
|
|
689
|
+
if (worker?.getState() === 'stopped') {
|
|
690
|
+
this.workers.delete(key);
|
|
691
|
+
worker = undefined;
|
|
692
|
+
}
|
|
693
|
+
if (worker === undefined) {
|
|
694
|
+
if (this.workers.size >= this.options.maxWorkers) {
|
|
695
|
+
throw new PersistentTransportError('Persistent AGY worker limit reached', 'WORKER_LIMIT');
|
|
696
|
+
}
|
|
697
|
+
worker = new PersistentAgyWorker(input.sessionId, key, input, this.options, stoppedWorker => {
|
|
698
|
+
if (this.workers.get(key) === stoppedWorker)
|
|
699
|
+
this.workers.delete(key);
|
|
700
|
+
});
|
|
701
|
+
this.workers.set(key, worker);
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
return await worker.request(input);
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
if (worker.getState() === 'stopped')
|
|
708
|
+
this.workers.delete(key);
|
|
709
|
+
throw error;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async dispose() {
|
|
713
|
+
if (this.disposed)
|
|
714
|
+
return;
|
|
715
|
+
this.disposed = true;
|
|
716
|
+
const workers = [...this.workers.values()];
|
|
717
|
+
await Promise.all(workers.map(worker => worker.stop('dispose')));
|
|
718
|
+
this.workers.clear();
|
|
719
|
+
}
|
|
720
|
+
getStats() {
|
|
721
|
+
let startingWorkers = 0;
|
|
722
|
+
let idleWorkers = 0;
|
|
723
|
+
let busyWorkers = 0;
|
|
724
|
+
let stoppingWorkers = 0;
|
|
725
|
+
for (const worker of this.workers.values()) {
|
|
726
|
+
switch (worker.getState()) {
|
|
727
|
+
case 'starting':
|
|
728
|
+
startingWorkers += 1;
|
|
729
|
+
break;
|
|
730
|
+
case 'idle':
|
|
731
|
+
idleWorkers += 1;
|
|
732
|
+
break;
|
|
733
|
+
case 'busy':
|
|
734
|
+
busyWorkers += 1;
|
|
735
|
+
break;
|
|
736
|
+
case 'stopping':
|
|
737
|
+
stoppingWorkers += 1;
|
|
738
|
+
break;
|
|
739
|
+
case 'stopped': break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
totalWorkers: this.workers.size,
|
|
744
|
+
startingWorkers,
|
|
745
|
+
idleWorkers,
|
|
746
|
+
busyWorkers,
|
|
747
|
+
stoppingWorkers,
|
|
748
|
+
maxWorkers: this.options.maxWorkers,
|
|
749
|
+
disposed: this.disposed,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
//# sourceMappingURL=persistent-transport.js.map
|