diff --git a/packages/util/src/npm-config.ts b/packages/util/src/npm-config.ts index bb127edb35b..e642ac64619 100644 --- a/packages/util/src/npm-config.ts +++ b/packages/util/src/npm-config.ts @@ -7,13 +7,15 @@ import Config from "@npmcli/config" import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js" import { Effect } from "effect" -const npmPath = fileURLToPath(new URL("..", import.meta.url)) +// Lazy: on workerd import.meta.url is undefined and constructing a URL from it +// at module scope fails startup validation; npm config is never used there. +const npmPath = () => fileURLToPath(new URL("..", import.meta.url)) export const load = (dir: string) => Effect.tryPromise({ try: async () => { const config = new Config({ - npmPath, + npmPath: npmPath(), cwd: dir, env: { ...process.env }, argv: [process.execPath, process.execPath, "--prefix", dir], diff --git a/packages/util/src/observability.ts b/packages/util/src/observability.ts index ffb17f14169..880a869244e 100644 --- a/packages/util/src/observability.ts +++ b/packages/util/src/observability.ts @@ -1,6 +1,6 @@ export * as Observability from "./observability.js" -import { NodeFileSystem } from "@effect/platform-node" +import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem" import { LayerNode } from "./effect/layer-node.js" import { Effect, Layer, Logger, References, Schema } from "effect" import { FetchHttpClient } from "effect/unstable/http" @@ -50,4 +50,10 @@ export function layer( ).pipe(Layer.catchCause(() => local)) } -export const node = LayerNode.make({ name: "observability", layer: layer(), deps: [] }) +// Layer.suspend: constructing the loggers eagerly at module scope performs +// I/O (file logger, run id) that workerd forbids in global scope. +export const node = LayerNode.make({ + name: "observability", + layer: Layer.suspend(() => layer()), + deps: [], +}) diff --git a/packages/util/src/observability/logging.ts b/packages/util/src/observability/logging.ts index 458ca2245d8..ee1836bf05c 100644 --- a/packages/util/src/observability/logging.ts +++ b/packages/util/src/observability/logging.ts @@ -3,7 +3,7 @@ import path from "path" import { Global } from "../global.js" import { runID } from "./shared.js" -function formatter(id: string = runID) { +function formatter(id: string = runID()) { return Logger.map(Logger.formatStructured, (output) => { const messages = Array.isArray(output.message) ? output.message : [output.message] return [ @@ -51,7 +51,7 @@ export function file(local = true, channel = "local") { return path.join(Global.Path.log, `opencode-${channel.replace(/[^a-zA-Z0-9._-]/g, "-")}.log`) } -export function fileLogger(target = file(), id: string = runID) { +export function fileLogger(target = file(), id: string = runID()) { // Do not set batchWindow to 0; it causes high idle CPU usage. return Effect.gen(function* () { const fs = yield* FileSystem.FileSystem diff --git a/packages/util/src/observability/otlp.ts b/packages/util/src/observability/otlp.ts index fb268bf4dc0..a72a5a85bcb 100644 --- a/packages/util/src/observability/otlp.ts +++ b/packages/util/src/observability/otlp.ts @@ -54,8 +54,8 @@ export function resource(app: App = { client: "opencode", version: "unknown", ch ...resourceAttributes(), "deployment.environment.name": app.channel, "opencode.client": app.client, - "opencode.run": runID, - "service.instance.id": runID, + "opencode.run": runID(), + "service.instance.id": runID(), }, } } diff --git a/packages/util/src/observability/shared.ts b/packages/util/src/observability/shared.ts index 76393aacce5..3ecc9987573 100644 --- a/packages/util/src/observability/shared.ts +++ b/packages/util/src/observability/shared.ts @@ -1 +1,8 @@ -export const runID = crypto.randomUUID().slice(0, 8) +// Lazy: workerd forbids generating random values in global scope, so the id +// materializes on first call (inside a handler) and stays stable afterwards. +let generated: string | undefined + +export function runID(): string { + generated ??= crypto.randomUUID().slice(0, 8) + return generated +}