@dsh-enhanced/traex-acp-provider 0.1.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.
@@ -0,0 +1,1216 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { isUtf8 } from 'node:buffer';
3
+ import { isAbsolute } from 'node:path';
4
+ import { Readable, Transform, Writable } from 'node:stream';
5
+ import { ClientSideConnection, PROTOCOL_VERSION, RequestError, ndJsonStream, } from '@agentclientprotocol/sdk';
6
+ import { zAuthMethodAgent, zCloseSessionResponse, zInitializeResponse, zNewSessionResponse, zPromptResponse, zRequestPermissionRequest, zRequestPermissionResponse, zSessionNotification, zSessionConfigOption, zSetSessionConfigOptionResponse, } from '@agentclientprotocol/sdk/dist/schema/zod.gen.js';
7
+ import { version } from './version.js';
8
+ /**
9
+ * The explicit, unmet preconditions that must ALL be proven true before an `AcpUsageSnapshot` may be
10
+ * mapped to a DSH `TokenUsage` and emitted as a usage chunk. This is a verification gate, not a
11
+ * to-do list: until each item is confirmed against a real TraeX fixture or an official field
12
+ * contract, the snapshot stays an internal diagnostic and no usage chunk is emitted.
13
+ *
14
+ * Grounding (DSH contract, `@deepseek-ai/dsh-llm` `types.d.ts`):
15
+ * - `inputTokens`/`outputTokens` are REQUIRED numbers and must be per-call, not cumulative.
16
+ * - `inputTokens` must be UNCACHED input only; cache is reported separately.
17
+ * - a real zero must be preserved; only genuinely-absent optionals stay undefined.
18
+ */
19
+ export const ACP_USAGE_DSH_MAPPING_GATE = Object.freeze([
20
+ // ACP usage is documented as cumulative "across all turns"; DSH TokenUsage is per-call. A single
21
+ // fresh session/one-turn invocation may make these coincide, but that must be proven, not assumed.
22
+ 'per-call-vs-cumulative: prove the snapshot reflects THIS call, not a running total across turns',
23
+ // DSH inputTokens excludes cached input; ACP inputTokens may fold cache in. If it does, the cached
24
+ // portion must be subtracted out, which requires knowing ACP's exact accounting.
25
+ 'uncached-input: prove ACP inputTokens excludes cache, or derive uncached input exactly',
26
+ // The cache split must line up field-for-field with DSH cacheReadTokens/cacheWriteTokens.
27
+ 'cache-fields: prove cachedReadTokens/cachedWriteTokens match DSH cache-read/-write semantics',
28
+ // reasoningTokens has no confirmed ACP source; thoughtTokens is not proven equivalent.
29
+ 'reasoning-mapping: prove (or decline) thoughtTokens -> DSH reasoningTokens equivalence',
30
+ ]);
31
+ /** Stable, credential-free failure surfaced by the TraeX ACP transport. */
32
+ export class TraexAcpError extends Error {
33
+ failure;
34
+ systemCode;
35
+ code = 'TRAEX_ACP_ERROR';
36
+ constructor(failure, message = failureMessage(failure),
37
+ /** Original OS error code only; the untrusted process error text is never exposed. */
38
+ systemCode) {
39
+ super(message, { cause: failure });
40
+ this.failure = failure;
41
+ this.systemCode = systemCode;
42
+ this.name = 'TraexAcpError';
43
+ }
44
+ }
45
+ const SAFE_ARGS = [
46
+ '--sandbox',
47
+ 'read-only',
48
+ '--ask-for-approval',
49
+ 'never',
50
+ 'acp',
51
+ 'serve',
52
+ ];
53
+ const defaults = {
54
+ timeoutMs: 10 * 60_000,
55
+ killGraceMs: 3_000,
56
+ maxMessageBytes: 256 * 1024,
57
+ maxProtocolBytes: 16 * 1024 * 1024,
58
+ maxProtocolMessages: 10_000,
59
+ maxOutputBytes: 512 * 1024,
60
+ maxStderrBytes: 32 * 1024,
61
+ };
62
+ const inheritedEnvNames = new Set([
63
+ 'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'USERPROFILE',
64
+ 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_RUNTIME_DIR',
65
+ 'TMPDIR', 'TMP', 'TEMP', 'LANG', 'LC_ALL', 'DBUS_SESSION_BUS_ADDRESS',
66
+ 'SystemRoot', 'WINDIR', 'ComSpec', 'PATHEXT', 'APPDATA', 'LOCALAPPDATA',
67
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
68
+ 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR', 'REQUESTS_CA_BUNDLE',
69
+ ]);
70
+ const secretEnvName = /(?:^|_)(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?|AUTH|AUTHORIZATION|BEARER|COOKIE|PRIVATE_KEY|ACCESS_KEY_ID|GITHUB_PAT|DATABASE_URL|DB_URL|CONNECTION_STRING|DSN)(?:_|$)/i;
71
+ const providerEndpointEnvName = /^(?:OPENAI|CODEX|AZURE_OPENAI|ANTHROPIC|XAI)_(?:BASE_URL|API_BASE|API_BASE_URL|ENDPOINT)$/i;
72
+ const proxyEnvName = /^(?:HTTP|HTTPS|ALL)_PROXY$/i;
73
+ const envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
74
+ const defaultSpawn = (command, args, options) => nodeSpawn(command, [...args], {
75
+ ...options,
76
+ stdio: [...options.stdio],
77
+ });
78
+ /**
79
+ * Runs a single TraeX ACP prompt and yields only text explicitly reported as
80
+ * `agent_message_chunk` for the newly-created session. The transport exposes no
81
+ * filesystem or terminal capability, rejects every permission request, never
82
+ * invokes a shell, and never performs an interactive authentication flow.
83
+ */
84
+ export async function* runTraexAcpText(invocation, options = {}) {
85
+ // Pre-spawn failures never handed the prompt to the ACP stream: report not-submitted and stop.
86
+ const reportPreSpawn = (failedPhase) => {
87
+ if (options.onSettled === undefined)
88
+ return;
89
+ try {
90
+ options.onSettled({ phase: failedPhase, promptSubmissionState: 'not-submitted', assistantTextObserved: false, teardownState: 'not-started' });
91
+ }
92
+ catch {
93
+ // A diagnostic sink must never change model-call settlement.
94
+ }
95
+ };
96
+ let limits;
97
+ try {
98
+ limits = validate(invocation, options);
99
+ }
100
+ catch (error) {
101
+ reportPreSpawn('initialize');
102
+ throw error;
103
+ }
104
+ if (options.signal?.aborted) {
105
+ reportPreSpawn('initialize');
106
+ throw new TraexAcpError('abort');
107
+ }
108
+ let child;
109
+ try {
110
+ child = (options.spawn ?? defaultSpawn)(invocation.command, invocation.args, {
111
+ cwd: invocation.cwd,
112
+ shell: false,
113
+ detached: process.platform !== 'win32',
114
+ stdio: ['pipe', 'pipe', 'pipe'],
115
+ env: buildTraexEnv(options.extraEnvNames),
116
+ });
117
+ }
118
+ catch (error) {
119
+ reportPreSpawn('initialize');
120
+ throw processFailure(error);
121
+ }
122
+ const queue = new TextQueue();
123
+ const rpcGuard = new AcpJsonRpcGuard();
124
+ let client;
125
+ let sessionId;
126
+ let outputBytes = 0;
127
+ let sawAssistantText = false;
128
+ let promptActive = false;
129
+ let turnFinished = false;
130
+ let operationFinished = false;
131
+ let terminating = false;
132
+ let fatal;
133
+ let closeResult;
134
+ let resolveClosed;
135
+ let shutdownPromise;
136
+ const childClosed = new Promise(resolve => { resolveClosed = resolve; });
137
+ const stderr = new BoundedBytes(limits.maxStderrBytes);
138
+ // Lifecycle facts reported through the optional diagnostic seam; never affect settlement.
139
+ let phase = 'initialize';
140
+ let promptSubmissionState = 'not-submitted';
141
+ let teardownState = 'not-started';
142
+ let terminalReason;
143
+ let reportedContext = false;
144
+ let usageSnapshot;
145
+ const failureRaised = Promise.withResolvers();
146
+ // Monotonic clock anchors (ms); each metric is only reported when both its ends are known.
147
+ let childSpawnedAt;
148
+ let firstProtocolMessageAt;
149
+ let promptAt;
150
+ let firstTextAt;
151
+ let terminalAt;
152
+ let teardownStartedAt;
153
+ let teardownSettledAt;
154
+ const buildMetrics = () => {
155
+ const metrics = {};
156
+ if (options.authProbeDurationMs !== undefined)
157
+ metrics.authProbeDurationMs = options.authProbeDurationMs;
158
+ if (childSpawnedAt !== undefined && firstProtocolMessageAt !== undefined) {
159
+ metrics.spawnToFirstProtocolMessageMs = firstProtocolMessageAt - childSpawnedAt;
160
+ }
161
+ if (promptAt !== undefined && firstTextAt !== undefined)
162
+ metrics.promptToFirstTextMs = firstTextAt - promptAt;
163
+ if (promptAt !== undefined && terminalAt !== undefined)
164
+ metrics.promptToTerminalMs = terminalAt - promptAt;
165
+ if (teardownStartedAt !== undefined && teardownSettledAt !== undefined) {
166
+ metrics.teardownDurationMs = teardownSettledAt - teardownStartedAt;
167
+ }
168
+ return Object.keys(metrics).length > 0 ? metrics : undefined;
169
+ };
170
+ const reportSettled = () => {
171
+ if (options.onSettled === undefined || reportedContext)
172
+ return;
173
+ reportedContext = true;
174
+ const metrics = buildMetrics();
175
+ const context = {
176
+ phase,
177
+ promptSubmissionState,
178
+ assistantTextObserved: sawAssistantText,
179
+ teardownState,
180
+ ...(terminalReason !== undefined ? { terminalReason } : {}),
181
+ ...(closeResult !== undefined ? { exitCode: closeResult.code, signal: closeResult.signal } : {}),
182
+ ...(metrics !== undefined ? { metrics } : {}),
183
+ ...(usageSnapshot !== undefined ? { usage: usageSnapshot } : {}),
184
+ };
185
+ try {
186
+ options.onSettled(context);
187
+ }
188
+ catch {
189
+ // A diagnostic sink must never change model-call settlement.
190
+ }
191
+ };
192
+ const observeCatalog = (observation) => {
193
+ if (options.onCatalogObserved === undefined)
194
+ return;
195
+ try {
196
+ options.onCatalogObserved(observation);
197
+ }
198
+ catch {
199
+ // A non-authoritative observation must never change model-call settlement.
200
+ }
201
+ };
202
+ const recordFailure = (error) => {
203
+ if (fatal !== undefined)
204
+ return;
205
+ fatal = error;
206
+ failureRaised.resolve(error);
207
+ };
208
+ const awaitRpc = async (rpc) => {
209
+ const result = await Promise.race([
210
+ rpc.then(value => ({ kind: 'value', value })),
211
+ failureRaised.promise.then(error => ({ kind: 'failure', error })),
212
+ ]);
213
+ if (result.kind === 'failure')
214
+ throw result.error;
215
+ return result.value;
216
+ };
217
+ const signalChild = (signal) => {
218
+ if (closeResult !== undefined)
219
+ return;
220
+ if (process.platform !== 'win32' && child.pid !== undefined && child.pid > 0) {
221
+ try {
222
+ process.kill(-child.pid, signal);
223
+ return;
224
+ }
225
+ catch {
226
+ // The detached process group may already be gone; try the direct child.
227
+ }
228
+ }
229
+ try {
230
+ child.kill(signal);
231
+ }
232
+ catch {
233
+ // Teardown is best-effort; settlement still waits for ChildProcess `close`.
234
+ }
235
+ };
236
+ const waitForChildClose = (timeoutMs) => {
237
+ if (closeResult !== undefined)
238
+ return Promise.resolve(true);
239
+ return new Promise(resolve => {
240
+ let settled = false;
241
+ const finish = (closed) => {
242
+ if (settled)
243
+ return;
244
+ settled = true;
245
+ clearTimeout(timer);
246
+ resolve(closed);
247
+ };
248
+ const timer = setTimeout(() => finish(false), timeoutMs);
249
+ timer.unref?.();
250
+ void childClosed.then(() => finish(true));
251
+ });
252
+ };
253
+ const shutdown = (cancelSession) => {
254
+ if (shutdownPromise !== undefined)
255
+ return shutdownPromise;
256
+ terminating = true;
257
+ teardownState = 'in-progress';
258
+ teardownStartedAt ??= performance.now();
259
+ shutdownPromise = (async () => {
260
+ if (cancelSession && sessionId !== undefined && client !== undefined && !client.signal.aborted) {
261
+ let cancelTimer;
262
+ try {
263
+ // Start the cooperative notification first, but never let a blocked
264
+ // stdin pipe prevent the authoritative process signals.
265
+ const sent = client.cancel({ sessionId }).catch(() => undefined);
266
+ await Promise.race([
267
+ sent,
268
+ new Promise(resolve => {
269
+ cancelTimer = setTimeout(resolve, Math.min(100, limits.killGraceMs));
270
+ }),
271
+ ]);
272
+ }
273
+ catch {
274
+ // A closing connection cannot receive cancellation; process signals remain authoritative.
275
+ }
276
+ finally {
277
+ if (cancelTimer !== undefined)
278
+ clearTimeout(cancelTimer);
279
+ }
280
+ }
281
+ if (closeResult === undefined) {
282
+ signalChild('SIGINT');
283
+ if (!await waitForChildClose(limits.killGraceMs)) {
284
+ signalChild('SIGKILL');
285
+ if (!await waitForChildClose(limits.killGraceMs)) {
286
+ // The request must still settle even if a broken process implementation never emits
287
+ // `close`. Keep the listeners/streams attached so a late close is drained and tracked,
288
+ // but do not report a second settlement.
289
+ teardownState = 'failed';
290
+ teardownSettledAt ??= performance.now();
291
+ recordFailure(new TraexAcpError('process'));
292
+ return;
293
+ }
294
+ }
295
+ }
296
+ teardownState = 'completed';
297
+ teardownSettledAt ??= performance.now();
298
+ })();
299
+ return shutdownPromise;
300
+ };
301
+ const stop = (error) => {
302
+ recordFailure(error);
303
+ void shutdown(true);
304
+ };
305
+ child.once('error', error => stop(processFailure(error)));
306
+ child.once('spawn', () => { childSpawnedAt ??= performance.now(); });
307
+ child.once('close', (code, signal) => {
308
+ closeResult = { code, signal };
309
+ resolveClosed?.();
310
+ if (!terminating) {
311
+ if (code !== 0 || signal !== null)
312
+ recordFailure(new TraexAcpError('process'));
313
+ else if (!turnFinished)
314
+ recordFailure(new TraexAcpError('protocol'));
315
+ }
316
+ });
317
+ child.stderr.on('data', (chunk) => stderr.append(chunk));
318
+ child.stderr.on('error', () => stop(new TraexAcpError('process')));
319
+ child.stdin.on('error', () => stop(new TraexAcpError('process')));
320
+ const guardedStdout = new BoundedNdjsonTransform(limits.maxMessageBytes, limits.maxProtocolBytes, limits.maxProtocolMessages, rpcGuard, () => { firstProtocolMessageAt ??= performance.now(); });
321
+ guardedStdout.once('error', (error) => {
322
+ stop(error instanceof TraexAcpError ? error : new TraexAcpError('protocol'));
323
+ });
324
+ child.stdout.on('error', () => stop(new TraexAcpError('process')));
325
+ child.stdout.pipe(guardedStdout);
326
+ const rawStream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(guardedStdout));
327
+ const stream = guardOutgoingStream(rawStream, rpcGuard);
328
+ const makeClient = (_agent) => ({
329
+ sessionUpdate(notification) {
330
+ const update = notification.update;
331
+ if (fatal !== undefined)
332
+ return Promise.resolve();
333
+ if (sessionId === undefined) {
334
+ stop(new TraexAcpError('protocol'));
335
+ return Promise.resolve();
336
+ }
337
+ if (notification.sessionId !== sessionId) {
338
+ stop(new TraexAcpError('protocol'));
339
+ return Promise.resolve();
340
+ }
341
+ if (update.sessionUpdate !== 'agent_message_chunk' || update.content.type !== 'text') {
342
+ return Promise.resolve();
343
+ }
344
+ if (!promptActive || turnFinished) {
345
+ stop(new TraexAcpError('protocol'));
346
+ return Promise.resolve();
347
+ }
348
+ const text = update.content.text;
349
+ if (text.length === 0)
350
+ return Promise.resolve();
351
+ const nextBytes = outputBytes + Buffer.byteLength(text, 'utf8');
352
+ if (nextBytes > limits.maxOutputBytes) {
353
+ stop(new TraexAcpError('output-limit'));
354
+ return Promise.resolve();
355
+ }
356
+ outputBytes = nextBytes;
357
+ sawAssistantText = true;
358
+ firstTextAt ??= performance.now();
359
+ if (phase === 'prompt')
360
+ phase = 'stream';
361
+ queue.push(text);
362
+ return Promise.resolve();
363
+ },
364
+ requestPermission() {
365
+ return Promise.resolve({ outcome: { outcome: 'cancelled' } });
366
+ },
367
+ });
368
+ client = new ClientSideConnection(makeClient, stream);
369
+ const onAbort = () => stop(new TraexAcpError('abort'));
370
+ options.signal?.addEventListener('abort', onAbort, { once: true });
371
+ if (options.signal?.aborted)
372
+ onAbort();
373
+ const timeout = setTimeout(() => stop(new TraexAcpError('timeout')), limits.timeoutMs);
374
+ timeout.unref?.();
375
+ const operation = (async () => {
376
+ try {
377
+ phase = 'initialize';
378
+ const initialized = await awaitRpc(client.initialize({
379
+ protocolVersion: PROTOCOL_VERSION,
380
+ clientCapabilities: {},
381
+ clientInfo: {
382
+ name: 'dsh-enhanced-traex-acp-provider',
383
+ title: 'DSH TraeX ACP Provider',
384
+ version,
385
+ },
386
+ }));
387
+ throwIfFatal(fatal);
388
+ if (initialized.protocolVersion !== PROTOCOL_VERSION || initialized.agentInfo?.name !== 'traex-acp') {
389
+ throw new TraexAcpError('protocol');
390
+ }
391
+ if (!initialized.authMethods?.some(isTraeSsoAuthMethod)) {
392
+ throw new TraexAcpError('auth');
393
+ }
394
+ phase = 'new-session';
395
+ const session = await awaitRpc(client.newSession({ cwd: invocation.cwd, mcpServers: [] }));
396
+ sessionId = session.sessionId;
397
+ if (sessionId.length === 0)
398
+ throw new TraexAcpError('protocol');
399
+ throwIfFatal(fatal);
400
+ phase = 'model-catalog';
401
+ const initialModelOption = validateModelCatalog(session.configOptions);
402
+ let activeModelOption = initialModelOption;
403
+ let activeConfigOptions = session.configOptions;
404
+ const initialReasoning = validateReasoningCatalog(activeConfigOptions);
405
+ observeCatalog(buildCatalogObservation(initialModelOption, initialReasoning === undefined
406
+ ? new Map()
407
+ : new Map([[initialModelOption.currentValue, catalogReasoning(initialReasoning)]]), false));
408
+ if (invocation.operation === 'catalog') {
409
+ const reasoningByModel = new Map();
410
+ const initialCurrentValue = initialModelOption.currentValue;
411
+ for (const entry of selectEntries(initialModelOption)) {
412
+ if (activeModelOption.currentValue !== entry.value) {
413
+ const updated = await awaitRpc(client.setSessionConfigOption({
414
+ sessionId,
415
+ configId: initialModelOption.id,
416
+ value: entry.value,
417
+ }));
418
+ activeModelOption = validateModelCatalog(updated.configOptions);
419
+ activeConfigOptions = updated.configOptions;
420
+ if (activeModelOption.id !== initialModelOption.id || activeModelOption.currentValue !== entry.value) {
421
+ throw new TraexAcpError('protocol');
422
+ }
423
+ throwIfFatal(fatal);
424
+ }
425
+ const reasoning = validateReasoningCatalog(activeConfigOptions);
426
+ if (reasoning !== undefined)
427
+ reasoningByModel.set(entry.value, catalogReasoning(reasoning));
428
+ }
429
+ observeCatalog(buildCatalogObservation(initialModelOption, reasoningByModel, true, initialCurrentValue));
430
+ // Catalog discovery is a successful no-prompt operation. Mark the session complete so a
431
+ // clean child close cannot be mistaken for premature EOF.
432
+ turnFinished = true;
433
+ if (initialized.agentCapabilities?.sessionCapabilities?.close !== undefined
434
+ && initialized.agentCapabilities.sessionCapabilities.close !== null) {
435
+ phase = 'close-session';
436
+ await awaitRpc(client.closeSession({ sessionId }));
437
+ throwIfFatal(fatal);
438
+ }
439
+ phase = 'child-close';
440
+ await shutdown(false);
441
+ throwIfFatal(fatal);
442
+ return;
443
+ }
444
+ if (invocation.model !== undefined) {
445
+ if (!selectValues(initialModelOption).includes(invocation.model))
446
+ throw new TraexAcpError('model');
447
+ phase = 'set-model';
448
+ const updated = await awaitRpc(client.setSessionConfigOption({
449
+ sessionId,
450
+ configId: initialModelOption.id,
451
+ value: invocation.model,
452
+ }));
453
+ activeModelOption = validateModelCatalog(updated.configOptions);
454
+ activeConfigOptions = updated.configOptions;
455
+ if (activeModelOption.id !== initialModelOption.id || activeModelOption.currentValue !== invocation.model) {
456
+ throw new TraexAcpError('protocol');
457
+ }
458
+ throwIfFatal(fatal);
459
+ }
460
+ if (invocation.reasoningEffort !== undefined) {
461
+ const reasoningOption = validateReasoningCatalog(activeConfigOptions);
462
+ if (reasoningOption === undefined || !selectValues(reasoningOption).includes(invocation.reasoningEffort)) {
463
+ throw new TraexAcpError('reasoning');
464
+ }
465
+ phase = 'set-reasoning';
466
+ const updated = await awaitRpc(client.setSessionConfigOption({
467
+ sessionId,
468
+ configId: reasoningOption.id,
469
+ value: invocation.reasoningEffort,
470
+ }));
471
+ activeModelOption = validateModelCatalog(updated.configOptions);
472
+ activeConfigOptions = updated.configOptions;
473
+ const selectedReasoning = validateReasoningCatalog(activeConfigOptions);
474
+ if (activeModelOption.currentValue !== (invocation.model ?? initialModelOption.currentValue)
475
+ || selectedReasoning?.id !== reasoningOption.id
476
+ || selectedReasoning.currentValue !== invocation.reasoningEffort) {
477
+ throw new TraexAcpError('protocol');
478
+ }
479
+ throwIfFatal(fatal);
480
+ }
481
+ const activeReasoning = validateReasoningCatalog(activeConfigOptions);
482
+ observeCatalog(buildCatalogObservation(activeModelOption, activeReasoning === undefined
483
+ ? new Map()
484
+ : new Map([[activeModelOption.currentValue, catalogReasoning(activeReasoning)]]), false));
485
+ // The prompt is about to enter the ACP stream; from here replay is never safe.
486
+ phase = 'prompt';
487
+ promptSubmissionState = 'submitted';
488
+ promptActive = true;
489
+ promptAt = performance.now();
490
+ const result = await awaitRpc(client.prompt({
491
+ sessionId,
492
+ prompt: [{ type: 'text', text: invocation.prompt }],
493
+ }));
494
+ promptActive = false;
495
+ throwIfFatal(fatal);
496
+ // The response itself is the terminal protocol event even when its reason represents a
497
+ // failed turn, or when later semantic checks (such as required text) fail.
498
+ phase = 'terminal';
499
+ terminalAt = performance.now();
500
+ terminalReason = result.stopReason;
501
+ usageSnapshot = snapshotUsage(result.usage);
502
+ if (result.stopReason === 'refusal')
503
+ throw new TraexAcpError('refusal');
504
+ if (result.stopReason === 'cancelled')
505
+ throw new TraexAcpError('abort');
506
+ if (!['end_turn', 'max_tokens', 'max_turn_requests'].includes(result.stopReason)) {
507
+ throw new TraexAcpError('protocol');
508
+ }
509
+ if (!sawAssistantText)
510
+ throw new TraexAcpError('protocol');
511
+ const stopReason = result.stopReason;
512
+ // Mark a validated completed turn before invoking an observer. A clean child close racing
513
+ // between terminal handling / closeSession and shutdown is no longer a premature EOF.
514
+ turnFinished = true;
515
+ try {
516
+ options.onStopReason?.(stopReason);
517
+ }
518
+ catch {
519
+ throw new TraexAcpError('protocol');
520
+ }
521
+ // Success still waits for bounded teardown; a close-session failure must not settle as success.
522
+ if (initialized.agentCapabilities?.sessionCapabilities?.close !== undefined
523
+ && initialized.agentCapabilities.sessionCapabilities.close !== null) {
524
+ phase = 'close-session';
525
+ await awaitRpc(client.closeSession({ sessionId }));
526
+ throwIfFatal(fatal);
527
+ }
528
+ phase = 'child-close';
529
+ await shutdown(false);
530
+ throwIfFatal(fatal);
531
+ }
532
+ catch (error) {
533
+ const normalized = fatal ?? normalizeFailure(error);
534
+ recordFailure(normalized);
535
+ // Teardown still runs to reap the child; `phase` records if close-session was where it failed.
536
+ await shutdown(true);
537
+ throw normalized;
538
+ }
539
+ finally {
540
+ clearTimeout(timeout);
541
+ options.signal?.removeEventListener('abort', onAbort);
542
+ const diagnostic = stderr.text();
543
+ if (diagnostic.length > 0) {
544
+ try {
545
+ options.onDiagnostic?.(diagnostic);
546
+ }
547
+ catch {
548
+ // Diagnostic consumers must not affect transport settlement.
549
+ }
550
+ }
551
+ reportSettled();
552
+ operationFinished = true;
553
+ }
554
+ })();
555
+ void operation.then(() => queue.close(), error => queue.close(normalizeFailure(error)));
556
+ try {
557
+ for await (const text of queue)
558
+ yield text;
559
+ }
560
+ finally {
561
+ if (!operationFinished && fatal === undefined)
562
+ stop(new TraexAcpError('abort'));
563
+ await operation.catch(() => undefined);
564
+ }
565
+ }
566
+ /**
567
+ * Perform a strict ACP initialize + session/new discovery without ever submitting a prompt.
568
+ * Every advertised model is selected inside the disposable session so model-specific reasoning
569
+ * efforts are captured exactly as TraeX exposes them.
570
+ */
571
+ export async function discoverTraexAcpModels(invocation, options = {}) {
572
+ let observation;
573
+ const callerObserver = options.onCatalogObserved;
574
+ for await (const text of runTraexAcpText({
575
+ ...invocation,
576
+ prompt: '',
577
+ operation: 'catalog',
578
+ }, {
579
+ ...options,
580
+ onCatalogObserved(value) {
581
+ observation = value;
582
+ callerObserver?.(value);
583
+ },
584
+ })) {
585
+ // A catalog-only operation must never produce assistant text.
586
+ if (text.length > 0)
587
+ throw new TraexAcpError('protocol');
588
+ }
589
+ if (observation === undefined || !observation.completeReasoning)
590
+ throw new TraexAcpError('protocol');
591
+ return observation;
592
+ }
593
+ /** Environment allowlist for the local Trae tool-account executable. */
594
+ export function buildTraexEnv(extraEnvNames = []) {
595
+ const result = {};
596
+ for (const name of new Set([...inheritedEnvNames, ...extraEnvNames])) {
597
+ if (!envName.test(name) || secretEnvName.test(name) || providerEndpointEnvName.test(name))
598
+ continue;
599
+ const value = process.env[name];
600
+ if (value === undefined)
601
+ continue;
602
+ const sanitized = sanitizeEnvValue(name, value);
603
+ if (sanitized !== undefined)
604
+ result[name] = sanitized;
605
+ }
606
+ return result;
607
+ }
608
+ function sanitizeEnvValue(name, value) {
609
+ if (!proxyEnvName.test(name))
610
+ return value;
611
+ if (!/^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(value))
612
+ return value.includes('@') ? undefined : value;
613
+ try {
614
+ const proxy = new URL(value);
615
+ if (proxy.username.length === 0 && proxy.password.length === 0)
616
+ return value;
617
+ proxy.username = '';
618
+ proxy.password = '';
619
+ return proxy.toString();
620
+ }
621
+ catch {
622
+ return value.includes('@') ? undefined : value;
623
+ }
624
+ }
625
+ function validate(invocation, options) {
626
+ if (invocation.command.trim().length === 0 || !isAbsolute(invocation.cwd)) {
627
+ throw new TraexAcpError('protocol');
628
+ }
629
+ if (invocation.args.length !== SAFE_ARGS.length
630
+ || invocation.args.some((value, index) => value !== SAFE_ARGS[index])) {
631
+ throw new TraexAcpError('protocol');
632
+ }
633
+ if (invocation.operation === 'catalog'
634
+ && (invocation.prompt.length !== 0 || invocation.model !== undefined || invocation.reasoningEffort !== undefined)) {
635
+ throw new TraexAcpError('protocol');
636
+ }
637
+ if (invocation.model !== undefined && invocation.model.length === 0) {
638
+ throw new TraexAcpError('protocol');
639
+ }
640
+ if (invocation.reasoningEffort !== undefined && invocation.reasoningEffort.length === 0) {
641
+ throw new TraexAcpError('protocol');
642
+ }
643
+ return {
644
+ timeoutMs: positiveInteger(options.timeoutMs, defaults.timeoutMs),
645
+ killGraceMs: positiveInteger(options.killGraceMs, defaults.killGraceMs),
646
+ maxMessageBytes: positiveInteger(options.maxMessageBytes, defaults.maxMessageBytes),
647
+ maxProtocolBytes: positiveInteger(options.maxProtocolBytes, defaults.maxProtocolBytes),
648
+ maxProtocolMessages: positiveInteger(options.maxProtocolMessages, defaults.maxProtocolMessages),
649
+ maxOutputBytes: positiveInteger(options.maxOutputBytes, defaults.maxOutputBytes),
650
+ maxStderrBytes: positiveInteger(options.maxStderrBytes, defaults.maxStderrBytes),
651
+ };
652
+ }
653
+ function positiveInteger(value, fallback) {
654
+ if (value === undefined)
655
+ return fallback;
656
+ if (!Number.isSafeInteger(value) || value <= 0)
657
+ throw new TraexAcpError('protocol');
658
+ return value;
659
+ }
660
+ function validateModelCatalog(options) {
661
+ const matches = (options ?? []).filter((option) => (option.type === 'select' && (option.category === 'model' || option.id === 'model')));
662
+ if (matches.length !== 1)
663
+ throw new TraexAcpError('protocol');
664
+ const option = matches[0];
665
+ const entries = selectEntries(option);
666
+ const values = entries.map(entry => entry.value);
667
+ if (option.currentValue.length === 0 || entries.length === 0)
668
+ throw new TraexAcpError('entitlement');
669
+ if (!values.includes(option.currentValue) || new Set(values).size !== values.length
670
+ || entries.some(entry => entry.value.length === 0 || entry.name.length === 0)) {
671
+ throw new TraexAcpError('protocol');
672
+ }
673
+ return option;
674
+ }
675
+ function validateReasoningCatalog(options) {
676
+ const matches = (options ?? []).filter((option) => (option.type === 'select' && (option.category === 'thought_level' || option.id === 'reasoning_effort')));
677
+ if (matches.length === 0)
678
+ return undefined;
679
+ if (matches.length !== 1)
680
+ throw new TraexAcpError('protocol');
681
+ const option = matches[0];
682
+ const entries = selectEntries(option);
683
+ const values = entries.map(entry => entry.value);
684
+ if (option.currentValue.length === 0 || entries.length === 0
685
+ || !values.includes(option.currentValue) || new Set(values).size !== values.length
686
+ || entries.some(entry => entry.value.length === 0 || entry.name.length === 0)) {
687
+ throw new TraexAcpError('protocol');
688
+ }
689
+ return option;
690
+ }
691
+ function catalogReasoning(option) {
692
+ return {
693
+ efforts: selectEntries(option).map(entry => ({
694
+ id: entry.value,
695
+ name: entry.name,
696
+ ...(typeof entry.description === 'string' && entry.description.length > 0
697
+ ? { description: entry.description }
698
+ : {}),
699
+ })),
700
+ defaultEffort: option.currentValue,
701
+ };
702
+ }
703
+ function buildCatalogObservation(modelOption, reasoningByModel, completeReasoning, currentValue = modelOption.currentValue) {
704
+ return {
705
+ currentValue,
706
+ modelValues: selectValues(modelOption),
707
+ models: selectEntries(modelOption).map(entry => ({
708
+ id: entry.value,
709
+ name: entry.name,
710
+ ...(typeof entry.description === 'string' && entry.description.length > 0
711
+ ? { description: entry.description }
712
+ : {}),
713
+ ...(reasoningByModel.has(entry.value) ? { reasoning: reasoningByModel.get(entry.value) } : {}),
714
+ })),
715
+ completeReasoning,
716
+ observedAt: Date.now(),
717
+ };
718
+ }
719
+ function selectValues(option) {
720
+ return selectEntries(option).map(entry => entry.value);
721
+ }
722
+ /**
723
+ * Extract only the explicit numeric fields of ACP's experimental usage into an internal snapshot.
724
+ * `_meta` and any unknown keys are dropped; optional counts stay undefined when absent, and a real
725
+ * zero is preserved. This is a diagnostic record, never a DSH `TokenUsage` mapping.
726
+ */
727
+ function snapshotUsage(usage) {
728
+ if (usage === undefined || usage === null)
729
+ return undefined;
730
+ if (!validTokenCount(usage.inputTokens)
731
+ || !validTokenCount(usage.outputTokens)
732
+ || !validTokenCount(usage.totalTokens)
733
+ || !validOptionalTokenCount(usage.cachedReadTokens)
734
+ || !validOptionalTokenCount(usage.cachedWriteTokens)
735
+ || !validOptionalTokenCount(usage.thoughtTokens)) {
736
+ throw new TraexAcpError('protocol');
737
+ }
738
+ return {
739
+ inputTokens: usage.inputTokens,
740
+ outputTokens: usage.outputTokens,
741
+ totalTokens: usage.totalTokens,
742
+ ...(typeof usage.cachedReadTokens === 'number' ? { cachedReadTokens: usage.cachedReadTokens } : {}),
743
+ ...(typeof usage.cachedWriteTokens === 'number' ? { cachedWriteTokens: usage.cachedWriteTokens } : {}),
744
+ ...(typeof usage.thoughtTokens === 'number' ? { thoughtTokens: usage.thoughtTokens } : {}),
745
+ };
746
+ }
747
+ function validTokenCount(value) {
748
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
749
+ }
750
+ function validOptionalTokenCount(value) {
751
+ return value === undefined || value === null || validTokenCount(value);
752
+ }
753
+ function selectEntries(option) {
754
+ return option.options.flatMap(entry => 'group' in entry ? entry.options : [entry]);
755
+ }
756
+ function throwIfFatal(error) {
757
+ if (error !== undefined)
758
+ throw error;
759
+ }
760
+ function normalizeFailure(error) {
761
+ if (error instanceof TraexAcpError)
762
+ return error;
763
+ if (error instanceof RequestError && error.code === -32000)
764
+ return new TraexAcpError('auth');
765
+ return new TraexAcpError('protocol');
766
+ }
767
+ function processFailure(error) {
768
+ const systemCode = error !== null && typeof error === 'object' && 'code' in error
769
+ && typeof error.code === 'string' ? error.code : undefined;
770
+ return new TraexAcpError('process', undefined, systemCode);
771
+ }
772
+ function failureMessage(failure) {
773
+ const messages = {
774
+ abort: 'TraeX ACP execution was aborted',
775
+ timeout: 'TraeX ACP execution timed out',
776
+ protocol: 'TraeX ACP protocol validation failed',
777
+ auth: 'TraeX ACP requires an existing non-interactive login',
778
+ entitlement: 'TraeX ACP did not expose an entitled model',
779
+ model: 'TraeX ACP did not expose the requested model',
780
+ reasoning: 'TraeX ACP did not expose the requested reasoning effort',
781
+ refusal: 'TraeX ACP refused the request',
782
+ process: 'TraeX ACP process failed',
783
+ 'output-limit': 'TraeX ACP output exceeded its configured limit',
784
+ };
785
+ return messages[failure];
786
+ }
787
+ const clientRequestMethods = new Set([
788
+ 'initialize',
789
+ 'session/new',
790
+ 'session/set_config_option',
791
+ 'session/prompt',
792
+ 'session/close',
793
+ ]);
794
+ class AcpJsonRpcGuard {
795
+ pendingClientRequests = new Map();
796
+ pendingAgentRequests = new Map();
797
+ acceptIncoming(envelope) {
798
+ if (envelope.jsonrpc !== '2.0')
799
+ throw new TraexAcpError('protocol');
800
+ const hasMethod = own(envelope, 'method');
801
+ const hasId = own(envelope, 'id');
802
+ const hasResult = own(envelope, 'result');
803
+ const hasError = own(envelope, 'error');
804
+ if (hasMethod) {
805
+ if (typeof envelope.method !== 'string' || envelope.method.length === 0 || hasResult || hasError) {
806
+ throw new TraexAcpError('protocol');
807
+ }
808
+ if (hasId)
809
+ this.acceptAgentRequest(envelope);
810
+ else
811
+ this.acceptAgentNotification(envelope);
812
+ return;
813
+ }
814
+ if (!hasId || hasResult === hasError || own(envelope, 'params'))
815
+ throw new TraexAcpError('protocol');
816
+ exactKeys(envelope, hasResult ? ['jsonrpc', 'id', 'result'] : ['jsonrpc', 'id', 'error']);
817
+ const key = rpcIdKey(envelope.id);
818
+ const method = this.pendingClientRequests.get(key);
819
+ if (method === undefined)
820
+ throw new TraexAcpError('protocol');
821
+ if (hasResult) {
822
+ if (!validClientResponse(method, envelope.result))
823
+ throw new TraexAcpError('protocol');
824
+ }
825
+ else if (!validJsonRpcError(envelope.error)) {
826
+ throw new TraexAcpError('protocol');
827
+ }
828
+ this.pendingClientRequests.delete(key);
829
+ }
830
+ observeOutgoing(message) {
831
+ const envelope = jsonObject(message);
832
+ if (envelope === undefined || envelope.jsonrpc !== '2.0')
833
+ throw new TraexAcpError('protocol');
834
+ const hasMethod = own(envelope, 'method');
835
+ const hasId = own(envelope, 'id');
836
+ const hasResult = own(envelope, 'result');
837
+ const hasError = own(envelope, 'error');
838
+ if (hasMethod) {
839
+ if (typeof envelope.method !== 'string' || hasResult || hasError)
840
+ throw new TraexAcpError('protocol');
841
+ if (hasId) {
842
+ exactKeys(envelope, own(envelope, 'params')
843
+ ? ['jsonrpc', 'id', 'method', 'params']
844
+ : ['jsonrpc', 'id', 'method']);
845
+ if (!clientRequestMethods.has(envelope.method))
846
+ throw new TraexAcpError('protocol');
847
+ const key = rpcIdKey(envelope.id);
848
+ if (this.pendingClientRequests.has(key))
849
+ throw new TraexAcpError('protocol');
850
+ this.pendingClientRequests.set(key, envelope.method);
851
+ }
852
+ else {
853
+ exactKeys(envelope, own(envelope, 'params')
854
+ ? ['jsonrpc', 'method', 'params']
855
+ : ['jsonrpc', 'method']);
856
+ if (envelope.method !== 'session/cancel')
857
+ throw new TraexAcpError('protocol');
858
+ }
859
+ return;
860
+ }
861
+ if (!hasId || hasResult === hasError || own(envelope, 'params'))
862
+ throw new TraexAcpError('protocol');
863
+ exactKeys(envelope, hasResult ? ['jsonrpc', 'id', 'result'] : ['jsonrpc', 'id', 'error']);
864
+ const key = rpcIdKey(envelope.id);
865
+ const method = this.pendingAgentRequests.get(key);
866
+ if (method !== 'session/request_permission' || hasError
867
+ || !zRequestPermissionResponse.safeParse(envelope.result).success
868
+ || !cancelledPermissionResponse(envelope.result)) {
869
+ throw new TraexAcpError('protocol');
870
+ }
871
+ this.pendingAgentRequests.delete(key);
872
+ }
873
+ acceptAgentNotification(envelope) {
874
+ exactKeys(envelope, own(envelope, 'params')
875
+ ? ['jsonrpc', 'method', 'params']
876
+ : ['jsonrpc', 'method']);
877
+ if (envelope.method !== 'session/update'
878
+ || !zSessionNotification.safeParse(envelope.params).success) {
879
+ throw new TraexAcpError('protocol');
880
+ }
881
+ }
882
+ acceptAgentRequest(envelope) {
883
+ exactKeys(envelope, own(envelope, 'params')
884
+ ? ['jsonrpc', 'id', 'method', 'params']
885
+ : ['jsonrpc', 'id', 'method']);
886
+ if (envelope.method !== 'session/request_permission'
887
+ || !zRequestPermissionRequest.safeParse(envelope.params).success) {
888
+ throw new TraexAcpError('protocol');
889
+ }
890
+ const key = rpcIdKey(envelope.id);
891
+ if (this.pendingAgentRequests.has(key))
892
+ throw new TraexAcpError('protocol');
893
+ this.pendingAgentRequests.set(key, envelope.method);
894
+ }
895
+ }
896
+ function guardOutgoingStream(stream, guard) {
897
+ return {
898
+ readable: stream.readable,
899
+ writable: new WritableStream({
900
+ async write(message) {
901
+ guard.observeOutgoing(message);
902
+ const writer = stream.writable.getWriter();
903
+ try {
904
+ await writer.write(message);
905
+ }
906
+ finally {
907
+ writer.releaseLock();
908
+ }
909
+ },
910
+ async close() {
911
+ const writer = stream.writable.getWriter();
912
+ try {
913
+ await writer.close();
914
+ }
915
+ finally {
916
+ writer.releaseLock();
917
+ }
918
+ },
919
+ async abort(reason) {
920
+ const writer = stream.writable.getWriter();
921
+ try {
922
+ await writer.abort(reason);
923
+ }
924
+ finally {
925
+ writer.releaseLock();
926
+ }
927
+ },
928
+ }),
929
+ };
930
+ }
931
+ function validClientResponse(method, result) {
932
+ if (method === 'initialize')
933
+ return validInitializeResponse(result);
934
+ if (method === 'session/new') {
935
+ return zNewSessionResponse.safeParse(result).success && validRawConfigOptions(result);
936
+ }
937
+ if (method === 'session/set_config_option') {
938
+ return zSetSessionConfigOptionResponse.safeParse(result).success && validRawConfigOptions(result);
939
+ }
940
+ if (method === 'session/prompt')
941
+ return validPromptResponse(result);
942
+ if (method === 'session/close')
943
+ return zCloseSessionResponse.safeParse(result).success;
944
+ return false;
945
+ }
946
+ function validPromptResponse(value) {
947
+ if (!zPromptResponse.safeParse(value).success)
948
+ return false;
949
+ const response = jsonObject(value);
950
+ if (response === undefined || !own(response, 'usage') || response.usage === null || response.usage === undefined) {
951
+ return true;
952
+ }
953
+ const usage = jsonObject(response.usage);
954
+ return usage !== undefined
955
+ && validTokenCount(usage.inputTokens)
956
+ && validTokenCount(usage.outputTokens)
957
+ && validTokenCount(usage.totalTokens)
958
+ && validOptionalTokenCount(usage.cachedReadTokens)
959
+ && validOptionalTokenCount(usage.cachedWriteTokens)
960
+ && validOptionalTokenCount(usage.thoughtTokens);
961
+ }
962
+ /** The SDK's generated initialize schema deliberately skips malformed optional fields. */
963
+ function validInitializeResponse(value) {
964
+ if (!zInitializeResponse.safeParse(value).success)
965
+ return false;
966
+ const response = jsonObject(value);
967
+ if (response === undefined)
968
+ return false;
969
+ if (own(response, 'agentInfo') && response.agentInfo !== null) {
970
+ const info = jsonObject(response.agentInfo);
971
+ if (info === undefined
972
+ || typeof info.name !== 'string' || info.name.length === 0
973
+ || typeof info.version !== 'string' || info.version.length === 0
974
+ || (own(info, 'title') && info.title !== null && typeof info.title !== 'string')) {
975
+ return false;
976
+ }
977
+ }
978
+ if (own(response, 'authMethods')) {
979
+ if (!Array.isArray(response.authMethods))
980
+ return false;
981
+ const traeMethods = response.authMethods.filter(method => jsonObject(method)?.id === 'trae-sso');
982
+ if (traeMethods.length > 1 || (traeMethods.length === 1 && !isTraeSsoAuthMethod(traeMethods[0]))) {
983
+ return false;
984
+ }
985
+ }
986
+ return true;
987
+ }
988
+ function isTraeSsoAuthMethod(value) {
989
+ const method = jsonObject(value);
990
+ return method !== undefined
991
+ && !own(method, 'type')
992
+ && method.id === 'trae-sso'
993
+ && typeof method.name === 'string'
994
+ && method.name.length > 0
995
+ && zAuthMethodAgent.safeParse(value).success;
996
+ }
997
+ function validRawConfigOptions(value) {
998
+ const response = jsonObject(value);
999
+ if (response === undefined || !own(response, 'configOptions'))
1000
+ return true;
1001
+ if (response.configOptions === null)
1002
+ return true;
1003
+ if (!Array.isArray(response.configOptions))
1004
+ return false;
1005
+ return response.configOptions.every(option => {
1006
+ const candidate = jsonObject(option);
1007
+ if (candidate?.id !== 'model' && candidate?.category !== 'model')
1008
+ return true;
1009
+ return zSessionConfigOption.safeParse(option).success;
1010
+ });
1011
+ }
1012
+ function cancelledPermissionResponse(value) {
1013
+ const response = jsonObject(value);
1014
+ const outcome = jsonObject(response?.outcome);
1015
+ return outcome?.outcome === 'cancelled';
1016
+ }
1017
+ function validJsonRpcError(value) {
1018
+ const error = jsonObject(value);
1019
+ if (error === undefined)
1020
+ return false;
1021
+ const allowed = own(error, 'data') ? ['code', 'message', 'data'] : ['code', 'message'];
1022
+ try {
1023
+ exactKeys(error, allowed);
1024
+ }
1025
+ catch {
1026
+ return false;
1027
+ }
1028
+ return Number.isSafeInteger(error.code) && typeof error.message === 'string';
1029
+ }
1030
+ function parseJsonObject(line) {
1031
+ if (!isUtf8(line))
1032
+ throw new TraexAcpError('protocol');
1033
+ const text = line.toString('utf8').trim();
1034
+ if (text.length === 0)
1035
+ throw new TraexAcpError('protocol');
1036
+ try {
1037
+ const value = JSON.parse(text);
1038
+ const result = jsonObject(value);
1039
+ if (result === undefined)
1040
+ throw new TraexAcpError('protocol');
1041
+ return result;
1042
+ }
1043
+ catch (error) {
1044
+ if (error instanceof TraexAcpError)
1045
+ throw error;
1046
+ throw new TraexAcpError('protocol');
1047
+ }
1048
+ }
1049
+ function jsonObject(value) {
1050
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
1051
+ ? value
1052
+ : undefined;
1053
+ }
1054
+ function own(value, key) {
1055
+ return Object.prototype.hasOwnProperty.call(value, key);
1056
+ }
1057
+ function exactKeys(value, allowed) {
1058
+ if (Object.keys(value).length !== allowed.length
1059
+ || Object.keys(value).some(key => !allowed.includes(key))) {
1060
+ throw new TraexAcpError('protocol');
1061
+ }
1062
+ }
1063
+ function rpcIdKey(value) {
1064
+ if (typeof value === 'string' && value.length > 0)
1065
+ return `s:${value}`;
1066
+ if (typeof value === 'number' && Number.isSafeInteger(value))
1067
+ return `n:${String(value)}`;
1068
+ throw new TraexAcpError('protocol');
1069
+ }
1070
+ class BoundedNdjsonTransform extends Transform {
1071
+ maxMessageBytes;
1072
+ maxProtocolBytes;
1073
+ maxProtocolMessages;
1074
+ guard;
1075
+ onAcceptedMessage;
1076
+ pending = Buffer.alloc(0);
1077
+ protocolBytes = 0;
1078
+ protocolMessages = 0;
1079
+ constructor(maxMessageBytes, maxProtocolBytes, maxProtocolMessages, guard, onAcceptedMessage) {
1080
+ super({ readableHighWaterMark: 64 * 1024, writableHighWaterMark: 64 * 1024 });
1081
+ this.maxMessageBytes = maxMessageBytes;
1082
+ this.maxProtocolBytes = maxProtocolBytes;
1083
+ this.maxProtocolMessages = maxProtocolMessages;
1084
+ this.guard = guard;
1085
+ this.onAcceptedMessage = onAcceptedMessage;
1086
+ }
1087
+ _transform(chunk, _encoding, callback) {
1088
+ try {
1089
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1090
+ if (bytes.byteLength > this.maxProtocolBytes - this.protocolBytes) {
1091
+ callback(new TraexAcpError('output-limit'));
1092
+ return;
1093
+ }
1094
+ this.protocolBytes += bytes.byteLength;
1095
+ let start = 0;
1096
+ for (let index = 0; index < bytes.length; index++) {
1097
+ if (bytes[index] !== 0x0a)
1098
+ continue;
1099
+ const part = bytes.subarray(start, index);
1100
+ this.consumeLine(part);
1101
+ start = index + 1;
1102
+ }
1103
+ const tail = bytes.subarray(start);
1104
+ if (this.pending.byteLength + tail.byteLength > this.maxMessageBytes) {
1105
+ callback(new TraexAcpError('output-limit'));
1106
+ return;
1107
+ }
1108
+ if (tail.byteLength > 0)
1109
+ this.pending = Buffer.concat([this.pending, tail]);
1110
+ callback();
1111
+ }
1112
+ catch (error) {
1113
+ callback(error instanceof Error ? error : new TraexAcpError('protocol'));
1114
+ }
1115
+ }
1116
+ _flush(callback) {
1117
+ try {
1118
+ if (this.pending.byteLength > 0) {
1119
+ this.acceptMessage(this.pending);
1120
+ this.push(this.pending);
1121
+ }
1122
+ this.pending = Buffer.alloc(0);
1123
+ callback();
1124
+ }
1125
+ catch (error) {
1126
+ callback(error instanceof Error ? error : new TraexAcpError('protocol'));
1127
+ }
1128
+ }
1129
+ consumeLine(part) {
1130
+ if (this.pending.byteLength + part.byteLength > this.maxMessageBytes) {
1131
+ throw new TraexAcpError('output-limit');
1132
+ }
1133
+ const line = this.pending.byteLength === 0 ? part : Buffer.concat([this.pending, part]);
1134
+ this.pending = Buffer.alloc(0);
1135
+ this.acceptMessage(line);
1136
+ this.push(Buffer.concat([line, Buffer.from('\n')]));
1137
+ }
1138
+ acceptMessage(line) {
1139
+ if (this.protocolMessages >= this.maxProtocolMessages)
1140
+ throw new TraexAcpError('output-limit');
1141
+ this.protocolMessages++;
1142
+ this.guard.acceptIncoming(parseJsonObject(line));
1143
+ this.onAcceptedMessage?.();
1144
+ }
1145
+ }
1146
+ class BoundedBytes {
1147
+ limit;
1148
+ chunks = [];
1149
+ bytes = 0;
1150
+ constructor(limit) {
1151
+ this.limit = limit;
1152
+ }
1153
+ append(value) {
1154
+ const next = Buffer.from(value);
1155
+ this.chunks.push(next);
1156
+ this.bytes += next.byteLength;
1157
+ let excess = this.bytes - this.limit;
1158
+ while (excess > 0 && this.chunks.length > 0) {
1159
+ const first = this.chunks[0];
1160
+ if (first.byteLength <= excess) {
1161
+ this.chunks.shift();
1162
+ this.bytes -= first.byteLength;
1163
+ excess -= first.byteLength;
1164
+ }
1165
+ else {
1166
+ this.chunks[0] = first.subarray(excess);
1167
+ this.bytes -= excess;
1168
+ excess = 0;
1169
+ }
1170
+ }
1171
+ }
1172
+ text() {
1173
+ let value = Buffer.concat(this.chunks, this.bytes).toString('utf8');
1174
+ while (Buffer.byteLength(value, 'utf8') > this.limit)
1175
+ value = value.slice(0, -1);
1176
+ return value;
1177
+ }
1178
+ }
1179
+ class TextQueue {
1180
+ values = [];
1181
+ waiter;
1182
+ error;
1183
+ done = false;
1184
+ push(value) {
1185
+ if (this.done)
1186
+ return;
1187
+ this.values.push(value);
1188
+ const waiter = this.waiter;
1189
+ this.waiter = undefined;
1190
+ waiter?.();
1191
+ }
1192
+ close(error) {
1193
+ if (this.done)
1194
+ return;
1195
+ this.done = true;
1196
+ this.error = error;
1197
+ const waiter = this.waiter;
1198
+ this.waiter = undefined;
1199
+ waiter?.();
1200
+ }
1201
+ async *[Symbol.asyncIterator]() {
1202
+ while (true) {
1203
+ if (this.values.length > 0)
1204
+ yield this.values.shift();
1205
+ else if (this.done) {
1206
+ if (this.error !== undefined)
1207
+ throw this.error;
1208
+ return;
1209
+ }
1210
+ else {
1211
+ await new Promise(resolve => { this.waiter = resolve; });
1212
+ }
1213
+ }
1214
+ }
1215
+ }
1216
+ //# sourceMappingURL=acp-client.js.map