Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ The repo is wired up with `@effect/language-service` as a `tsconfig.json` `plugi
## Effect Observability Guidance
- Inspect the target repo’s existing Effect runtime and observability wiring before adding anything new.
- Prefer the repo’s existing Effect-native observability APIs if available.
- If `effect/unstable/observability` is already the best fit, prefer it over adding `@effect/opentelemetry`.
- If `effect/observability` is already the best fit, prefer it over adding `@effect/opentelemetry`.
- Only add new OpenTelemetry SDK packages when the repo already uses them or they are clearly required.
- Merge telemetry into the main runtime once, not per-feature.
- Prefer structured log annotations so fields like `sessionID`, `modelID`, `providerID`, and `tool` are queryable.
Expand Down
66 changes: 13 additions & 53 deletions bun.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@
"typescript": "^6.0.3"
},
"dependencies": {
"@effect/atom-react": "4.0.0-beta.90",
"@effect/opentelemetry": "4.0.0-beta.90",
"@effect/platform-bun": "4.0.0-beta.90",
"@effect/atom-react": "4.0.1",
"@effect/opentelemetry": "4.0.1",
"@effect/platform-bun": "4.0.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
Expand All @@ -105,7 +105,7 @@
"@opentelemetry/sdk-trace-node": "^2.8.0",
"@opentui/core": "0.4.2",
"@opentui/react": "0.4.2",
"effect": "4.0.0-beta.90",
"effect": "4.0.1",
"protobufjs": "7.6.4",
"react": "^19.2.7",
"scheduler": "^0.27.0"
Expand Down
2 changes: 1 addition & 1 deletion skills/motel-debug/references/effect.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Apply these only when the target repo already uses Effect or `@effect/*`.

- Inspect the existing runtime and observability wiring before adding anything new.
- Prefer the repo's existing Effect-native observability APIs if they already exist.
- If `effect/unstable/observability` is already the best fit, prefer it over adding new OTEL packages.
- If `effect/observability` is already the best fit, prefer it over adding new OTEL packages.
- Merge telemetry into the main runtime once, not per feature or per request path.

## Instrumentation
Expand Down
2 changes: 1 addition & 1 deletion src/httpApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Schema } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api"
import {
AiCallSummary, AiCallDetail,
FacetItem, StatsItem, TraceSpanStatus,
Expand Down
10 changes: 5 additions & 5 deletions src/localServer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import path from "node:path"
import { Effect, FileSystem, Layer } from "effect"
import { config } from "./config.js"
import { HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi"
import * as HttpMiddleware from "effect/unstable/http/HttpMiddleware"
import * as HttpRouter from "effect/unstable/http/HttpRouter"
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
import * as HttpStaticServer from "effect/unstable/http/HttpStaticServer"
import { HttpApiBuilder, HttpApiScalar } from "effect/http-api"
import * as HttpMiddleware from "effect/http/HttpMiddleware"
import * as HttpRouter from "effect/http/HttpRouter"
import * as HttpServerResponse from "effect/http/HttpServerResponse"
import * as HttpStaticServer from "effect/http/HttpStaticServer"
import * as BunHttpServer from "@effect/platform-bun/BunHttpServer"
import { MotelHttpApi } from "./httpApi.js"
import { AI_LIST, LOG_LIST, LOG_STATS, SPAN_LIST, TRACE_LIST, TRACE_STATS, listMeta, logCursorArgs, paginateLogs, paginateSummaries, parseLimit, parseListParams as decodeListParams, parseLookbackMinutes, requestUrl as decodeRequestUrl, traceCursorArgs, type ListBounds, type ListParams } from "./httpListPolicy.js"
Expand Down
6 changes: 4 additions & 2 deletions src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { BunRuntime, BunStdio } from "@effect/platform-bun"
import { Effect, Layer, Logger, Schema } from "effect"
import { MOTEL_VERSION } from "./registry.js"
import { McpServer, Tool, Toolkit } from "effect/unstable/ai"
import { McpProtocol, McpServer, Tool, Toolkit } from "effect/ai"
import { TraceSpanStatus } from "./domain.js"
import { MotelClient, MotelClientLive } from "./motelClient.js"
import { Locator, LocatorLive } from "./locator.js"
Expand Down Expand Up @@ -424,10 +424,12 @@ const ServerLayer = McpServer.toolkit(MotelToolkit).pipe(
McpServer.layerStdio({
name: "motel",
version: MOTEL_VERSION,
protocols: [McpProtocol.v2025_11_25, McpProtocol.v2025_06_18, McpProtocol.v2025_03_26, McpProtocol.v2024_11_05],
}),
),
Layer.provide(BunStdio.layer),
Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])),
Layer.provide(Logger.layer([Logger.consolePretty()])),
Layer.provide(Layer.succeed(Logger.LogToStderr, true)),
)

Layer.launch(ServerLayer).pipe(BunRuntime.runMain)
10 changes: 4 additions & 6 deletions src/services/AsyncIngest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@

import * as BunWorker from "@effect/platform-bun/BunWorker"
import { Context, Duration, Effect, Exit, Layer, Scope } from "effect"
import * as RpcClient from "effect/unstable/rpc/RpcClient"
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError"
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
import type { WorkerError } from "effect/unstable/workers/WorkerError"
import * as RpcClient from "effect/rpc/RpcClient"
import type { RpcClientError } from "effect/rpc/RpcClientError"
import type { WorkerError } from "effect/workers/WorkerError"
import { IngestRpcs } from "./ingestRpc.ts"

// RpcClient.make always surfaces RpcClientError in addition to the
Expand All @@ -38,10 +37,9 @@ export class AsyncIngest extends Context.Service<
>()("@motel/AsyncIngest") {}

// Protocol: RpcClient.layerProtocolWorker manages a worker pool and
// speaks msgpack over structured-clone messages. `size: 1` matches
// speaks over structured-clone messages. `size: 1` matches
// SQLite's single-writer constraint.
const WorkerProtocol = RpcClient.layerProtocolWorker({ size: 1 }).pipe(
Layer.provide(RpcSerialization.layerMsgPack),
Layer.provide(
BunWorker.layer(() => new Worker(new URL("./telemetryWorker.ts", import.meta.url))),
),
Expand Down
10 changes: 4 additions & 6 deletions src/services/TelemetryQuery.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import * as BunWorker from "@effect/platform-bun/BunWorker"
import { Duration, Effect, Exit, Layer, Scope } from "effect"
import * as RpcClient from "effect/unstable/rpc/RpcClient"
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
import type { WorkerError } from "effect/unstable/workers/WorkerError"
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError"
import * as RpcClient from "effect/rpc/RpcClient"
import type { WorkerError } from "effect/workers/WorkerError"
import type { RpcClientError } from "effect/rpc/RpcClientError"
import { TelemetryStoreReadonly, type TelemetryStoreReader } from "./TelemetryStore.js"
import { QueryRpcs } from "./queryRpc.js"

type QueryMethod = keyof TelemetryStoreReader
type QueryClient = RpcClient.FromGroup<typeof QueryRpcs, RpcClientError | WorkerError>
type QueryClientEntry = { readonly client: QueryClient; readonly clientScope: Scope.Scope }
type QueryClientEntry = { readonly client: QueryClient; readonly clientScope: Scope.Closeable }

const WorkerProtocol = RpcClient.layerProtocolWorker({ size: 1 }).pipe(
Layer.provide(RpcSerialization.layerMsgPack),
Layer.provide(BunWorker.layer(() => new Worker(new URL("./telemetryQueryWorker.ts", import.meta.url)))),
)

Expand Down
13 changes: 7 additions & 6 deletions src/services/ingestRpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* seconds at a time. Read-only TelemetryStore methods use the separate
* query RPC worker so no synchronous SQLite call runs on the HTTP thread.
*
* Payloads are typed as Schema.Unknown because OTLP's protobuf-JSON
* Payloads are typed as StructuredClone (a pass-through) because OTLP's protobuf-JSON
* shape is enormous and nested — the store validates structurally
* during the actual insert loop, and serialising a schema through
* the worker boundary would add overhead that beats the purpose of
Expand All @@ -18,21 +18,22 @@
*/

