@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
package/AGENTS.md CHANGED
@@ -94,11 +94,11 @@ The repo is wired up with `@effect/language-service` as a `tsconfig.json` `plugi
94
94
  - `src/runtime.ts` wires the Effect beta runtime and OTEL trace + log exporters.
95
95
  - `src/localServer.ts` starts the local Bun OTLP/query server.
96
96
  - `src/httpApi.ts` defines the typed Effect HttpApi surface and OpenAPI spec for the local server.
97
+ - `src/httpListPolicy.ts` owns pure HTTP list/search parameter decoding, bounds, cursors, and pagination metadata shaping.
97
98
  - `src/server.ts` runs the local server without the TUI.
98
99
  - `src/instructions.ts` contains the copied setup instructions for other Effect apps.
99
- - `src/services/TelemetryStore.ts` persists traces and logs in SQLite and exposes indexed queries.
100
- - `src/services/TraceQueryService.ts` reads traces from the local store.
101
- - `src/services/LogQueryService.ts` reads logs from the local store.
100
+ - `src/services/TelemetryStore.ts` persists traces and logs in SQLite and exposes indexed queries through writer and read-only service identifiers.
101
+ - `src/services/TelemetryQuery.ts` proxies read-only store calls to `src/services/telemetryQueryWorker.ts`, keeping synchronous Bun SQLite queries off the HTTP event loop.
102
102
  - `src/config.ts` is the source of truth for ports and env-driven OTEL settings.
103
103
  - `web/` is a Vite + React SPA for the browser-based UI (Tailwind CSS, `@effect/atom-react`, `AtomHttpApi`).
104
104
  - `web/src/api.ts` creates the typed `AtomHttpApi.Service` client from `src/httpApi.ts`.
@@ -107,15 +107,14 @@ The repo is wired up with `@effect/language-service` as a `tsconfig.json` `plugi
107
107
  - The server in `src/localServer.ts` serves `web/dist/` as static files with SPA fallback for non-API routes.
108
108
 
109
109
  ## Tests
110
- - `bun test` runs the suite. Three kinds of tests live in the repo:
110
+ - `bun run test` runs the suite. Three kinds of tests live in the repo:
111
111
  - `src/telemetry.test.ts` exercises the SQLite TelemetryStore with
112
112
  OTLP payloads end-to-end.
113
113
  - `src/ui/waterfallNav.test.ts` unit-tests the pure collapse/expand
114
114
  resolver (no UI).
115
115
  - `src/ui/*.repro.test.ts` drive the real TUI under `tuistory` to
116
- reproduce regressions; each has a sibling `*.repro.seed.ts` that
117
- seeds a deterministic trace into SQLite in a child process. These
118
- are auto-skipped when `tuistory` isn't installed.
116
+ reproduce regressions, using deterministic traces seeded into SQLite
117
+ by child processes. They become no-op passes when `tuistory` is absent.
119
118
 
120
119
  ## Effect Observability Guidance
121
120
  - Inspect the target repo’s existing Effect runtime and observability wiring before adding anything new.
@@ -140,12 +139,16 @@ The repo is wired up with `@effect/language-service` as a `tsconfig.json` `plugi
140
139
  - `MOTEL_OTEL_EXPORTER_URL`: defaults to `http://127.0.0.1:27686/v1/traces`
141
140
  - `MOTEL_OTEL_LOGS_EXPORTER_URL`: defaults to `http://127.0.0.1:27686/v1/logs`
142
141
  - `MOTEL_OTEL_QUERY_URL`: defaults to `http://127.0.0.1:27686`
143
- - `MOTEL_OTEL_DB_PATH`: defaults to `.motel-data/telemetry.sqlite`
142
+ - `MOTEL_OTEL_DB_PATH`: defaults to `${XDG_STATE_HOME:-~/.local/state}/motel/telemetry.sqlite` (one shared DB per machine; daemon log + lock + instance registry live in the same directory)
143
+ - `MOTEL_RUNTIME_DIR`: overrides the daemon log, lock, and instance-registry directory (primarily for isolated tests and custom managed instances)
144
144
  - `MOTEL_OTEL_TRACE_LOOKBACK_MINUTES`: defaults to `1440` (24h)
145
145
  - `MOTEL_OTEL_TRACE_LIMIT`: defaults to `100`
