@aryasaatvik/motel 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -90,7 +90,7 @@ The repo is wired up with `@effect/language-service` as a `tsconfig.json` `plugi
90
90
  - `src/ui/useKeyboardNav.ts` centralises the keyboard handlers and
91
91
  cross-pane navigation state transitions.
92
92
  - `src/motel.ts` composes and runs the public Effect command tree; `src/cli.ts` exports typed telemetry query leaves and handlers.
93
- - `src/runtime.ts` wires the Effect beta runtime and OTEL trace + log exporters.
93
+ - `src/runtime.ts` wires the Effect runtime and OTEL trace + log exporters.
94
94
  - `src/localServer.ts` starts the local Bun OTLP/query server.
95
95
  - `src/httpApi.ts` defines the typed Effect HttpApi surface and OpenAPI spec for the local server.
96
96
  - `src/httpListPolicy.ts` owns pure HTTP list/search parameter decoding, bounds, cursors, and pagination metadata shaping.
@@ -118,7 +118,7 @@ The repo is wired up with `@effect/language-service` as a `tsconfig.json` `plugi
118
118
  ## Effect Observability Guidance
119
119
  - Inspect the target repo’s existing Effect runtime and observability wiring before adding anything new.
120
120
  - Prefer the repo’s existing Effect-native observability APIs if available.
121
- - If `effect/unstable/observability` is already the best fit, prefer it over adding `@effect/opentelemetry`.
121
+ - If `effect/observability` is already the best fit, prefer it over adding `@effect/opentelemetry`.
122
122
  - Only add new OpenTelemetry SDK packages when the repo already uses them or they are clearly required.
123
123
  - Merge telemetry into the main runtime once, not per-feature.
124
124
  - Prefer structured log annotations so fields like `sessionID`, `modelID`, `providerID`, and `tool` are queryable.
package/README.md CHANGED
@@ -50,7 +50,7 @@ curl -fsSL https://bun.sh/install | bash
50
50
  the keyboard cheat sheet, or `c` to copy paste-ready setup instructions
51
51
  for any Effect/OTEL app you want to trace.
52
52
 