import { Schema } from "effect"
import * as Rpc from "effect/unstable/rpc/Rpc"
import * as RpcGroup from "effect/unstable/rpc/RpcGroup"
import * as Rpc from "effect/rpc/Rpc"
import * as RpcGroup from "effect/rpc/RpcGroup"
import { StructuredClone } from "./rpcSchema.js"

export class IngestError extends Schema.TaggedErrorClass<IngestError>()("IngestError", {
export class IngestError extends Schema.TaggedError<IngestError>()("IngestError", {
message: Schema.String,
}) {}

export const IngestRpcs = RpcGroup.make(
Rpc.make("ingestTraces", {
payload: { payload: Schema.Unknown },
payload: { payload: StructuredClone },
success: Schema.Struct({ insertedSpans: Schema.Number }),
error: IngestError,
}),
Rpc.make("ingestLogs", {
payload: { payload: Schema.Unknown },
payload: { payload: StructuredClone },
success: Schema.Struct({ insertedLogs: Schema.Number }),
error: IngestError,
}),
Expand Down
11 changes: 6 additions & 5 deletions src/services/queryRpc.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Schema } from "effect"
import * as Rpc from "effect/unstable/rpc/Rpc"
import * as RpcGroup from "effect/unstable/rpc/RpcGroup"
import * as Rpc from "effect/rpc/Rpc"
import * as RpcGroup from "effect/rpc/RpcGroup"
import { StructuredClone } from "./rpcSchema.js"

export class QueryError extends Schema.TaggedErrorClass<QueryError>()("QueryError", {
export class QueryError extends Schema.TaggedError<QueryError>()("QueryError", {
message: Schema.String,
}) {}

export const QueryRpcs = RpcGroup.make(
Rpc.make("query", {
payload: { method: Schema.String, args: Schema.Array(Schema.Unknown) },
success: Schema.Unknown,
payload: { method: Schema.String, args: Schema.Array(StructuredClone) },
success: StructuredClone,
error: QueryError,
}),
)
6 changes: 6 additions & 0 deletions src/services/rpcSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Schema } from "effect"

// Worker RPC encodes with the JSON codec, which walks the whole value and
// rejects `Date`, `undefined`, non-finite numbers, etc. Worker messages travel
// via structured clone, so pass values through untouched.
export const StructuredClone = Schema.declare((_: unknown): _ is unknown => true, { toCodecJson: () => undefined })
4 changes: 1 addition & 3 deletions src/services/telemetryQueryWorker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { BunRuntime } from "@effect/platform-bun"
import * as BunWorkerRunner from "@effect/platform-bun/BunWorkerRunner"
import { Effect, Layer } from "effect"
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
import * as RpcServer from "effect/unstable/rpc/RpcServer"
import * as RpcServer from "effect/rpc/RpcServer"
import { TelemetryStoreQueryWorkerLive, TelemetryStoreReadonly, type TelemetryStoreReader } from "./TelemetryStore.js"
import { QueryError, QueryRpcs } from "./queryRpc.js"

Expand All @@ -25,7 +24,6 @@ const WorkerLive = RpcServer.layer(QueryRpcs).pipe(
Layer.provide(QueryHandlers),
Layer.provide(TelemetryStoreQueryWorkerLive),
Layer.provide(RpcServer.layerProtocolWorkerRunner),
Layer.provide(RpcSerialization.layerMsgPack),
Layer.provide(BunWorkerRunner.layer),
)

Expand Down
4 changes: 1 addition & 3 deletions src/services/telemetryWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
import { BunRuntime } from "@effect/platform-bun"
import * as BunWorkerRunner from "@effect/platform-bun/BunWorkerRunner"
import { Effect, Layer } from "effect"
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
import * as RpcServer from "effect/unstable/rpc/RpcServer"
import * as RpcServer from "effect/rpc/RpcServer"
import type { OtlpLogExportRequest, OtlpTraceExportRequest } from "../otlp.ts"
import { IngestError, IngestRpcs } from "./ingestRpc.ts"
import { TelemetryStore, TelemetryStoreWorkerLive } from "./TelemetryStore.ts"
Expand Down Expand Up @@ -49,7 +48,6 @@ const WorkerLive = RpcServer.layer(IngestRpcs).pipe(
Layer.provide(IngestHandlers),
Layer.provide(TelemetryStoreWorkerLive),
Layer.provide(RpcServer.layerProtocolWorkerRunner),
Layer.provide(RpcSerialization.layerMsgPack),
Layer.provide(BunWorkerRunner.layer),
)

Expand Down
2 changes: 1 addition & 1 deletion src/ui/aiState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Effect } from "effect"
import * as Atom from "effect/unstable/reactivity/Atom"
import * as Atom from "effect/reactivity/Atom"
import type { AiCallDetail } from "../domain.ts"
import { queryRuntime } from "../runtime.ts"
import { TelemetryStoreReadonly } from "../services/TelemetryStore.ts"
Expand Down
2 changes: 1 addition & 1 deletion src/ui/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as Atom from "effect/unstable/reactivity/Atom"
import * as Atom from "effect/reactivity/Atom"
import { config, explicitServiceName } from "../config.ts"
import type { LogItem, TraceItem, TraceSummaryItem } from "../domain.ts"
import type { ThemeName } from "./theme.ts"
Expand Down
2 changes: 1 addition & 1 deletion src/ui/serviceSelection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("initial TUI service selection", () => {

// Fresh processes exercise import-time config and persistence without module-cache leakage.
const result = Bun.spawnSync([process.execPath, "--no-env-file", "--eval", `
import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
import * as AtomRegistry from "effect/reactivity/AtomRegistry"
import { config } from "./src/config.ts"
import { selectedTraceServiceAtom } from "./src/ui/atoms.ts"
const registry = AtomRegistry.make()
Expand Down
4 changes: 2 additions & 2 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
"preview": "vite preview"
},
"dependencies": {
"@effect/atom-react": "4.0.0-beta.90",
"@effect/atom-react": "4.0.1",
"@tailwindcss/vite": "^4.3.1",
"@tanstack/react-virtual": "^3.14.3",
"effect": "4.0.0-beta.90",
"effect": "4.0.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.0",
Expand Down
4 changes: 2 additions & 2 deletions web/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { AtomHttpApi } from "effect/unstable/reactivity"
import { FetchHttpClient } from "effect/http"
import { AtomHttpApi } from "effect/reactivity"
import { MotelHttpApi } from "@motel/httpApi"

export const MotelClient = AtomHttpApi.Service()("MotelClient", {
Expand Down
2 changes: 1 addition & 1 deletion web/src/pages/AiCallsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Fragment, useMemo, Suspense, useState } from "react"
import { useSearchParams, Link } from "react-router-dom"
import { useAtomValue } from "@effect/atom-react"
import type { AsyncResult } from "effect/unstable/reactivity"
import type { AsyncResult } from "effect/reactivity"
import { MotelClient } from "../api"
import { PageHeader, RefreshButton, LoadingState, ErrorState, EmptyState } from "../components/shared"
import { formatDuration, formatRelativeTime } from "../format"
Expand Down
2 changes: 1 addition & 1 deletion web/src/pages/LogsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo, useState, Suspense } from "react"
import { useSearchParams, Link } from "react-router-dom"
import { useAtomValue } from "@effect/atom-react"
import type { AsyncResult } from "effect/unstable/reactivity"
import type { AsyncResult } from "effect/reactivity"
import { MotelClient } from "../api"
import {
PageContainer,
Expand Down
2 changes: 1 addition & 1 deletion web/src/pages/TracesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useMemo, useRef, Suspense } from "react"
import { useSearchParams, useNavigate } from "react-router-dom"
import { useAtomValue } from "@effect/atom-react"
import { useVirtualizer } from "@tanstack/react-virtual"
import type { AsyncResult } from "effect/unstable/reactivity"
import type { AsyncResult } from "effect/reactivity"
import { MotelClient } from "../api"
import { PageHeader, RefreshButton, LiveBadge, LoadingState, ErrorState, EmptyState } from "../components/shared"
import { formatDuration, formatRelativeTime, serviceColor } from "../format"
Expand Down