@kitlangton/motel 0.2.5 → 0.2.6

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.
Files changed (60) hide show
  1. package/AGENTS.md +11 -8
  2. package/README.md +13 -2
  3. package/package.json +31 -19
  4. package/skills/motel-debug/SKILL.md +203 -0
  5. package/skills/motel-debug/references/effect.md +38 -0
  6. package/src/App.tsx +3 -5
  7. package/src/StartupGate.tsx +8 -10
  8. package/src/cli.ts +15 -16
  9. package/src/config.ts +7 -1
  10. package/src/daemon.test.ts +332 -51
  11. package/src/daemon.ts +103 -152
  12. package/src/httpApi.ts +1 -0
  13. package/src/httpListPolicy.test.ts +76 -0
  14. package/src/httpListPolicy.ts +129 -0
  15. package/src/localServer.ts +194 -323
  16. package/src/mcp.ts +2 -1
  17. package/src/opentui-jsx.d.ts +11 -0
  18. package/src/otlp.test.ts +65 -0
  19. package/src/otlp.ts +20 -0
  20. package/src/otlpProtobuf.ts +35 -0
  21. package/src/registry.ts +37 -11
  22. package/src/runtime.ts +2 -6
  23. package/src/services/AsyncIngest.ts +20 -8
  24. package/src/services/LogQueryService.ts +11 -25
  25. package/src/services/TelemetryQuery.ts +62 -0
  26. package/src/services/TelemetryStore.ts +433 -249
  27. package/src/services/TraceQueryService.ts +18 -52
  28. package/src/services/ingestRpc.ts +2 -4
  29. package/src/services/queryRpc.ts +15 -0
  30. package/src/services/telemetryQueryWorker.ts +32 -0
  31. package/src/services/telemetryWorker.ts +5 -8
  32. package/src/storybook/aiChatStory.tsx +1 -1
  33. package/src/telemetry.test.ts +307 -41
  34. package/src/ui/AiChatView.tsx +1 -1
  35. package/src/ui/AttrFilterModal.tsx +1 -1
  36. package/src/ui/ServiceLogs.tsx +10 -7
  37. package/src/ui/SpanContentView.tsx +24 -21
  38. package/src/ui/TraceDetailsPane.tsx +1 -1
  39. package/src/ui/TraceList.tsx +1 -1
  40. package/src/ui/aiState.ts +10 -22
  41. package/src/ui/app/TraceWorkspace.tsx +2 -1
  42. package/src/ui/app/useAppLayout.ts +1 -1
  43. package/src/ui/app/useTraceScreenData.ts +22 -18
  44. package/src/ui/cachedLoader.test.ts +23 -0
  45. package/src/ui/cachedLoader.ts +60 -0
  46. package/src/ui/loaders.ts +34 -53
  47. package/src/ui/primitives.tsx +1 -1
  48. package/src/ui/state.ts +2 -0
  49. package/src/ui/traceDetailsWidth.repro.test.ts +12 -1
  50. package/src/ui/traceSortNav.repro.seed.ts +1 -1
  51. package/src/ui/traceSortNav.repro.test.ts +12 -2
  52. package/src/ui/useAttrFilterPicker.ts +10 -8
  53. package/src/ui/useKeyboardNav.ts +3 -6
  54. package/src/ui/waterfallNav.repro.seed.ts +1 -1
  55. package/src/ui/waterfallNav.repro.test.ts +16 -8
  56. package/web/dist/assets/index-B01z9BaO.css +2 -0
  57. package/web/dist/assets/index-M86tcih5.js +22 -0
  58. package/web/dist/index.html +2 -2
  59. package/web/dist/assets/index-DnyVo03x.js +0 -27
  60. package/web/dist/assets/index-DzuHNBGV.css +0 -2
@@ -2,6 +2,10 @@ import { Effect, Layer, Context } from "effect"
2
2
  import type { AiCallDetail, SpanItem, TraceItem, TraceSummaryItem } from "../domain.js"
3
3
  import { TelemetryStore } from "./TelemetryStore.js"
4
4
 
5
+ /**
6
+ * Compatibility adapter for consumers importing the historical trace query module.
7
+ * New internal callers should use TelemetryStoreReadonly directly.
8
+ */
5
9
  export class TraceQueryService extends Context.Service<
6
10
  TraceQueryService,