53
- Requirements: [Bun](https://bun.sh/) v1.1 or newer.
53
+ Requirements: [Bun](https://bun.sh/) matching `package.json` `engines.bun` (`>=1.3.0`).
54
54
 
55
55
  ### macOS service from the maintained checkout
56
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryasaatvik/motel",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -91,17 +91,17 @@
91
91
  "prepublishOnly": "bun run web:build"
92
92
  },
93
93
  "devDependencies": {
94
- "@effect/language-service": "0.87.1",
94
+ "@effect/language-service": "0.87.2",
95
95
  "@types/bun": "^1.3.14",
96
96
  "@types/react": "^19.2.17",
97
97
  "tegami": "1.4.0",
98
98
  "typescript": "^6.0.3"
99
99
  },
100
100
  "dependencies": {
101
- "@effect/atom-react": "4.0.0-rc.112",
102
- "@effect/opentelemetry": "4.0.0-rc.112",
103
- "@effect/platform-bun": "4.0.0-rc.112",
104
- "@effect/platform-node-shared": "4.0.0-rc.112",
101
+ "@effect/atom-react": "4.0.0",
102
+ "@effect/opentelemetry": "4.0.0",
103
+ "@effect/platform-bun": "4.0.0",
104
+ "@effect/platform-node-shared": "4.0.0",
105
105
  "@opentelemetry/api": "^1.9.0",
106
106
  "@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
107
107
  "@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
@@ -112,7 +112,7 @@
112
112
  "@opentelemetry/sdk-trace-node": "^2.8.0",
113
113
  "@opentui/core": "0.4.2",
114
114
  "@opentui/react": "0.4.2",
115
- "effect": "4.0.0-rc.112",
115
+ "effect": "4.0.0",
116
116
  "protobufjs": "7.6.4",
117
117
  "react": "^19.2.7",
118
118
  "scheduler": "^0.27.0"
@@ -6,7 +6,7 @@ Apply these only when the target repo already uses Effect or `@effect/*`.
6
6
 
7
7
  - Inspect the existing runtime and observability wiring before adding anything new.
8
8
  - Prefer the repo's existing Effect-native observability APIs if they already exist.
9
- - If `effect/unstable/observability` is already the best fit, prefer it over adding new OTEL packages.
9
+ - If `effect/observability` is already the best fit, prefer it over adding new OTEL packages.
10
10
  - Merge telemetry into the main runtime once, not per feature or per request path.
11
11
 
12
12
  ## Instrumentation
package/src/cli.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Console, Effect, Option, References } from "effect"
2
- import { Argument, Command } from "effect/unstable/cli"
2
+ import { Argument, Command } from "effect/cli"
3
3
  import { config } from "./config.js"
4
4
  import { otelServerInstructions } from "./instructions.js"
5
5
  import { attributeFiltersFromArgs, isAttributeFilterToken } from "./queryFilters.js"
@@ -16,7 +16,7 @@ const query = <A>(effect: Effect.Effect<A, unknown, TelemetryStoreReadonly>) =>
16
16
 
17
17
  const optional = (value: Option.Option<string>, fallback?: string) => Option.getOrElse(value, () => fallback)
18
18
 
19
- const searchOperand = Argument.string("service-or-filter").pipe(
19
+ const searchOperand = Argument.String("service-or-filter").pipe(
20
20
  Argument.mapTryCatch(
21
21
  (value) => {
22
22
  if (value.startsWith("attr.") && !isAttributeFilterToken(value)) {
@@ -34,8 +34,8 @@ const services = Command.make("services", {}, () =>
34
34
  ).pipe(Command.withDescription("List observed telemetry services"))
35
35
 
36
36
  const traces = Command.make("traces", {
37
- service: Argument.string("service").pipe(Argument.optional),
38
- limit: Argument.integer("limit").pipe(Argument.optional),
37
+ service: Argument.String("service").pipe(Argument.optional),
38
+ limit: Argument.Int("limit").pipe(Argument.optional),
39
39
  }, ({ service, limit }) =>
40
40
  query(Effect.flatMap(TelemetryStoreReadonly, (store) =>
41
41
  store.listRecentTraces(optional(service, config.otel.serviceName)!, {
@@ -45,13 +45,13 @@ const traces = Command.make("traces", {
45
45
  ).pipe(Command.withDescription("List recent traces"))
46
46
 
47
47
  const trace = Command.make("trace", {
48
- traceId: Argument.string("trace-id"),
48
+ traceId: Argument.String("trace-id"),
49
49
  }, ({ traceId }) =>
50
50
  query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.getTrace(traceId))),
51
51
  ).pipe(Command.withDescription("Get one trace"))
52
52
 
53
53
  const span = Command.make("span", {
54
- spanId: Argument.string("span-id"),
54
+ spanId: Argument.String("span-id"),
55
55
  }, ({ spanId }) =>
56
56
  Effect.promise(() => fetch(`${config.otel.queryUrl}/api/spans/${encodeURIComponent(spanId)}`).then((response) => response.json())).pipe(
57
57
  Effect.andThen(json),
@@ -59,7 +59,7 @@ const span = Command.make("span", {
59
59
  ).pipe(Command.withDescription("Get one span"))
60
60
 
61
61
  const traceSpans = Command.make("trace-spans", {
62
- traceId: Argument.string("trace-id"),
62
+ traceId: Argument.String("trace-id"),
63
63
  }, ({ traceId }) =>
64
64
  query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.listTraceSpans(traceId))),
65
65
  ).pipe(Command.withDescription("List spans in one trace"))
@@ -102,8 +102,8 @@ const searchTraces = Command.make("search-traces", {
102
102
  }).pipe(Command.withDescription("Search trace summaries by service, operation, and attributes"))
103
103
 
104
104
  const traceStats = Command.make("trace-stats", {
105
- groupBy: Argument.string("groupBy"),
106
- agg: Argument.choice("aggregation", ["count", "avg_duration", "p95_duration", "error_rate"]),
105
+ groupBy: Argument.String("groupBy"),
106
+ agg: Argument.Literals("aggregation", ["count", "avg_duration", "p95_duration", "error_rate"]),
107
107
  args: searchOperand,
108
108
  }, ({ groupBy, agg, args }) => {
109
109
  const values = args as ReadonlyArray<string>
@@ -121,7 +121,7 @@ const instructions = Command.make("instructions", {}, () => Console.log(otelServ
121
121
  .pipe(Command.withDescription("Print Effect telemetry setup instructions"))
122
122
 
123
123
  const logs = Command.make("logs", {
124
- service: Argument.string("service").pipe(Argument.optional),
124
+ service: Argument.String("service").pipe(Argument.optional),
125
125
  }, ({ service }) =>
126
126
  query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.listRecentLogs(optional(service, config.otel.serviceName)!))),
127
127
  ).pipe(Command.withDescription("List recent logs"))
@@ -141,7 +141,7 @@ const searchLogs = Command.make("search-logs", {
141
141
  }).pipe(Command.withDescription("Search logs by service, body, and attributes"))
142
142
 
143
143
  const logStats = Command.make("log-stats", {
144
- groupBy: Argument.string("groupBy"),
144
+ groupBy: Argument.String("groupBy"),
145
145
  args: searchOperand,
146
146
  }, ({ groupBy, args }) => {
147
147
  const values = args as ReadonlyArray<string>
@@ -156,13 +156,13 @@ const logStats = Command.make("log-stats", {
156
156
  }).pipe(Command.withDescription("Aggregate log metrics"))
157
157
 
158
158
  const traceLogs = Command.make("trace-logs", {
159
- traceId: Argument.string("trace-id"),
159
+ traceId: Argument.String("trace-id"),
160
160
  }, ({ traceId }) =>
161
161
  query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.listTraceLogs(traceId))),
162
162
  ).pipe(Command.withDescription("List logs for one trace"))
163
163
 
164
164
  const spanLogs = Command.make("span-logs", {
165
- spanId: Argument.string("span-id"),
165
+ spanId: Argument.String("span-id"),
166
166
  }, ({ spanId }) =>
167
167
  query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.searchLogs({
168
168
  spanId,
@@ -171,8 +171,8 @@ const spanLogs = Command.make("span-logs", {
171
171
  ).pipe(Command.withDescription("List logs for one span"))
172
172
 
173
173
  const facets = Command.make("facets", {
174
- type: Argument.choice("type", ["traces", "logs"]),
175
- field: Argument.string("field"),
174
+ type: Argument.Literals("type", ["traces", "logs"]),
175
+ field: Argument.String("field"),
176
176
  }, ({ type, field }) =>
177
177
  query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.listFacets({ type, field, limit: 20 }))),
178
178
  ).pipe(Command.withDescription("List facet values"))
package/src/httpApi.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Schema } from "effect"
2
- import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
2
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api"
3
3
  import {
4
4
  AiCallSummary, AiCallDetail,
5
5
  FacetItem, StatsItem, TraceSpanStatus,
@@ -1,11 +1,11 @@
1
1
  import path from "node:path"
2
2
  import { Effect, FileSystem, Layer } from "effect"
3
3
  import { config } from "./config.js"
4
- import { HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi"
5
- import * as HttpMiddleware from "effect/unstable/http/HttpMiddleware"
6
- import * as HttpRouter from "effect/unstable/http/HttpRouter"
7
- import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
8
- import * as HttpStaticServer from "effect/unstable/http/HttpStaticServer"
4
+ import { HttpApiBuilder, HttpApiScalar } from "effect/http-api"
5
+ import * as HttpMiddleware from "effect/http/HttpMiddleware"
6
+ import * as HttpRouter from "effect/http/HttpRouter"
7
+ import * as HttpServerResponse from "effect/http/HttpServerResponse"
8
+ import * as HttpStaticServer from "effect/http/HttpStaticServer"
9
9
  import * as BunHttpServer from "@effect/platform-bun/BunHttpServer"
10
10
  import { MotelHttpApi } from "./httpApi.js"
11
11
  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"
package/src/mcp.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { BunRuntime, BunStdio } from "@effect/platform-bun"
3
3
  import { Effect, Layer, Logger, Schema } from "effect"
4
4
  import { MOTEL_VERSION } from "./registry.js"
5
- import { McpProtocol, McpServer, Tool, Toolkit } from "effect/unstable/ai"
5
+ import { McpProtocol, McpServer, Tool, Toolkit } from "effect/ai"
6
6
  import { TraceSpanStatus } from "./domain.js"
7
7
  import { MotelClient, MotelClientLive } from "./motelClient.js"
8
8
  import { Locator, LocatorLive } from "./locator.js"
@@ -433,7 +433,8 @@ const ServerLayer = McpServer.toolkit(MotelToolkit).pipe(
433
433
  }),
434
434
  ),
435
435
  Layer.provide(BunStdio.layer),
436
- Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])),
436
+ // stdout carries the MCP stdio protocol, so logs must go to stderr.
437
+ Layer.provide(Layer.merge(Logger.layer([Logger.consolePretty()]), Layer.succeed(Logger.LogToStderr, true))),
437
438
  )
438
439
 
439
440
  Layer.launch(ServerLayer).pipe(BunRuntime.runMain)
package/src/motel.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { BunRuntime, BunServices } from "@effect/platform-bun"
4
4
  import { Console, Effect, Layer } from "effect"
5
- import { CliOutput, Command, Flag } from "effect/unstable/cli"
5
+ import { CliOutput, Command, Flag } from "effect/cli"
6
6
  import packageJson from "../package.json" with { type: "json" }
7
7
  import { queryCommands } from "./cli.js"
8
8
  import { applyManagedDaemonEnv } from "./daemon.js"
@@ -40,7 +40,7 @@ const restart = Command.make("restart", {}, () =>
40
40
 
41
41
  const serviceManager = createLaunchAgentManager()
42
42
  const serviceInstall = Command.make("install", {
43
- replace: Flag.boolean("replace").pipe(Flag.withDefault(false)),
43
+ replace: Flag.Boolean("replace").pipe(Flag.withDefault(false)),
44
44
  }, ({ replace }) =>
45
45
  serviceManager.install(replace).pipe(Effect.andThen(json)),
46
46
  ).pipe(Command.withDescription("Install the per-user Motel LaunchAgent"))
@@ -20,11 +20,10 @@
20
20
  */
21
21
 
22
22
  import * as BunWorker from "@effect/platform-bun/BunWorker"
23
- import { Context, Effect, Layer, Scope, Schema } from "effect"
24
- import * as RpcClient from "effect/unstable/rpc/RpcClient"
25
- import type { RpcClientError } from "effect/unstable/rpc/RpcClientError"
26
- import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
27
- import type { WorkerError } from "effect/unstable/workers/WorkerError"
23
+ import { Context, Effect, Latch, Layer, Scope, Schema } from "effect"
24
+ import * as RpcClient from "effect/rpc/RpcClient"
25
+ import type { RpcClientError } from "effect/rpc/RpcClientError"
26
+ import type { WorkerError } from "effect/workers/WorkerError"
28
27
  import { IngestProgress, WriterEvent, type IngestReadiness } from "../ingestReadiness.ts"
29
28
  import { IngestRpcs } from "./ingestRpc.ts"
30
29
 
@@ -52,14 +51,22 @@ export const AsyncIngestLive = Layer.effect(
52
51
  const event = Schema.decodeUnknownSync(WriterEvent)(data)
53
52
  progress.receive(event)
54
53
  }
54
+ // Opens once the current worker has answered the platform handshake (or
55
+ // died). Effect's worker close message is only received after that
56
+ // handshake; one posted earlier is dropped and the platform finalizer
57
+ // then waits five seconds before terminating the worker.
58
+ let handshake = Latch.makeUnsafe(true)
55
59
  const WorkerProtocol = RpcClient.layerProtocolWorker({ size: 1 }).pipe(
56
- Layer.provide(RpcSerialization.layerMsgPack),
57
60
  Layer.provide(BunWorker.layer(() => {
58
61
  const worker = new Worker(new URL("./telemetryWorker.ts", import.meta.url), {
59
62
  env: { ...process.env, MOTEL_INGEST_DIAGNOSTICS_CHANNEL: channelName },
60
63
  })
61
- worker.addEventListener("error", () => progress.fail())
62
- worker.addEventListener("close", () => progress.fail())
64
+ const spawned = Latch.makeUnsafe(false)
65
+ handshake = spawned
66
+ const settle = () => spawned.openUnsafe()
67
+ worker.addEventListener("message", settle, { once: true })
68
+ worker.addEventListener("error", () => { settle(); progress.fail() })
69
+ worker.addEventListener("close", () => { settle(); progress.fail() })
63
70
  return worker
64
71
  })),
65
72
  )
@@ -76,6 +83,10 @@ export const AsyncIngestLive = Layer.effect(
76
83
  const getClient = yield* Effect.cached(Effect.gen(function*() {
77
84
  const clientScope = yield* Scope.fork(scope, "sequential")
78
85
  const protocolContext = yield* Layer.buildWithScope(WorkerProtocol, clientScope)
86
+ // Added after the protocol's finalizers, so it runs before them: a
87
+ // shutdown during worker startup waits for the handshake and then
88
+ // closes the writer gracefully instead of timing out and terminating it.
89
+ yield* Scope.addFinalizer(clientScope, Effect.suspend(() => handshake.await).pipe(Effect.timeoutOption("5 seconds")))
79
90
  return yield* RpcClient.make(IngestRpcs).pipe(
80
91
  Effect.provide(protocolContext),
81
92
  Effect.provideService(Scope.Scope, clientScope),
@@ -18,8 +18,8 @@
18
18
  */
19
19
 
20
20
  import { Schema } from "effect"
21
- import * as Rpc from "effect/unstable/rpc/Rpc"
22
- import * as RpcGroup from "effect/unstable/rpc/RpcGroup"
21
+ import * as Rpc from "effect/rpc/Rpc"
22
+ import * as RpcGroup from "effect/rpc/RpcGroup"
23
23
 
24
24
  export class IngestError extends Schema.TaggedError<IngestError>()("IngestError", {
25
25
  message: Schema.String,
@@ -17,8 +17,7 @@
17
17
  import { BunRuntime } from "@effect/platform-bun"
18
18
  import * as BunWorkerRunner from "@effect/platform-bun/BunWorkerRunner"
19
19
  import { Effect, Layer } from "effect"
20
- import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
21
- import * as RpcServer from "effect/unstable/rpc/RpcServer"
20
+ import * as RpcServer from "effect/rpc/RpcServer"
22
21
  import type { OtlpLogExportRequest, OtlpTraceExportRequest } from "../otlp.ts"
23
22
  import { WriterDiagnostics, type WriterEvent } from "../ingestReadiness.ts"
24
23
  import { IngestError, IngestRpcs } from "./ingestRpc.ts"
@@ -57,7 +56,6 @@ const WorkerLive = RpcServer.layer(IngestRpcs).pipe(
57
56
  Layer.provide(IngestHandlers),
58
57
  Layer.provide(TelemetryStoreWorkerLive),
59
58
  Layer.provide(RpcServer.layerProtocolWorkerRunner),
60
- Layer.provide(RpcSerialization.layerMsgPack),
61
59
  Layer.provide(BunWorkerRunner.layer),
62
60
  )
63
61
 
package/src/ui/aiState.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Effect } from "effect"
2
- import * as Atom from "effect/unstable/reactivity/Atom"
2
+ import * as Atom from "effect/reactivity/Atom"
3
3
  import type { AiCallDetail } from "../domain.ts"
4
4
  import { queryRuntime } from "../runtime.ts"
5
5
  import { TelemetryStoreReadonly } from "../services/TelemetryStore.ts"
package/src/ui/atoms.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as Atom from "effect/unstable/reactivity/Atom"
1
+ import * as Atom from "effect/reactivity/Atom"
2
2
  import { config, explicitServiceName } from "../config.ts"
3
3
  import type { LogItem, TraceItem, TraceSummaryItem } from "../domain.ts"
4
4
  import type { ThemeName } from "./theme.ts"
@@ -20,7 +20,7 @@ describe("initial TUI service selection", () => {
20
20
 
21
21
  // Fresh processes exercise import-time config and persistence without module-cache leakage.
22
22
  const result = Bun.spawnSync([process.execPath, "--no-env-file", "--eval", `
23
- import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
23
+ import * as AtomRegistry from "effect/reactivity/AtomRegistry"
24
24
  import { config } from "./src/config.ts"
25
25
  import { selectedTraceServiceAtom } from "./src/ui/atoms.ts"
26
26
  const registry = AtomRegistry.make()