History

fix(server): flush bounded validation diagnostics before failure

Julius Marminge committed 2 months ago

Commit 2126d7a1 parent d54b58b6 files · +212 −29Browse files

Unified · Split

6 files
  1. apps/server/src/provider/AntigravityInstallation.test.ts +28 −4
  2. apps/server/src/provider/acp/AcpSessionRuntime.ts +26 −7
  3. apps/server/src/provider/acp/AntigravityAcpSupport.test.ts +69 −0
  4. apps/server/src/provider/acp/AntigravityAcpSupport.ts +7 −4
  5. apps/server/src/provider/antigravityAuthSupport.test.ts +34 −0
  6. apps/server/src/provider/antigravityAuthSupport.ts +48 −14
apps/server/src/provider/AntigravityInstallation.test.ts+28−4
@@ -438,6 +438,24 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 437 unmodified lines
438 stderr: [`discarded prefix ${"x".repeat(8_000)}\n`, "last startup diagnostic\n"],438 stderr: [`discarded prefix ${"x".repeat(8_000)}\n`, "last startup diagnostic\n"],
439 expected: "last startup diagnostic",439 expected: "last startup diagnostic",
440 },440 },
441 {
442 name: "unterminated diagnostic",
443 stderr: ["Failed to create an AF_INET6 socket."],
444 expected: "Failed to create an AF_INET6 socket.",
445 },
446 {
447 name: "oversized diagnostic",
448 stderr: [`discarded prefix ${"x".repeat(65_536)} last startup diagnostic\n`],
449 expected: "last startup diagnostic",
450 },
451 {
452 name: "fragmented oversized authorization message",
453 stderr: [
454 `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${"x".repeat(65_536)}`,
455 "secret-authorization-tail\nlast startup diagnostic",
456 ],
457 expected: "last startup diagnostic",
458 },
441 {459 {
442 name: "no diagnostic",460 name: "no diagnostic",
443 stderr: [],461 stderr: [],
@@ -453,6 +471,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 9 unmodified lines
453 }471 }
454 const helper = command.args[0] === "-e";472 const helper = command.args[0] === "-e";
455 const exited = yield* Deferred.make<ChildProcessSpawner.ExitCode>();473 const exited = yield* Deferred.make<ChildProcessSpawner.ExitCode>();
474 const exitObserved = yield* Deferred.make<void>();
456 const terminate = Deferred.succeed(exited, ChildProcessSpawner.ExitCode(1)).pipe(475 const terminate = Deferred.succeed(exited, ChildProcessSpawner.ExitCode(1)).pipe(
457 Effect.asVoid,476 Effect.asVoid,
458 );477 );
@@ -461,22 +480,26 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 2 unmodified lines
461 pid: ChildProcessSpawner.ProcessId(helper ? 1 : 2),480 pid: ChildProcessSpawner.ProcessId(helper ? 1 : 2),
462 exitCode: helper481 exitCode: helper
463 ? Effect.succeed(ChildProcessSpawner.ExitCode(0))482 ? Effect.succeed(ChildProcessSpawner.ExitCode(0))
464 : Deferred.await(exited),483 : Deferred.await(exited).pipe(
484 Effect.tap(() => Deferred.succeed(exitObserved, undefined)),
485 ),
465 isRunning: Deferred.isDone(exited).pipe(Effect.map((done) => !done)),486 isRunning: Deferred.isDone(exited).pipe(Effect.map((done) => !done)),
466 kill: () => terminate,487 kill: () => terminate,
467 unref: Effect.succeed(Effect.void),488 unref: Effect.succeed(Effect.void),
468 stdin: Sink.drain,489 stdin: Sink.drain,
469 stdout: helper490 stdout: helper
470 ? Stream.empty491 ? Stream.empty
471 : Stream.fromEffect(Deferred.await(exited)).pipe(Stream.flatMap(() => Stream.empty)),492 : Stream.fromEffect(terminate).pipe(Stream.flatMap(() => Stream.empty)),
472 stderr: helper493 stderr: helper
473 ? Stream.make(494 ? Stream.make(
474 encoder.encode(495 encoder.encode(
475 `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeJsonString(command.args.at(-1))}\n`,496 `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeJsonString(command.args.at(-1))}\n`,
476 ),497 ),
477 )498 )
478 : Stream.fromIterable(testCase.stderr.map((line) => encoder.encode(line))).pipe(499 : Stream.fromEffect(Deferred.await(exitObserved)).pipe(
479 Stream.ensuring(terminate),500 Stream.flatMap(() =>
501 Stream.fromIterable(testCase.stderr.map((line) => encoder.encode(line))),
502 ),
480 ),503 ),
481 all: Stream.empty,504 all: Stream.empty,
482 getInputFd: () => Sink.drain,505 getInputFd: () => Sink.drain,
@@ -494,6 +517,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 11 unmodified lines
494 expect(state.message).toContain(testCase.expected);517 expect(state.message).toContain(testCase.expected);
495 expect(state.message?.length).toBeLessThan(4_200);518 expect(state.message?.length).toBeLessThan(4_200);
496 expect(state.message).not.toContain("discarded prefix");519 expect(state.message).not.toContain("discarded prefix");
520 expect(state.message).not.toContain("secret-authorization-tail");
497 yield* Deferred.await(stagingReleased);521 yield* Deferred.await(stagingReleased);
498 yield* expectPreviousRelease(installation);522 yield* expectPreviousRelease(installation);
499 }),523 }),
apps/server/src/provider/acp/AcpSessionRuntime.ts+26−7
@@ -104,6 +104,8 @@ export interface AcpSessionRuntimeOptions { · 103 unmodified lines
104 ) => EffectAcpSchema.SessionNotification;104 ) => EffectAcpSchema.SessionNotification;
105 /** Receives bounded stderr chunks. Redact secrets before logging. A failure closes the runtime. */105 /** Receives bounded stderr chunks. Redact secrets before logging. A failure closes the runtime. */
106 readonly onStderr?: (text: string) => Effect.Effect<void, EffectAcpErrors.AcpError>;106 readonly onStderr?: (text: string) => Effect.Effect<void, EffectAcpErrors.AcpError>;
107 /** Flushes diagnostics when stderr ends. Failed initialization drains an exited child's stderr first. */
108 readonly onStderrEnd?: Effect.Effect<void>;
107 readonly requestLogger?: (event: AcpSessionRequestLogEvent) => Effect.Effect<void, never>;109 readonly requestLogger?: (event: AcpSessionRequestLogEvent) => Effect.Effect<void, never>;
108 readonly protocolLogging?: {110 readonly protocolLogging?: {
109 readonly logIncoming?: boolean;111 readonly logIncoming?: boolean;
@@ -449,13 +451,16 @@ export const make = ( · 339 unmodified lines
449 ),451 ),
450 );452 );
451 453
452 yield* child.stderr.pipe(454 const stderrFiber = yield* child.stderr.pipe(
453 Stream.decodeText(),455 Stream.decodeText(),
454 Stream.runForEach((chunk) =>456 Stream.runForEach(
455 (options.onStderr457 Effect.fn("AcpSessionRuntime.handleStderr")(
456 ? options.onStderr(chunk.slice(-maxStderrChunkLength))458 function* (chunk: string) {
457 : Effect.void459 if (!options.onStderr) return;
458 ).pipe(460 for (let offset = 0; offset < chunk.length; offset += maxStderrChunkLength) {
461 yield* options.onStderr(chunk.slice(offset, offset + maxStderrChunkLength));
462 }
463 },
459 Effect.catch((error) =>464 Effect.catch((error) =>
460 Effect.gen(function* () {465 Effect.gen(function* () {
461 yield* Deferred.fail(stderrFailure, error);466 yield* Deferred.fail(stderrFailure, error);
@@ -465,6 +470,7 @@ export const make = ( · 3 unmodified lines
465 ),470 ),
466 ),471 ),
467 ),472 ),
473 Effect.ensuring(options.onStderrEnd ?? Effect.void),
468 Effect.ignore,474 Effect.ignore,
469 Effect.forkIn(runtimeScope),475 Effect.forkIn(runtimeScope),
470 );476 );
@@ -971,7 +977,20 @@ export const make = ( · 500 unmodified lines
971 handleUnknownExtNotification: acp.handleUnknownExtNotification,977 handleUnknownExtNotification: acp.handleUnknownExtNotification,
972 handleExtRequest: acp.handleExtRequest,978 handleExtRequest: acp.handleExtRequest,
973 handleExtNotification: acp.handleExtNotification,979 handleExtNotification: acp.handleExtNotification,
974 initialize: () => ensureConnected.pipe(Effect.andThen(sendInitialize)),980 initialize: () =>
981 ensureConnected.pipe(
982 Effect.andThen(sendInitialize),
983 Effect.onError(() =>
984 Effect.gen(function* () {
985 if (!options.onStderrEnd) return;
986 const running = yield* child.isRunning.pipe(Effect.orElseSucceed(() => true));
987 if (running) return;
988 // A descendant may inherit stderr, so a closed process cannot wait forever for EOF.
989 yield* Fiber.await(stderrFiber).pipe(Effect.timeoutOption("1 second"));
990 yield* options.onStderrEnd;
991 }),
992 ),
993 ),
975 start: () => start,994 start: () => start,
976 getEvents: () => Stream.fromQueue(eventQueue),995 getEvents: () => Stream.fromQueue(eventQueue),
977 drainEvents,996 drainEvents,
apps/server/src/provider/acp/AntigravityAcpSupport.test.ts+69−0
@@ -8,8 +8,15 @@ import { · 7 unmodified lines
8 type RuntimeMode,8 type RuntimeMode,
9} from "@t3tools/contracts";9} from "@t3tools/contracts";
10import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
11import * as Deferred from "effect/Deferred";
12import * as Exit from "effect/Exit";
13import * as Fiber from "effect/Fiber";
11import * as FileSystem from "effect/FileSystem";14import * as FileSystem from "effect/FileSystem";
12import * as Path from "effect/Path";15import * as Path from "effect/Path";
16import * as Sink from "effect/Sink";
17import * as Stream from "effect/Stream";
18import * as TestClock from "effect/testing/TestClock";
19import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
13import * as EffectAcpErrors from "effect-acp/errors";20import * as EffectAcpErrors from "effect-acp/errors";
14import type * as EffectAcpSchema from "effect-acp/schema";21import type * as EffectAcpSchema from "effect-acp/schema";
15 22
@@ -19,8 +26,70 @@ import { · 3 unmodified lines
19 antigravityPermissionMode,26 antigravityPermissionMode,
20 applyAntigravityAcpModelSelection,27 applyAntigravityAcpModelSelection,
21 buildAntigravityPrompt,28 buildAntigravityPrompt,
29 makeAntigravityAcpRuntime,
22} from "./AntigravityAcpSupport.ts";30} from "./AntigravityAcpSupport.ts";
23 31
32it.effect.each([false, true])(
33 "drains exited validation stderr with inherited pipe: %s",
34 (inherited) =>
35 Effect.gen(function* () {
36 const releaseStderr = yield* Deferred.make<void>();
37 const draining = yield* Deferred.make<void>();
38 const completed = yield* Deferred.make<void>();
39 const diagnostics: string[] = [];
40 const childProcessSpawner = ChildProcessSpawner.make(() =>
41 Effect.succeed(
42 ChildProcessSpawner.makeHandle({
43 pid: ChildProcessSpawner.ProcessId(1),
44 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(1)),
45 isRunning: Deferred.succeed(draining, undefined).pipe(Effect.as(false)),
46 kill: () => Effect.void,
47 unref: Effect.succeed(Effect.void),
48 stdin: Sink.drain,
49 stdout: Stream.empty,
50 stderr: inherited
51 ? Stream.make(new TextEncoder().encode("final startup diagnostic")).pipe(
52 Stream.concat(
53 Stream.fromEffect(Deferred.await(releaseStderr)).pipe(
54 Stream.flatMap(() => Stream.empty),
55 ),
56 ),
57 )
58 : Stream.fromEffect(Deferred.await(releaseStderr)).pipe(
59 Stream.map(() => new TextEncoder().encode("final startup diagnostic")),
60 ),
61 all: Stream.empty,
62 getInputFd: () => Sink.drain,
63 getOutputFd: () => Stream.empty,
64 }),
65 ),
66 );
67 const runtime = yield* makeAntigravityAcpRuntime({
68 spawn: { command: "antigravity-fixture", args: [] },
69 cwd: "/fixture",
70 childProcessSpawner,
71 clientInfo: { name: "test", version: "1" },
72 onDiagnostic: (message) => Effect.sync(() => void diagnostics.push(message)),
73 });
74 const initialization = yield* runtime.initialize().pipe(
75 Effect.exit,
76 Effect.tap(() => Deferred.succeed(completed, undefined)),
77 Effect.forkScoped,
78 );
79 expect(
80 yield* Effect.raceFirst(
81 Deferred.await(draining).pipe(Effect.as("draining")),
82 Deferred.await(completed).pipe(Effect.as("completed")),
83 ),
84 ).toBe("draining");
85 expect(yield* Deferred.isDone(completed)).toBe(false);
86 if (inherited) yield* TestClock.adjust("1 second");
87 else yield* Deferred.succeed(releaseStderr, undefined);
88 expect(Exit.isFailure(yield* Fiber.join(initialization))).toBe(true);
89 expect(diagnostics).toEqual(["final startup diagnostic"]);
90 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
91);
92
24const modelConfig = {93const modelConfig = {
25 id: "model",94 id: "model",
26 name: "Model",95 name: "Model",
apps/server/src/provider/acp/AntigravityAcpSupport.ts+7−4
@@ -31,6 +31,7 @@ export interface AntigravityAcpRuntimeInput extends Omit< · 30 unmodified lines
31 | "cancelBehavior"31 | "cancelBehavior"
32 | "clientCapabilities"32 | "clientCapabilities"
33 | "onStderr"33 | "onStderr"
34 | "onStderrEnd"
34 | "resumeMethod"35 | "resumeMethod"
35 | "transformSessionUpdate"36 | "transformSessionUpdate"
36 | "transformStdout"37 | "transformStdout"
@@ -59,6 +60,10 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")( · 22 unmodified lines
59 EffectAcpErrors.AcpError,60 EffectAcpErrors.AcpError,
60 Crypto.Crypto | Scope.Scope61 Crypto.Crypto | Scope.Scope
61> {62> {
63 const handleStderr = makeAntigravityStderrHandler({
64 ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}),
65 ...(input.onDiagnostic ? { onDiagnostic: input.onDiagnostic } : {}),
66 });
62 const context = yield* Layer.build(67 const context = yield* Layer.build(
63 AcpSessionRuntime.layer({68 AcpSessionRuntime.layer({
64 ...input,69 ...input,
@@ -75,10 +80,8 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")( · 10 unmodified lines
75 transformStdout: makeAntigravityStdoutTransform(80 transformStdout: makeAntigravityStdoutTransform(
76 input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},81 input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},
77 ),82 ),
78 onStderr: makeAntigravityStderrHandler({83 onStderr: handleStderr,
79 ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}),84 ...(input.onDiagnostic ? { onStderrEnd: handleStderr.flushDiagnostics } : {}),
80 ...(input.onDiagnostic ? { onDiagnostic: input.onDiagnostic } : {}),
81 }),
82 transformSessionUpdate: normalizeAntigravitySessionUpdate,85 transformSessionUpdate: normalizeAntigravitySessionUpdate,
83 }).pipe(86 }).pipe(
84 Layer.provide(87 Layer.provide(
apps/server/src/provider/antigravityAuthSupport.test.ts+34−0
@@ -391,6 +391,38 @@ describe("Antigravity stdout compatibility", () => { · 390 unmodified lines
391});391});
392 392
393describe("Antigravity stderr compatibility", () => {393describe("Antigravity stderr compatibility", () => {
394 it.effect.each([
395 { name: "native", prefix: ANTIGRAVITY_AUTH_STDOUT_PREFIX },
396 { name: "browser", prefix: ANTIGRAVITY_AUTH_BROWSER_MARKER },
397 { name: "indented native", prefix: ` ${ANTIGRAVITY_AUTH_STDOUT_PREFIX}` },
398 { name: "indented browser", prefix: ` ${ANTIGRAVITY_AUTH_BROWSER_MARKER}` },
399 { name: "logged native", prefix: `INFO ${ANTIGRAVITY_AUTH_STDOUT_PREFIX}` },
400 { name: "logged browser", prefix: `INFO ${ANTIGRAVITY_AUTH_BROWSER_MARKER}` },
401 {
402 name: "long log prefix",
403 prefix: `${"logged prefix ".repeat(4_096)}${ANTIGRAVITY_AUTH_BROWSER_MARKER}`,
404 },
405 {
406 name: "long indentation",
407 prefix: `${" ".repeat(32_768)}${ANTIGRAVITY_AUTH_BROWSER_MARKER}`,
408 },
409 ])("discards oversized auth messages across fragment boundaries: $name", ({ prefix }) =>
410 Effect.gen(function* () {
411 const diagnostics: string[] = [];
412 const handleStderr = makeAntigravityStderrHandler({
413 onDiagnostic: (message) => Effect.sync(() => void diagnostics.push(message)),
414 });
415 yield* handleStderr(prefix.slice(0, 12));
416 yield* handleStderr(`${prefix.slice(12)}${"x".repeat(32_768)}`);
417 yield* handleStderr("secret-authorization-tail");
418 yield* handleStderr.flushDiagnostics;
419 yield* handleStderr("\nlast startup diagnostic");
420 yield* handleStderr.flushDiagnostics;
421 yield* handleStderr.flushDiagnostics;
422 expect(diagnostics).toEqual(["last startup diagnostic"]);
423 }),
424 );
425
394 it.effect("keeps sign-in URLs out of collected native diagnostics", () =>426 it.effect("keeps sign-in URLs out of collected native diagnostics", () =>
395 Effect.gen(function* () {427 Effect.gen(function* () {
396 const diagnostics: string[] = [];428 const diagnostics: string[] = [];
@@ -407,6 +439,8 @@ describe("Antigravity stderr compatibility", () => { · 10 unmodified lines
407 yield* handleStderr(` ${ANTIGRAVITY_AUTH_STDOUT_PREFIX}${authorizationUrl}\n`);439 yield* handleStderr(` ${ANTIGRAVITY_AUTH_STDOUT_PREFIX}${authorizationUrl}\n`);
408 yield* handleStderr(` ${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`);440 yield* handleStderr(` ${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`);
409 yield* handleStderr(`${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`);441 yield* handleStderr(`${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`);
442 yield* handleStderr(`INFO ${ANTIGRAVITY_AUTH_STDOUT_PREFIX}${authorizationUrl}\n`);
443 yield* handleStderr(`INFO ${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`);
410 expect(diagnostics).toEqual(["Failed to create an AF_INET6 socket."]);444 expect(diagnostics).toEqual(["Failed to create an AF_INET6 socket."]);
411 }),445 }),
412 );446 );
apps/server/src/provider/antigravityAuthSupport.ts+48−14
@@ -470,23 +470,20 @@ export function makeAntigravityStderrHandler( · 469 unmodified lines
470 } = {},470 } = {},
471) {471) {
472 let pending = "";472 let pending = "";
473 let oversizedLine: "diagnostic" | "discard" | undefined;
474 const isAuthorizationLine = (message: string) =>
475 message.includes(ANTIGRAVITY_AUTH_STDOUT_PREFIX) ||
476 message.includes(ANTIGRAVITY_AUTH_BROWSER_MARKER);
473 const handleLine = (line: string) => {477 const handleLine = (line: string) => {
474 const message = line.endsWith("\r") ? line.slice(0, -1) : line;478 const message = line.endsWith("\r") ? line.slice(0, -1) : line;
475 if (message.length > maxBrowserHelperLineLength) {
476 return Effect.void;
477 }
478 const url = message.startsWith(ANTIGRAVITY_AUTH_STDOUT_PREFIX)479 const url = message.startsWith(ANTIGRAVITY_AUTH_STDOUT_PREFIX)
479 ? Effect.succeed(message.slice(ANTIGRAVITY_AUTH_STDOUT_PREFIX.length))480 ? Effect.succeed(message.slice(ANTIGRAVITY_AUTH_STDOUT_PREFIX.length))
480 : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER)481 : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER)
481 ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length))482 ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length))
482 : undefined;483 : undefined;
483 if (url === undefined) {484 if (url === undefined) {
484 // Malformed or indented auth messages must not enter installation diagnostics.485 // Malformed or log-prefixed auth messages must not enter installation diagnostics.
485 const trimmed = message.trimStart();486 if (isAuthorizationLine(message)) {
486 if (
487 trimmed.startsWith(ANTIGRAVITY_AUTH_STDOUT_PREFIX) ||
488 trimmed.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER)
489 ) {
490 return Effect.void;487 return Effect.void;
491 }488 }
492 return input.onDiagnostic?.(message) ?? Effect.void;489 return input.onDiagnostic?.(message) ?? Effect.void;
@@ -503,10 +500,47 @@ export function makeAntigravityStderrHandler( · 10 unmodified lines
503 );500 );
504 };501 };
505 502
506 return Effect.fn("antigravityAuthSupport.handleStderr")(function* (text: string) {503 const finishLine = () => {
507 const lines = `${pending}${text}`.split("\n");504 const message = pending;
508 pending = lines.pop() ?? "";505 const kind = oversizedLine;
509 if (pending.length > maxBrowserHelperLineLength) pending = "";506 pending = "";
510 yield* Effect.forEach(lines, handleLine, { discard: true });507 oversizedLine = undefined;
508 if (kind === "discard") {
509 return Effect.void;
510 }
511 if (kind === "diagnostic") return input.onDiagnostic?.(message) ?? Effect.void;
512 return handleLine(message);
513 };
514
515 const handleStderr = Effect.fn("antigravityAuthSupport.handleStderr")(function* (text: string) {
516 let offset = 0;
517 while (offset < text.length) {
518 const newline = text.indexOf("\n", offset);
519 const end = newline === -1 ? text.length : newline;
520 if (oversizedLine !== "discard") {
521 pending += text.slice(offset, end);
522 if (pending.length > maxBrowserHelperLineLength) {
523 oversizedLine =
524 isAuthorizationLine(pending) || pending.trimStart().length === 0
525 ? "discard"
526 : "diagnostic";
527 pending = oversizedLine === "discard" ? "" : pending.slice(-maxBrowserHelperLineLength);
528 }
529 }
530 if (newline !== -1) yield* finishLine();
531 offset = end + 1;
532 }
533 });
534
535 return Object.assign(handleStderr, {
536 flushDiagnostics: Effect.suspend(() => {
537 if (pending.length === 0 || oversizedLine === "discard" || isAuthorizationLine(pending)) {
538 return Effect.void;
539 }
540 const message = pending.endsWith("\r") ? pending.slice(0, -1) : pending;
541 pending = "";
542 oversizedLine = undefined;
543 return input.onDiagnostic?.(message) ?? Effect.void;
544 }),
511 });545 });
512}546}