146
146
  - `MOTEL_OTEL_LOG_LIMIT`: defaults to `80`
147
147
  - `MOTEL_OTEL_RETENTION_HOURS`: defaults to `168` (7d)
148
148
  - `MOTEL_OTEL_MAX_DB_SIZE_MB`: defaults to `1024` (size-based retention cap)
149
+ - `MOTEL_OTEL_RETENTION_TRACE_BATCH`: defaults to `100` completed traces per cleanup pass
150
+ - `MOTEL_OTEL_RETENTION_LOG_BATCH`: defaults to `5000` logs per cleanup pass
151
+ - `MOTEL_OTEL_RETENTION_INTERVAL_SECONDS`: defaults to `10`
149
152
 
150
153
  ## TUI Keys
151
154
  - `?`: toggle shortcut help
package/README.md CHANGED
@@ -62,8 +62,19 @@ http://127.0.0.1:27686/v1/traces
62
62
  http://127.0.0.1:27686/v1/logs
63
63
  ```
64
64
 
65
- Motel keeps everything in a local SQLite database at
66
- `.motel-data/telemetry.sqlite`. No Docker, no cloud account.
65
+ Motel keeps everything in a machine-global local SQLite database at
66
+ `${XDG_STATE_HOME:-~/.local/state}/motel/telemetry.sqlite`. One managed
67
+ daemon is shared across local projects. No Docker, no cloud account.
68
+
69
+ The store retains seven days of telemetry by default and targets a 1 GB
70
+ active-data ceiling using bounded background batches. Recent data is preserved while
71
+ the oldest completed traces and logs are removed first. Configure the policy
72
+ with `MOTEL_OTEL_RETENTION_HOURS`, `MOTEL_OTEL_MAX_DB_SIZE_MB`,
73
+ `MOTEL_OTEL_RETENTION_TRACE_BATCH`, `MOTEL_OTEL_RETENTION_LOG_BATCH`, and
74
+ `MOTEL_OTEL_RETENTION_INTERVAL_SECONDS`. Existing databases created without
75
+ incremental auto-vacuum are never silently rewritten at startup; deleted pages
76
+ are reused, but shrinking such a historical file requires an explicit offline
77
+ SQLite `VACUUM` chosen by the user.
67
78
 
68
79
  ## How agents connect
69
80
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitlangton/motel",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,14 +27,22 @@
27
27
  "web"
28
28
  ],
29
29
  "engines": {
30
- "bun": ">=1.1.0"
30
+ "bun": ">=1.3.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
+ "overrides": {
36
+ "@grpc/grpc-js": "1.14.4",
37
+ "protobufjs": "7.6.4",
38
+ "shell-quote": "1.9.0",
39
+ "ws": "8.21.0"
40
+ },
35
41
  "files": [
36
42
  "src",
37
43
  "web/dist",
44
+ "skills/motel-debug/SKILL.md",
45
+ "skills/motel-debug/references/effect.md",
38
46
  "LICENSE",
39
47
  "README.md",
40
48
  "AGENTS.md"
@@ -72,30 +80,34 @@
72
80
  "bench:ingest-logs": "bun run scripts/bench-ingest-logs.ts",
73
81
  "bench:ingest-traces": "bun run scripts/bench-ingest-traces.ts",
74
82
  "bench:search-spans": "bun run scripts/bench-search-spans.ts",
83
+ "release:validate": "bun run scripts/validate-release.ts",
75
84
  "typecheck": "tsc --noEmit",
85
+ "web:typecheck": "tsc --noEmit -p web/tsconfig.json",
76
86
  "prepublishOnly": "bun run web:build"
77
87
  },
78
88
  "devDependencies": {
79
- "@effect/language-service": "^0.85.1",
80
- "@types/bun": "^1.3.12",
81
- "@types/react": "^19.2.14",
82
- "typescript": "^6.0.2"
89
+ "@effect/language-service": "^0.86.2",
90
+ "@types/bun": "^1.3.14",
91
+ "@types/react": "^19.2.17",
92
+ "typescript": "^6.0.3"
83
93
  },
84
94
  "dependencies": {
85
- "@effect/atom-react": "^4.0.0-beta.49",
86
- "@effect/opentelemetry": "^4.0.0-beta.49",
87
- "@effect/platform-bun": "^4.0.0-beta.50",
95
+ "@effect/atom-react": "4.0.0-beta.90",
96
+ "@effect/opentelemetry": "4.0.0-beta.90",
97
+ "@effect/platform-bun": "4.0.0-beta.90",
88
98
  "@opentelemetry/api": "^1.9.0",
89
- "@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
90
- "@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
91
- "@opentelemetry/sdk-logs": "^0.214.0",
92
- "@opentelemetry/sdk-node": "^0.214.0",
93
- "@opentelemetry/sdk-trace-base": "^2.5.0",
94
- "@opentelemetry/sdk-trace-node": "^2.6.1",
95
- "@opentui/core": "^0.1.99",
96
- "@opentui/react": "^0.1.99",
97
- "effect": "^4.0.0-beta.49",
98
- "react": "^19.2.5",
99
+ "@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
100
+ "@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
101
+ "@opentelemetry/otlp-transformer": "0.214.0",
102
+ "@opentelemetry/sdk-logs": "^0.219.0",
103
+ "@opentelemetry/sdk-node": "^0.219.0",
104
+ "@opentelemetry/sdk-trace-base": "^2.8.0",
105
+ "@opentelemetry/sdk-trace-node": "^2.8.0",
106
+ "@opentui/core": "0.4.2",
107
+ "@opentui/react": "0.4.2",
108
+ "effect": "4.0.0-beta.90",
109
+ "protobufjs": "7.6.4",
110
+ "react": "^19.2.7",
99
111
  "scheduler": "^0.27.0"
100
112
  }
101
113
  }
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: motel-debug
3
+ description: Debug applications with motel, a local OpenTelemetry ingest and query server. Use when the user wants runtime-evidence debugging with traces or logs, wants temporary debug instrumentation that can be removed later, or needs a repo wired to send OTLP/HTTP telemetry to a local motel server. If the target repo uses Effect or @effect/*, also read references/effect.md.
4
+ ---
5
+
6
+ # Motel Debug
7
+
8
+ You are in **debug mode**. Debug with runtime evidence, not guesswork.
9
+
10
+ Agents guess based on code alone. You need actual runtime data. Motel is the local OpenTelemetry server that collects traces and logs — use it as your evidence loop.
11
+
12
+ Default local server details:
13
+
14
+ - Base URL: `http://127.0.0.1:27686`
15
+ - OTLP traces: `POST /v1/traces`
16
+ - OTLP logs: `POST /v1/logs`
17
+ - Query API: `GET /api/*`
18
+ - OpenAPI: `GET /openapi.json`
19
+ - Header: `Content-Type: application/json`
20
+ - Auth: none by default
21
+
22
+ If the user provides a different motel URL, use that instead of the default.
23
+
24
+ ## Workflow
25
+
26
+ ### 1. Verify motel is running — and start it if not
27
+
28
+ Check `GET /api/health`. If it returns 200, continue.
29
+
30
+ If it fails (connection refused, timeout, non-200), motel isn't running.
31
+ Start it as a background daemon — **do not** launch the TUI, which is
32
+ interactive and will block your shell:
33
+
34
+ ```bash
35
+ motel start
36
+ ```
37
+
38
+ `motel start` ensures the machine-global managed daemon is running, writes
39
+ runtime files under `${XDG_STATE_HOME:-~/.local/state}/motel/`, and returns a
40
+ JSON status blob. It is idempotent and shared across local projects. If motel isn't on `PATH`, fall
41
+ back to `bunx @kitlangton/motel start`.
42
+
43
+ After starting, re-check `GET /api/health` (may take 1–2s to become
44
+ ready). If it still fails, read `${XDG_STATE_HOME:-~/.local/state}/motel/daemon.log` for the error
45
+ and surface it to the user.
46
+
47
+ Other lifecycle commands, for reference:
48
+
49
+ ```bash
50
+ motel status # JSON status (running? pid? originating workdir?)
51
+ motel stop # stop the shared managed daemon for all local projects
52
+ ```
53
+
54
+ Discover reporting services with `GET /api/services` when needed.
55
+
56
+ ### 2. Generate hypotheses
57
+
58
+ Before touching any code, generate **3-5 specific hypotheses** about why the bug occurs. Be precise — "the cache key doesn't include the user ID" is better than "something is wrong with caching."
59
+
60
+ ### 3. Instrument with tagged debug blocks
61
+
62
+ Add the minimum instrumentation needed to confirm or reject **all** hypotheses in parallel. Every debug block must:
63
+
64
+ - Be wrapped in `#region motel debug` / `#endregion motel debug` markers
65
+ - Include a `debug.hypothesis` attribute linking it to a specific hypothesis
66
+ - Use whatever tracing/logging mechanism the codebase already has (spans, structured logs, annotations — not raw `fetch` calls)
67
+
68
+ Tag every piece of debug instrumentation with structured attributes so you can query it later. Reuse these keys:
69
+
70
+ | Key | Purpose |
71
+ |-----|---------|
72
+ | `debug.session` | Groups all instrumentation for this debug session |
73
+ | `debug.hypothesis` | Links to a specific hypothesis (e.g. `"cache-miss"`, `"A"`) |
74
+ | `debug.step` | Position in the flow (e.g. `"entry"`, `"before-write"`, `"after-read"`) |
75
+ | `debug.label` | Human-readable description of what this point captures |
76
+
77
+ Choose log placements based on your hypotheses:
78
+
79
+ - Function entry with parameters
80
+ - Function exit with return values
81
+ - Values before and after critical operations
82
+ - Branch execution paths (which if/else ran)
83
+ - State mutations and intermediate values
84
+ - Suspected error or edge-case values
85
+
86
+ Guidelines:
87
+
88
+ - At least 1 instrumentation point is required; never skip instrumentation
89
+ - Do not exceed 10 — if you think you need more, narrow your hypotheses
90
+ - Typical range is 2-6
91
+
92
+ ### 4. Reproduce the issue
93
+
94
+ - If a failing test exists, run it directly
95
+ - If reproduction is straightforward (CLI command, curl, simple script), write and run it yourself
96
+ - Otherwise, ask the user to reproduce — provide clear numbered steps and remind them to restart if needed
97
+ - Once a reproduction pathway is established, reuse it for all subsequent iterations
98
+
99
+ ### 5. Analyze evidence
100
+
101
+ Query motel for the debug instrumentation:
102
+
103
+ ```bash
104
+ curl "http://127.0.0.1:27686/api/spans/search?service=<service>&attr.debug.hypothesis=<id>"
105
+ curl "http://127.0.0.1:27686/api/logs/search?service=<service>&attr.debug.session=<session>"
106
+ curl "http://127.0.0.1:27686/api/traces/search?service=<service>&attr.debug.hypothesis=<id>"
107
+ ```
108
+
109
+ For each hypothesis, evaluate: **CONFIRMED**, **REJECTED**, or **INCONCLUSIVE** — cite specific spans, logs, or attribute values as evidence.
110
+
111
+ ### 6. Fix only with evidence
112
+
113
+ Do **not** fix without runtime evidence. When you fix:
114
+
115
+ - Keep all debug instrumentation in place — do not remove it yet
116
+ - Make the fix as small and targeted as possible
117
+ - Reuse existing architecture and patterns; do not overengineer
118
+
119
+ ### 7. Verify the fix
120
+
121
+ Reproduce the issue again with instrumentation still active. Compare before/after evidence:
122
+
123
+ - Cite specific log lines or span attributes that prove the fix works
124
+ - If the fix failed: **revert code changes from rejected hypotheses** (do not let speculative fixes accumulate), generate new hypotheses from different subsystems, add more instrumentation, and iterate
125
+ - Iteration is expected. Taking longer with more data yields better fixes.
126
+
127
+ ### 8. Clean up
128
+
129
+ Only after the fix is verified **and** the user confirms there are no remaining issues:
130
+
131
+ - Run the cleanup script or remove blocks manually (see Cleanup section below)
132
+ - Run `git diff` to confirm only the intentional fix remains
133
+
134
+ ## Instrumentation Rules
135
+
136
+ Wrap every temporary debug block in these exact markers:
137
+
138
+ ```ts
139
+ // #region motel debug
140
+ // temporary debug instrumentation
141
+ // #endregion motel debug
142
+ ```
143
+
144
+ Use whatever the codebase already provides for tracing and logging. The markers are language-comment wrappers — adapt the comment syntax for non-JS/TS files (e.g. `# #region motel debug` for Python).
145
+
146
+ **Do not:**
147
+ - Log secrets, tokens, passwords, or raw PII
148
+ - Remove instrumentation before post-fix verification succeeds
149
+ - Use `setTimeout`, `sleep`, or artificial delays as a "fix"
150
+ - Let code changes from rejected hypotheses accumulate — revert them
151
+
152
+ ## Query Patterns
153
+
154
+ Two filter prefixes for attribute search:
155
+
156
+ | Prefix | Match type | Example |
157
+ |--------|-----------|---------|
158
+ | `attr.<key>=<value>` | Exact match | `attr.debug.hypothesis=cache-miss` |
159
+ | `attrContains.<key>=<substring>` | Case-insensitive substring | `attrContains.ai.prompt.messages=hello world` |
160
+
161
+ ```bash
162
+ curl http://127.0.0.1:27686/api/health
163
+ curl http://127.0.0.1:27686/api/services
164
+
165
+ # Trace search
166
+ curl "http://127.0.0.1:27686/api/traces/search?service=<service>&operation=<text>&attr.debug.session=<session>"
167
+
168
+ # Span search (supports traceId to scope to one trace)
169
+ curl "http://127.0.0.1:27686/api/spans/search?service=<service>&traceId=<trace-id>&attr.debug.hypothesis=<id>"
170
+ curl "http://127.0.0.1:27686/api/spans/search?service=<service>&attrContains.ai.prompt.messages=<phrase>"
171
+
172
+ # Log search (supports severity filter, case-insensitive body search)
173
+ curl "http://127.0.0.1:27686/api/logs/search?service=<service>&severity=ERROR&body=<text>"
174
+ curl "http://127.0.0.1:27686/api/logs/search?service=<service>&attrContains.debug.label=<substring>"
175
+
176
+ # AI call search (compact summaries with previews)
177
+ curl "http://127.0.0.1:27686/api/ai/calls?model=gpt-5.4&sessionId=<session>"
178
+ curl "http://127.0.0.1:27686/api/ai/calls?text=<phrase>&status=error"
179
+
180
+ # AI call detail (full prompt/response payloads)
181
+ curl "http://127.0.0.1:27686/api/ai/calls/<span-id>"
182
+
183
+ # AI stats
184
+ curl "http://127.0.0.1:27686/api/ai/stats?groupBy=model&agg=total_input_tokens"
185
+
186
+ curl http://127.0.0.1:27686/openapi.json
187
+ ```
188
+
189
+ List and search responses include `meta.nextCursor` when more data is available.
190
+
191
+ Motel gives you trace-correlated data — you can see which span a debug log belongs to, the parent operation, timing, and the full trace tree. Use `GET /api/traces/<trace-id>/spans` and `GET /api/spans/<span-id>/logs` to navigate the correlation.
192
+
193
+ For AI/LLM calls, use `/api/ai/calls` for compact searchable summaries (with prompt/response previews and token usage), and `/api/ai/calls/<span-id>` for full payloads.
194
+
195
+ ## Effect
196
+
197
+ If the target repo uses Effect, read `references/effect.md` before changing runtime wiring or adding instrumentation.
198
+
199
+ ## Cleanup
200
+
201
+ Use the bundled script at `scripts/clear-motel-debug.ts` when you want deterministic cleanup. It removes every block between `#region motel debug` and `#endregion motel debug` in JS/TS files and fails on unmatched markers.
202
+
203
+ If you cannot run the script, delete every marked block manually and then grep for `#region motel debug` to confirm none remain.
@@ -0,0 +1,38 @@
1
+ # Effect Notes
2
+
3
+ Apply these only when the target repo already uses Effect or `@effect/*`.
4
+
5
+ ## Runtime
6
+
7
+ - Inspect the existing runtime and observability wiring before adding anything new.
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.
10
+ - Merge telemetry into the main runtime once, not per feature or per request path.
11
+
12
+ ## Instrumentation
13
+
14
+ - Prefer `Effect.fn("...")` for meaningful workflow spans.
15
+ - Add a few child spans around boundaries that are likely to fail or add latency.
16
+ - Emit `Effect.logInfo`, `Effect.logWarning`, and `Effect.logError` with structured fields.
17
+ - Put searchable values in annotations/attributes, not only in the free-form log body.
18
+ - Reuse stable debug keys such as `debug.session`, `debug.hypothesis`, `debug.step`, and `debug.label`.
19
+
20
+ ## Debug Blocks
21
+
22
+ Wrap temporary debug-only Effect instrumentation in removable markers.
23
+
24
+ ```ts
25
+ // #region motel debug
26
+ const program = Effect.fn("feature/doThing")(function*() {
27
+ yield* Effect.logInfo("entered doThing", {
28
+ debug: {
29
+ session: "abc123",
30
+ hypothesis: "cache-miss",
31
+ step: "entry",
32
+ },
33
+ })
34
+ })
35
+ // #endregion motel debug
36
+ ```
37
+
38
+ Keep those blocks until the fix is verified, then remove them with the cleanup script.
package/src/App.tsx CHANGED
@@ -14,16 +14,14 @@ import {
14
14
  attrPickerInputAtom,
15
15
  attrPickerModeAtom,
16
16
  attrFacetStateAtom,
17
- chatDetailChunkIdAtom,
18
- chatDetailScrollOffsetAtom,
19
17
  noticeAtom,
20
- persistSelectedTheme,
21
18
  selectedAttrIndexAtom,
22
- selectedChatChunkIdAtom,
23
19
  selectedThemeAtom,
24
20
  waterfallFilterModeAtom,
25
21
  waterfallFilterTextAtom,
26
- } from "./ui/state.ts"
22
+ } from "./ui/atoms.ts"
23
+ import { chatDetailChunkIdAtom, chatDetailScrollOffsetAtom, selectedChatChunkIdAtom } from "./ui/aiState.ts"
24
+ import { persistSelectedTheme } from "./ui/persistence.ts"
27
25
  import type { ThemeName } from "./ui/theme.ts"