7
11
  {
@@ -22,56 +26,18 @@ export class TraceQueryService extends Context.Service<
22
26
 
23
27
  export const TraceQueryServiceLive = Layer.effect(
24
28
  TraceQueryService,
25
- Effect.gen(function* () {
26
- const store = yield* TelemetryStore
27
-
28
- const listServices = Effect.fn("motel/TraceQueryService.listServices")(function* () {
29
- const services = yield* store.listServices
30
- yield* Effect.annotateCurrentSpan("trace.service_count", services.length)
31
- return services
32
- })()
33
-
34
- const listRecentTraces = Effect.fn("motel/TraceQueryService.listRecentTraces")(function* (serviceName: string, options?: { readonly lookbackMinutes?: number; readonly limit?: number; readonly cursorStartedAtMs?: number; readonly cursorTraceId?: string }) {
35
- yield* Effect.annotateCurrentSpan({
36
- "trace.service_name": serviceName,
37
- })
38
- const traces = yield* store.listRecentTraces(serviceName, options)
39
- yield* Effect.annotateCurrentSpan("trace.result_count", traces.length)
40
- return traces
41
- })
42
-
43
- const listTraceSummaries = Effect.fn("motel/TraceQueryService.listTraceSummaries")(function* (serviceName: string | null, options?: { readonly lookbackMinutes?: number; readonly limit?: number; readonly cursorStartedAtMs?: number; readonly cursorTraceId?: string }) {
44
- yield* Effect.annotateCurrentSpan({
45
- "trace.service_name": serviceName,
46
- })
47
- const traces = yield* store.listTraceSummaries(serviceName, options)
48
- yield* Effect.annotateCurrentSpan("trace.result_count", traces.length)
49
- return traces
50
- })
51
-
52
- const getTrace = Effect.fn("motel/TraceQueryService.getTrace")(function* (traceId: string) {
53
- yield* Effect.annotateCurrentSpan("trace.trace_id", traceId)
54
- return yield* store.getTrace(traceId)
55
- })
56
-
57
- const getSpan = Effect.fn("motel/TraceQueryService.getSpan")(function* (spanId: string) {
58
- yield* Effect.annotateCurrentSpan("trace.span_id", spanId)
59
- return yield* store.getSpan(spanId)
60
- })
61
-
62
- return TraceQueryService.of({
63
- listServices,
64
- listRecentTraces,
65
- listTraceSummaries,
66
- searchTraceSummaries: store.searchTraceSummaries,
67
- listFacets: store.listFacets,
68
- searchTraces: store.searchTraces,
69
- traceStats: store.traceStats,
70
- getTrace,
71
- getSpan,
72
- getAiCall: store.getAiCall,
73
- listTraceSpans: store.listTraceSpans,
74
- searchSpans: store.searchSpans,
75
- })
76
- }),
29
+ Effect.map(TelemetryStore, (store) => TraceQueryService.of({
30
+ listServices: store.listServices,
31
+ listRecentTraces: store.listRecentTraces,
32
+ listTraceSummaries: store.listTraceSummaries,
33
+ searchTraceSummaries: store.searchTraceSummaries,
34
+ listFacets: store.listFacets,
35
+ searchTraces: store.searchTraces,
36
+ traceStats: store.traceStats,
37
+ getTrace: store.getTrace,
38
+ getSpan: store.getSpan,
39
+ getAiCall: store.getAiCall,
40
+ listTraceSpans: store.listTraceSpans,
41
+ searchSpans: store.searchSpans,
42
+ })),
77
43
  )
@@ -5,10 +5,8 @@
5
5
  *
6
6
  * Only ingestTraces and ingestLogs run through RPC — those are the
7
7
  * methods whose SQLite writes used to block the main event loop for
8
- * seconds at a time. Every other TelemetryStore method stays on the
9
- * main thread with its own direct DB connection; SQLite's WAL mode
10
- * lets the reader (main) and writer (worker) hold independent
11
- * connections to the same file concurrently without contention.
8
+ * seconds at a time. Read-only TelemetryStore methods use the separate
9
+ * query RPC worker so no synchronous SQLite call runs on the HTTP thread.
12
10
  *
13
11
  * Payloads are typed as Schema.Unknown because OTLP's protobuf-JSON
14
12
  * shape is enormous and nested — the store validates structurally
@@ -0,0 +1,15 @@
1
+ import { Schema } from "effect"
2
+ import * as Rpc from "effect/unstable/rpc/Rpc"
3
+ import * as RpcGroup from "effect/unstable/rpc/RpcGroup"
4
+
5
+ export class QueryError extends Schema.TaggedErrorClass<QueryError>()("QueryError", {
6
+ message: Schema.String,
7
+ }) {}
8
+
9
+ export const QueryRpcs = RpcGroup.make(
10
+ Rpc.make("query", {
11
+ payload: { method: Schema.String, args: Schema.Array(Schema.Unknown) },
12
+ success: Schema.Unknown,
13
+ error: QueryError,
14
+ }),
15
+ )
@@ -0,0 +1,32 @@
1
+ import { BunRuntime } from "@effect/platform-bun"
2
+ import * as BunWorkerRunner from "@effect/platform-bun/BunWorkerRunner"
3
+ import { Effect, Layer } from "effect"
4
+ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
5
+ import * as RpcServer from "effect/unstable/rpc/RpcServer"
6
+ import { TelemetryStoreQueryWorkerLive, TelemetryStoreReadonly, type TelemetryStoreReader } from "./TelemetryStore.js"
7
+ import { QueryError, QueryRpcs } from "./queryRpc.js"
8
+
9
+ type QueryMethod = keyof TelemetryStoreReader
10
+
11
+ const QueryHandlers = QueryRpcs.toLayer(Effect.gen(function*() {
12
+ const store = yield* TelemetryStoreReadonly
13
+ return {
14
+ query: ({ method, args }) => {
15
+ const member = Reflect.get(store, method as QueryMethod) as unknown
16
+ const result = typeof member === "function" ? Reflect.apply(member, store, args) : member
17
+ return (result as Effect.Effect<unknown, Error>).pipe(
18
+ Effect.mapError((error) => new QueryError({ message: String(error) })),
19
+ )
20
+ },
21
+ }
22
+ }))
23
+
24
+ const WorkerLive = RpcServer.layer(QueryRpcs).pipe(
25
+ Layer.provide(QueryHandlers),
26
+ Layer.provide(TelemetryStoreQueryWorkerLive),
27
+ Layer.provide(RpcServer.layerProtocolWorkerRunner),
28
+ Layer.provide(RpcSerialization.layerMsgPack),
29
+ Layer.provide(BunWorkerRunner.layer),
30
+ )
31
+
32
+ Layer.launch(WorkerLive).pipe(BunRuntime.runMain)
@@ -6,15 +6,12 @@
6
6
  * evaluated in a FRESH module graph on the worker side. In particular
7
7
  * `TelemetryStoreWorkerLive` opens its own SQLite connection here — the main
8
8
  * thread's store connection is unrelated. SQLite's WAL journal mode
9
- * lets both connections coexist against the same `.sqlite` file: the
10
- * worker writes, the main thread reads, and neither blocks the other.
9
+ * lets writer and query-worker connections coexist against the same
10
+ * `.sqlite` file without blocking the HTTP event loop.
11
11
  *
12
- * The worker only exposes `ingestTraces` / `ingestLogs` (see
13
- * ingestRpc.ts). Query methods stay on the main thread because they're
14
- * already fast (1-14ms) and round-tripping them through structured-
15
- * clone would add more overhead than it saves. This is a deliberately
16
- * narrow interface — the payoff is that main-thread HTTP queries
17
- * never queue behind a heavy OTLP batch again.
12
+ * The worker exposes only `ingestTraces` / `ingestLogs` (see ingestRpc.ts)
13
+ * and owns writer maintenance. Read queries run in telemetryQueryWorker.ts;
14
+ * neither path can block the HTTP event loop.
18
15
  */
19
16
 
20
17
  import { BunRuntime } from "@effect/platform-bun"
@@ -18,7 +18,7 @@ import { useEffect, useMemo, useRef, useState } from "react"
18
18
  import { buildChunks, type Chunk } from "../ui/aiChatModel.ts"
19
19
  import { AiChatView } from "../ui/AiChatView.tsx"
20
20
  import { Divider, TextLine } from "../ui/primitives.tsx"
21
- import type { AiCallDetailState } from "../ui/state.ts"
21
+ import type { AiCallDetailState } from "../ui/aiState.ts"
22
22
  import { applyTheme, colors, SEPARATOR } from "../ui/theme.ts"
23
23
  import type { ChatFixture } from "./fixtures/index.ts"
24
24
  import { errorFixture } from "./fixtures/errorState.ts"