From 0bfd18aca886e2a927c214a742530f305633c168 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 22 Jul 2026 15:43:14 +0000 Subject: [PATCH 1/4] fix(logger): move logger to main thread to have compatibility with compiled binary --- src/index.ts | 2 +- src/logging/fileLogger.tsx | 45 +++++++++++++++++------------- src/logging/pino-roll.d.ts | 34 ++++++++++++++++++++++ src/middleware/withLogging.test.ts | 2 +- src/testing/logging.tsx | 3 +- 5 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 src/logging/pino-roll.d.ts diff --git a/src/index.ts b/src/index.ts index bb41c6042..d456198db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ process.exit( // TODO: wire this id into telemetry as well const cliSessionId = crypto.randomUUID(); - const rootLogger = createFileLogger({ + const rootLogger = await createFileLogger({ filePath: join(homedir(), ".agentcore", "logs", "output"), // TODO: allow overriding via global settings logLevel: LOG_LEVEL.DEBUG, diff --git a/src/logging/fileLogger.tsx b/src/logging/fileLogger.tsx index b08f3c64c..e51b83c6b 100644 --- a/src/logging/fileLogger.tsx +++ b/src/logging/fileLogger.tsx @@ -1,4 +1,5 @@ import pino from "pino"; +import pinoRoll from "pino-roll"; import { type AsyncLogger, type LoggerBindings, type LogLevel } from "./types"; export interface FileLoggerConfig { @@ -32,31 +33,35 @@ function wrapPinoLogger(pinoLogger: pino.Logger): AsyncLogger { * @param config - Logger configuration (file path, rotation limits, level). * @returns A {@link AsyncLogger} that writes to a rotating file via pino. */ -export function createFileLogger(config: FileLoggerConfig): AsyncLogger { +export async function createFileLogger(config: FileLoggerConfig): Promise { const maxSizeInMB = config.maxSizeInMB ?? 10; const maxFileCount = config.maxFileCount ?? 5; const bindings = config.bindings ?? {}; + + // setup logging stream in the same thread as main execution to avoid separate worker thread. + // separate worker thread attempts to import pino-roll at runtime, which fails when run as an executable. + const stream = await pinoRoll({ + extension: ".log", + dateFormat: "yyyy-MM-dd'T'HH-mm-ss", + // Rotate when file reaches {maxSizeInMB} MB, and start deleting once we have {maxFileCount} files + size: `${maxSizeInMB}m`, + limit: { count: maxFileCount }, + file: config.filePath, + mkdir: true, + }); + return wrapPinoLogger( - pino({ - level: config.logLevel, - base: undefined, // omit pid and hostname - formatters: { - level(label) { - return { level: label }; - }, - }, - transport: { - target: "pino-roll", - options: { - extension: ".log", - dateFormat: "yyyy-MM-dd'T'HH-mm-ss", - // Rotate when file reaches {maxSizeInMB} MB, and start deleting once we have {maxFileCount} files - size: `${maxSizeInMB}m`, - limit: { count: maxFileCount }, - file: config.filePath, - mkdir: true, + pino( + { + level: config.logLevel, + base: undefined, // omit pid and hostname + formatters: { + level(label) { + return { level: label }; + }, }, }, - }), + stream, + ), ).child(bindings); } diff --git a/src/logging/pino-roll.d.ts b/src/logging/pino-roll.d.ts new file mode 100644 index 000000000..a2ae9d21f --- /dev/null +++ b/src/logging/pino-roll.d.ts @@ -0,0 +1,34 @@ +// Type declarations for pino-roll, which does not ship its own types. +// Source of truth: https://github.com/mcollina/pino-roll/blob/master/pino-roll.js +declare module "pino-roll" { + import type { SonicBoom, SonicBoomOpts } from "sonic-boom"; + + interface LimitOptions { + count?: number; + } + + interface PinoRollOptions extends Omit { + /** Absolute or relative path to the log file. */ + file: string; + /** Maximum size before rotation (e.g. "10m", "1g"). */ + size?: string | number; + /** Rotation frequency ("daily", "hourly", or milliseconds). */ + frequency?: string | number; + /** File extension appended after the number (e.g. ".log"). */ + extension?: string; + /** Whether to create a symlink to the current log file. */ + symlink?: boolean; + /** Date format string appended to the file name. */ + dateFormat?: string; + /** Strategy for removing old log files. */ + limit?: LimitOptions; + /** Create parent directories if they don't exist. */ + mkdir?: boolean; + } + + /** + * Creates a Pino transport (a SonicBoom stream) that writes to files + * and automatically rotates based on size, frequency, or both. + */ + export default function pinoRoll(options: PinoRollOptions): Promise; +} diff --git a/src/middleware/withLogging.test.ts b/src/middleware/withLogging.test.ts index 862c35891..c38ed0c3d 100644 --- a/src/middleware/withLogging.test.ts +++ b/src/middleware/withLogging.test.ts @@ -14,7 +14,7 @@ describe("withLogging", () => { beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "logging-test-")); - logger = createFileLogger({ + logger = await createFileLogger({ filePath: join(tempDir, "output"), logLevel: LOG_LEVEL.DEBUG, }); diff --git a/src/testing/logging.tsx b/src/testing/logging.tsx index ba4e56521..cc3321fa9 100644 --- a/src/testing/logging.tsx +++ b/src/testing/logging.tsx @@ -58,8 +58,7 @@ export async function assertLogsMatch( ): Promise { let lastResults: ReturnType = []; - // pino-roll writes via async worker threads, so logs may not be flushed to - // disk immediately. Poll until all query conditions are satisfied. + // Poll until all query conditions are satisfied. try { await waitFor(async () => { const content = await readLogFile(dir); From e753b375f586c19bdccc422c6af903bfd94a2fa3 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 22 Jul 2026 16:02:20 +0000 Subject: [PATCH 2/4] fix(root): ensure any initialization errors are logged --- src/index.ts | 1 - src/runnable/index.tsx | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index d456198db..af0fad40e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,7 +50,6 @@ process.exit( await rootHandler.route(argv); } catch (e) { const error = e instanceof Error ? e : new Error(String(e)); - io.stderr.write(`${error.name}: ${error.message}\n`); rootLogger .child({ errorName: error.name, errorMessage: error.message, stack: error.stack ?? "" }) .error(); diff --git a/src/runnable/index.tsx b/src/runnable/index.tsx index b4a7f84ac..51fef1ec6 100644 --- a/src/runnable/index.tsx +++ b/src/runnable/index.tsx @@ -27,7 +27,9 @@ export async function runWithExitCode( try { await fn(argv); return ExitCode.SUCCESS; - } catch { + } catch (e) { + const error = e instanceof Error ? e : new Error(String(e)); + console.error(`${error.name}: ${error.message}`); return ExitCode.FAILURE; } } From d45048d2d9aef41af95a6f05c1b7b33845341d6a Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 22 Jul 2026 16:02:41 +0000 Subject: [PATCH 3/4] feat(ci): add compile step to ci that verifies binary --- .github/workflows/build.yml | 2 -- .github/workflows/ci.yml | 6 +++++ .github/workflows/compile.yml | 44 +++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/compile.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 031e84709..21302d06d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,5 +31,3 @@ jobs: if: always() - run: bun pm pack if: always() - - run: bun run compile - if: always() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c8e47aba..c4929a6bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,3 +27,9 @@ jobs: ref: ${{ github.event.pull_request.head.sha || github.sha }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + compile-binaries: + uses: ./.github/workflows/compile.yml + permissions: + contents: read + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml new file mode 100644 index 000000000..5805144ac --- /dev/null +++ b/.github/workflows/compile.yml @@ -0,0 +1,44 @@ +# Compiles native binaries for each platform and verifies they run. +name: compile-binaries +on: + workflow_call: + inputs: + ref: + required: true + type: string + +jobs: + smoke-test: + name: Compile (${{ matrix.name }}) + runs-on: ${{ fromJSON(matrix.runner) }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - name: Linux + runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}"]' + target: linux-x64 + binary: ./dist/bin/agentcore-linux-x64 + - name: Windows + runner: '["codebuild-agentcore-e2e-${{ github.run_id }}-${{ github.run_attempt }}", "image:windows-1.0"]' + target: windows-x64 + binary: ./dist/bin/agentcore-windows-x64.exe + # CodeBuild does not support macOS. + - name: macOS + runner: '["macos-latest"]' + target: darwin-arm64 + binary: ./dist/bin/agentcore-darwin-arm64 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + - name: Compile binary + run: bun run compile:${{ matrix.target }} + - name: Verify binary runs + run: | + ${{ matrix.binary }} --help From 1c5ee77373f69daaebd85e119c66b47411b700b3 Mon Sep 17 00:00:00 2001 From: Hweinstock Date: Wed, 22 Jul 2026 17:06:48 +0000 Subject: [PATCH 4/4] refactor(logging): rename declaration file to use camelCase --- src/logging/{pino-roll.d.ts => pinoRoll.d.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/logging/{pino-roll.d.ts => pinoRoll.d.ts} (100%) diff --git a/src/logging/pino-roll.d.ts b/src/logging/pinoRoll.d.ts similarity index 100% rename from src/logging/pino-roll.d.ts rename to src/logging/pinoRoll.d.ts