28
26
  import { applyTheme, colors, SEPARATOR, themeLabel } from "./ui/theme.ts"
29
27
  import { useKeyboardNav } from "./ui/useKeyboardNav.ts"
@@ -13,7 +13,6 @@ type ConflictStatus = DaemonStatus & {
13
13
  readonly pid: number
14
14
  readonly workdir: string
15
15
  readonly reason: string
16
- readonly sameWorkdir: false
17
16
  }
18
17
 
19
18
  type ConflictScreenState = {
@@ -61,8 +60,7 @@ const isRecoverableConflict = (status: DaemonStatus | null): status is ConflictS
61
60
  status.service === MOTEL_SERVICE_ID &&
62
61
  status.pid !== null &&
63
62
  status.workdir !== null &&
64
- status.reason !== null &&
65
- !status.sameWorkdir
63
+ status.reason !== null
66
64
 
67
65
  const stopConflictingDaemon = async (status: ConflictStatus) => {
68
66
  const port = parsePort(status.url)
@@ -151,7 +149,7 @@ const RecoveryScreen = ({
151
149
  </box>
152
150
  <box paddingTop={1} flexDirection="column">
153
151
  {notice ? <PlainLine text={notice} fg={busy ? colors.warning : colors.count} /> : null}
154
- <PlainLine text={busy ? "Working..." : "j/k or ↑↓ select · enter run · r retry · k kill conflicting daemon · q quit"} fg={colors.count} />
152
+ <PlainLine text={busy ? "Working..." : "j/k or ↑↓ select · enter run · r retry · k stop incompatible daemon · q quit"} fg={colors.count} />
155
153
  </box>
156
154
  </box>
157
155
  </box>
@@ -192,7 +190,7 @@ export const StartupGate = () => {
192
190
  { key: "r", label: "Retry startup", run: attemptStart },
193
191
  {
194
192
  key: "k",
195
- label: `Stop conflicting daemon (${startupState.status.pid})`,
193
+ label: `Stop incompatible daemon (${startupState.status.pid})`,
196
194
  run: async () => {
197
195
  setStartupState((current) => current.kind === "conflict"
198
196
  ? { ...current, busy: true, notice: `Stopping daemon ${current.status.pid}...` }
@@ -253,16 +251,16 @@ export const StartupGate = () => {
253
251
  const status = startupState.status
254
252
  const detailLines = [
255
253
  `Port: ${status.url}`,
256
- `Conflicting workdir: ${status.workdir}`,
257
- `Conflicting pid: ${status.pid}`,
258
- `Database: ${status.databasePath}`,
254
+ `Daemon workdir: ${status.workdir}`,
255
+ `Daemon pid: ${status.pid}`,
256
+ `Incompatible database: ${status.databasePath}`,
259
257
  status.workdir.startsWith("/tmp") || status.workdir.startsWith("/private/tmp")
260
258
  ? "This looks like a temp/test daemon."
261
- : "This looks like a real motel daemon started from another project.",
259
+ : "This daemon is using a different database configuration.",
262
260
  ]
263
261
  return (
264
262
  <RecoveryScreen
265
- title="Daemon Conflict"
263
+ title="Daemon Configuration Conflict"
266
264
  message={startupState.message}
267
265
  width={width}
268
266
  height={height}
package/src/cli.ts CHANGED
@@ -3,18 +3,17 @@ import { config } from "./config.js"
3
3
  import { otelServerInstructions } from "./instructions.js"
4
4
  import { attributeFiltersFromArgs, isAttributeFilterToken } from "./queryFilters.js"
5
5
  import { queryRuntime } from "./runtime.js"
6
- import { LogQueryService } from "./services/LogQueryService.js"
7
- import { TraceQueryService } from "./services/TraceQueryService.js"
6
+ import { TelemetryStoreReadonly } from "./services/TelemetryStore.js"
8
7
 
9
8
  const [command, ...args] = process.argv.slice(2)
10
9
 
11
- const runQuiet = <A, E, R extends TraceQueryService | LogQueryService | never>(effect: Effect.Effect<A, E, R>) =>
10
+ const runQuiet = <A, E, R extends TelemetryStoreReadonly | never>(effect: Effect.Effect<A, E, R>) =>
12
11
  queryRuntime.runPromise(effect.pipe(Effect.provideService(References.MinimumLogLevel, "None")))
13
12
 
14
13
  try {
15
14
  switch (command) {
16
15
  case "services": {
17
- const result = await runQuiet(Effect.flatMap(TraceQueryService.asEffect(), (query) => query.listServices))
16
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listServices))
18
17
  console.log(JSON.stringify(result, null, 2))
19
18
  break
20
19
  }
@@ -22,7 +21,7 @@ try {
22
21
  case "traces": {
23
22
  const service = args[0] ?? config.otel.serviceName
24
23
  const limit = args[1] ? Number.parseInt(args[1], 10) : config.otel.traceFetchLimit
25
- const result = await runQuiet(Effect.flatMap(TraceQueryService.asEffect(), (query) => query.listRecentTraces(service, { limit })))
24
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listRecentTraces(service, { limit })))
26
25
  console.log(JSON.stringify(result, null, 2))
27
26
  break
28
27
  }
@@ -33,7 +32,7 @@ try {
33
32
  throw new Error("Usage: bun run cli trace <trace-id>")
34
33
  }
35
34
 
36
- const result = await runQuiet(Effect.flatMap(TraceQueryService.asEffect(), (query) => query.getTrace(traceId)))
35
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.getTrace(traceId)))
37
36
  console.log(JSON.stringify(result, null, 2))
38
37
  break
39
38
  }
@@ -55,7 +54,7 @@ try {
55
54
  throw new Error("Usage: bun run cli trace-spans <trace-id>")
56
55
  }
57
56
 
58
- const result = await runQuiet(Effect.flatMap(TraceQueryService.asEffect(), (query) => query.listTraceSpans(traceId)))
57
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listTraceSpans(traceId)))
59
58
  console.log(JSON.stringify(result, null, 2))
60
59
  break
61
60
  }
@@ -68,7 +67,7 @@ try {
68
67
  const attributeStartIndex = operation ? 2 : 1
69
68
  const attributeFilters = attributeFiltersFromArgs(args.slice(attributeStartIndex))
70
69
  const result = await runQuiet(
71
- Effect.flatMap(TraceQueryService.asEffect(), (query) =>
70
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
72
71
  query.searchSpans({
73
72
  serviceName: service,
74
73
  operation,
@@ -87,7 +86,7 @@ try {
87
86
  const operation = args[1] && !isAttributeFilterToken(args[1]) ? args[1] : undefined
88
87
  const attributeFilters = attributeFiltersFromArgs(args.slice(operation ? 2 : 1))
89
88
  const result = await runQuiet(
90
- Effect.flatMap(TraceQueryService.asEffect(), (query) =>
89
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
91
90
  query.searchTraces({
92
91
  serviceName: service,
93
92
  operation,
@@ -110,7 +109,7 @@ try {
110
109
  }
111
110
 
112
111
  const result = await runQuiet(
113
- Effect.flatMap(TraceQueryService.asEffect(), (query) =>
112
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
114
113
  query.traceStats({
115
114
  groupBy,
116
115
  agg,
@@ -131,7 +130,7 @@ try {
131
130
 
132
131
  case "logs": {
133
132
  const service = args[0] ?? config.otel.serviceName
134
- const result = await runQuiet(Effect.flatMap(LogQueryService.asEffect(), (query) => query.listRecentLogs(service)))
133
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listRecentLogs(service)))
135
134
  console.log(JSON.stringify(result, null, 2))
136
135
  break
137
136
  }
@@ -141,7 +140,7 @@ try {
141
140
  const body = args[1] && !isAttributeFilterToken(args[1]) ? args[1] : undefined
142
141
  const attributeFilters = attributeFiltersFromArgs(args.slice(body ? 2 : 1))
143
142
  const result = await runQuiet(
144
- Effect.flatMap(LogQueryService.asEffect(), (query) =>
143
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
145
144
  query.searchLogs({
146
145
  serviceName: service,
147
146
  body,
@@ -163,7 +162,7 @@ try {
163
162
  }
164
163
 
165
164
  const result = await runQuiet(
166
- Effect.flatMap(LogQueryService.asEffect(), (query) =>
165
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
167
166
  query.logStats({
168
167
  groupBy,
169
168
  agg: "count",
@@ -183,7 +182,7 @@ try {
183
182
  throw new Error("Usage: bun run cli trace-logs <trace-id>")
184
183
  }
185
184
 
186
- const result = await runQuiet(Effect.flatMap(LogQueryService.asEffect(), (query) => query.listTraceLogs(traceId)))
185
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listTraceLogs(traceId)))
187
186
  console.log(JSON.stringify(result, null, 2))
188
187
  break
189
188
  }
@@ -195,7 +194,7 @@ try {
195
194
  }
196
195
 
197
196
  const result = await runQuiet(
198
- Effect.flatMap(LogQueryService.asEffect(), (query) =>
197
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
199
198
  query.searchLogs({
200
199
  spanId,
201
200
  limit: config.otel.logFetchLimit,
@@ -214,7 +213,7 @@ try {
214
213
  }
215
214
 
216
215
  const result = await runQuiet(
217
- Effect.flatMap(LogQueryService.asEffect(), (query) =>
216
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
218
217
  query.listFacets({ type, field, limit: 20 }),
219
218
  ),
220
219
  )
package/src/config.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { motelStateDir } from "./registry.js"
2
+ import * as path from "node:path"
3
+
1
4
  const parseBoolean = (value: string | undefined, defaultValue: boolean) => {
2
5
  const normalized = value?.trim().toLowerCase()
3
6
  if (!normalized) return defaultValue
@@ -29,11 +32,14 @@ export const config = {
29
32
  queryUrl: baseUrl,
30
33
  exporterUrl: process.env.MOTEL_OTEL_EXPORTER_URL?.trim() || resolveOtelUrl("/v1/traces"),
31
34
  logsExporterUrl: process.env.MOTEL_OTEL_LOGS_EXPORTER_URL?.trim() || resolveOtelUrl("/v1/logs"),
32
- databasePath: process.env.MOTEL_OTEL_DB_PATH?.trim() || `${import.meta.dir}/../.motel-data/telemetry.sqlite`,
35
+ databasePath: process.env.MOTEL_OTEL_DB_PATH?.trim() || path.join(motelStateDir(), "telemetry.sqlite"),
33
36
  traceLookbackMinutes: parsePositiveInt(process.env.MOTEL_OTEL_TRACE_LOOKBACK_MINUTES, 1440),
34
37
  traceFetchLimit: parsePositiveInt(process.env.MOTEL_OTEL_TRACE_LIMIT, 100),
35
38
  logFetchLimit: parsePositiveInt(process.env.MOTEL_OTEL_LOG_LIMIT, 80),
36
39
  retentionHours: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_HOURS, 168),
37
40
  maxDbSizeMb: parsePositiveInt(process.env.MOTEL_OTEL_MAX_DB_SIZE_MB, 1024),
41
+ retentionTraceBatch: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_TRACE_BATCH, 100),
42
+ retentionLogBatch: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_LOG_BATCH, 5_000),
43
+ retentionIntervalSeconds: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_INTERVAL_SECONDS, 10),
38
44
  },
39
45
  } as const