Unified · Split
| @@ -438,6 +438,24 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 437 unmodified lines | ||
| 438 | 438 | stderr: [`discarded prefix ${"x".repeat(8_000)}\n`, "last startup diagnostic\n"], |
| 439 | 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 | 460 | name: "no diagnostic", |
| 443 | 461 | stderr: [], |
| @@ -453,6 +471,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 9 unmodified lines | ||
| 453 | 471 | } |
| 454 | 472 | const helper = command.args[0] === "-e"; |
| 455 | 473 | const exited = yield* Deferred.make<ChildProcessSpawner.ExitCode>(); |
| 474 | + const exitObserved = yield* Deferred.make<void>(); | |
| 456 | 475 | const terminate = Deferred.succeed(exited, ChildProcessSpawner.ExitCode(1)).pipe( |
| 457 | 476 | Effect.asVoid, |
| 458 | 477 | ); |
| @@ -461,22 +480,26 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 2 unmodified lines | ||
| 461 | 480 | pid: ChildProcessSpawner.ProcessId(helper ? 1 : 2), |
| 462 | 481 | exitCode: helper |
| 463 | 482 | ? Effect.succeed(ChildProcessSpawner.ExitCode(0)) |
| 464 | − : Deferred.await(exited), | |
| 483 | + : Deferred.await(exited).pipe( | |
| 484 | + Effect.tap(() => Deferred.succeed(exitObserved, undefined)), | |
| 485 | + ), | |
| 465 | 486 | isRunning: Deferred.isDone(exited).pipe(Effect.map((done) => !done)), |
| 466 | 487 | kill: () => terminate, |
| 467 | 488 | unref: Effect.succeed(Effect.void), |
| 468 | 489 | stdin: Sink.drain, |
| 469 | 490 | stdout: helper |
| 470 | 491 | ? Stream.empty |
| 471 | − : Stream.fromEffect(Deferred.await(exited)).pipe(Stream.flatMap(() => Stream.empty)), | |
| 492 | + : Stream.fromEffect(terminate).pipe(Stream.flatMap(() => Stream.empty)), | |
| 472 | 493 | stderr: helper |
| 473 | 494 | ? Stream.make( |
| 474 | 495 | encoder.encode( |
| 475 | 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( | |
| 479 | − Stream.ensuring(terminate), | |
| 499 | + : Stream.fromEffect(Deferred.await(exitObserved)).pipe( | |
| 500 | + Stream.flatMap(() => | |
| 501 | + Stream.fromIterable(testCase.stderr.map((line) => encoder.encode(line))), | |
| 502 | + ), | |
| 480 | 503 | ), |
| 481 | 504 | all: Stream.empty, |
| 482 | 505 | getInputFd: () => Sink.drain, |
| @@ -494,6 +517,7 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 11 unmodified lines | ||
| 494 | 517 | expect(state.message).toContain(testCase.expected); |
| 495 | 518 | expect(state.message?.length).toBeLessThan(4_200); |
| 496 | 519 | expect(state.message).not.toContain("discarded prefix"); |
| 520 | + expect(state.message).not.toContain("secret-authorization-tail"); | |
| 497 | 521 | yield* Deferred.await(stagingReleased); |
| 498 | 522 | yield* expectPreviousRelease(installation); |
| 499 | 523 | }), |
| @@ -104,6 +104,8 @@ export interface AcpSessionRuntimeOptions { · 103 unmodified lines | ||
| 104 | 104 | ) => EffectAcpSchema.SessionNotification; |
| 105 | 105 | /** Receives bounded stderr chunks. Redact secrets before logging. A failure closes the runtime. */ |
| 106 | 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 | 109 | readonly requestLogger?: (event: AcpSessionRequestLogEvent) => Effect.Effect<void, never>; |
| 108 | 110 | readonly protocolLogging?: { |
| 109 | 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 | 455 | Stream.decodeText(), |
| 454 | − Stream.runForEach((chunk) => | |
| 455 | − (options.onStderr | |
| 456 | − ? options.onStderr(chunk.slice(-maxStderrChunkLength)) | |
| 457 | − : Effect.void | |
| 458 | − ).pipe( | |
| 456 | + Stream.runForEach( | |
| 457 | + Effect.fn("AcpSessionRuntime.handleStderr")( | |
| 458 | + function* (chunk: string) { | |
| 459 | + if (!options.onStderr) return; | |
| 460 | + for (let offset = 0; offset < chunk.length; offset += maxStderrChunkLength) { | |
| 461 | + yield* options.onStderr(chunk.slice(offset, offset + maxStderrChunkLength)); | |
| 462 | + } | |
| 463 | + }, | |
| 459 | 464 | Effect.catch((error) => |
| 460 | 465 | Effect.gen(function* () { |
| 461 | 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 | 474 | Effect.ignore, |
| 469 | 475 | Effect.forkIn(runtimeScope), |
| 470 | 476 | ); |
| @@ -971,7 +977,20 @@ export const make = ( · 500 unmodified lines | ||
| 971 | 977 | handleUnknownExtNotification: acp.handleUnknownExtNotification, |
| 972 | 978 | handleExtRequest: acp.handleExtRequest, |
| 973 | 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 | 994 | start: () => start, |
| 976 | 995 | getEvents: () => Stream.fromQueue(eventQueue), |
| 977 | 996 | drainEvents, |
| @@ -8,8 +8,15 @@ import { · 7 unmodified lines | ||
| 8 | 8 | type RuntimeMode, |
| 9 | 9 | } from "@t3tools/contracts"; |
| 10 | 10 | import * as Effect from "effect/Effect"; |
| 11 | +import * as Deferred from "effect/Deferred"; | |
| 12 | +import * as Exit from "effect/Exit"; | |
| 13 | +import * as Fiber from "effect/Fiber"; | |
| 11 | 14 | import * as FileSystem from "effect/FileSystem"; |
| 12 | 15 | import * as Path from "effect/Path"; |
| 16 | +import * as Sink from "effect/Sink"; | |
| 17 | +import * as Stream from "effect/Stream"; | |
| 18 | +import * as TestClock from "effect/testing/TestClock"; | |
| 19 | +import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; | |
| 13 | 20 | import * as EffectAcpErrors from "effect-acp/errors"; |
| 14 | 21 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 15 | 22 | |
| @@ -19,8 +26,70 @@ import { · 3 unmodified lines | ||
| 19 | 26 | antigravityPermissionMode, |
| 20 | 27 | applyAntigravityAcpModelSelection, |
| 21 | 28 | buildAntigravityPrompt, |
| 29 | + makeAntigravityAcpRuntime, | |
| 22 | 30 | } from "./AntigravityAcpSupport.ts"; |
| 23 | 31 | |
| 32 | +it.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 | + | |
| 24 | 93 | const modelConfig = { |
| 25 | 94 | id: "model", |
| 26 | 95 | name: "Model", |
| @@ -31,6 +31,7 @@ export interface AntigravityAcpRuntimeInput extends Omit< · 30 unmodified lines | ||
| 31 | 31 | | "cancelBehavior" |
| 32 | 32 | | "clientCapabilities" |
| 33 | 33 | | "onStderr" |
| 34 | + | "onStderrEnd" | |
| 34 | 35 | | "resumeMethod" |
| 35 | 36 | | "transformSessionUpdate" |
| 36 | 37 | | "transformStdout" |
| @@ -59,6 +60,10 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")( · 22 unmodified lines | ||
| 59 | 60 | EffectAcpErrors.AcpError, |
| 60 | 61 | Crypto.Crypto | Scope.Scope |
| 61 | 62 | > { |
| 63 | + const handleStderr = makeAntigravityStderrHandler({ | |
| 64 | + ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}), | |
| 65 | + ...(input.onDiagnostic ? { onDiagnostic: input.onDiagnostic } : {}), | |
| 66 | + }); | |
| 62 | 67 | const context = yield* Layer.build( |
| 63 | 68 | AcpSessionRuntime.layer({ |
| 64 | 69 | ...input, |
| @@ -75,10 +80,8 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")( · 10 unmodified lines | ||
| 75 | 80 | transformStdout: makeAntigravityStdoutTransform( |
| 76 | 81 | input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}, |
| 77 | 82 | ), |
| 78 | − onStderr: makeAntigravityStderrHandler({ | |
| 79 | − ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}), | |
| 80 | − ...(input.onDiagnostic ? { onDiagnostic: input.onDiagnostic } : {}), | |
| 81 | − }), | |
| 83 | + onStderr: handleStderr, | |
| 84 | + ...(input.onDiagnostic ? { onStderrEnd: handleStderr.flushDiagnostics } : {}), | |
| 82 | 85 | transformSessionUpdate: normalizeAntigravitySessionUpdate, |
| 83 | 86 | }).pipe( |
| 84 | 87 | Layer.provide( |
| @@ -391,6 +391,38 @@ describe("Antigravity stdout compatibility", () => { · 390 unmodified lines | ||
| 391 | 391 | }); |
| 392 | 392 | |
| 393 | 393 | describe("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 | 426 | it.effect("keeps sign-in URLs out of collected native diagnostics", () => |
| 395 | 427 | Effect.gen(function* () { |
| 396 | 428 | const diagnostics: string[] = []; |
| @@ -407,6 +439,8 @@ describe("Antigravity stderr compatibility", () => { · 10 unmodified lines | ||
| 407 | 439 | yield* handleStderr(` ${ANTIGRAVITY_AUTH_STDOUT_PREFIX}${authorizationUrl}\n`); |
| 408 | 440 | yield* handleStderr(` ${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`); |
| 409 | 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 | 444 | expect(diagnostics).toEqual(["Failed to create an AF_INET6 socket."]); |
| 411 | 445 | }), |
| 412 | 446 | ); |
| @@ -470,23 +470,20 @@ export function makeAntigravityStderrHandler( · 469 unmodified lines | ||
| 470 | 470 | } = {}, |
| 471 | 471 | ) { |
| 472 | 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 | 477 | const handleLine = (line: string) => { |
| 474 | 478 | const message = line.endsWith("\r") ? line.slice(0, -1) : line; |
| 475 | − if (message.length > maxBrowserHelperLineLength) { | |
| 476 | − return Effect.void; | |
| 477 | − } | |
| 478 | 479 | const url = message.startsWith(ANTIGRAVITY_AUTH_STDOUT_PREFIX) |
| 479 | 480 | ? Effect.succeed(message.slice(ANTIGRAVITY_AUTH_STDOUT_PREFIX.length)) |
| 480 | 481 | : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER) |
| 481 | 482 | ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length)) |
| 482 | 483 | : undefined; |
| 483 | 484 | if (url === undefined) { |
| 484 | − // Malformed or indented auth messages must not enter installation diagnostics. | |
| 485 | − const trimmed = message.trimStart(); | |
| 486 | − if ( | |
| 487 | − trimmed.startsWith(ANTIGRAVITY_AUTH_STDOUT_PREFIX) || | |
| 488 | − trimmed.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER) | |
| 489 | − ) { | |
| 485 | + // Malformed or log-prefixed auth messages must not enter installation diagnostics. | |
| 486 | + if (isAuthorizationLine(message)) { | |
| 490 | 487 | return Effect.void; |
| 491 | 488 | } |
| 492 | 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) { | |
| 507 | − const lines = `${pending}${text}`.split("\n"); | |
| 508 | − pending = lines.pop() ?? ""; | |
| 509 | − if (pending.length > maxBrowserHelperLineLength) pending = ""; | |
| 510 | − yield* Effect.forEach(lines, handleLine, { discard: true }); | |
| 503 | + const finishLine = () => { | |
| 504 | + const message = pending; | |
| 505 | + const kind = oversizedLine; | |
| 506 | + pending = ""; | |
| 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 | } |