Run prettier on backlog folder
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
id: doc-1
|
||||
title: Alternatives for reducing collection chat token usage
|
||||
type: other
|
||||
created_date: '2026-05-02 16:12'
|
||||
updated_date: '2026-05-04 06:55'
|
||||
created_date: "2026-05-02 16:12"
|
||||
updated_date: "2026-05-04 06:55"
|
||||
---
|
||||
|
||||
# ML-156 Research: Alternatives for reducing collection chat token usage
|
||||
|
||||
Research document for [ML-156 - Explore alternatives to reduce token usage when providing collection context to LLM for collection chat](backlog://task/ML-156).
|
||||
@@ -14,11 +15,12 @@ Research document for [ML-156 - Explore alternatives to reduce token usage when
|
||||
## Current state
|
||||
|
||||
**Token flow per new collection chat:**
|
||||
|
||||
1. `Collection.collection_summary/0` runs on mount via `start_async`
|
||||
2. Loads ALL records: `from(r in Record, where: not is_nil(r.purchased_at), order_by: [order_alphabetically()], select: ^essential_fields())`
|
||||
3. Groups by `musicbrainz_id`, formats each group as `"Artist - Title (year, formats) [genre1, genre2]"`
|
||||
4. Builds stats header: `"# Stats: N releases, M artists\nGenres: ...\nFormats: ...\nEras: ..."`
|
||||
5. Returns `{stats + "\n\n" + catalog, group_count}`
|
||||
5. Returns `{stats + "\n\n" + catalog, group_count}`
|
||||
6. Stored in `@collection_summary` assign on the LiveView
|
||||
7. Passed to Chat component as `chat_context={@collection_summary}`
|
||||
8. When user sends first message, `do_send_message` calls `chat_module.stream_response(messages, chat_context, callback)`
|
||||
@@ -30,6 +32,7 @@ Research document for [ML-156 - Explore alternatives to reduce token usage when
|
||||
**For 500 releases:** ~9,000 input tokens for catalog alone + ~100 tokens for stats + ~500 tokens for prompt template = ~9,600 tokens
|
||||
|
||||
**Key files:**
|
||||
|
||||
- `lib/music_library/collection.ex:203-235` — `collection_summary/0` (loads and formats all records)
|
||||
- `lib/music_library/chats/collection_chat.ex:18-31` — `build_instructions/2` (embeds summary in prompt)
|
||||
- `lib/music_library/chats/prompt.ex` — `Prompt.build/2` (wraps in identity + approach)
|
||||
@@ -41,6 +44,7 @@ Research document for [ML-156 - Explore alternatives to reduce token usage when
|
||||
### Streaming architecture constraints
|
||||
|
||||
The Chat component dispatches streaming to a `Task.Supervisor` child:
|
||||
|
||||
```elixir
|
||||
Task.Supervisor.start_child(MusicLibrary.TaskSupervisor, fn ->
|
||||
case chat_module.stream_response(stream_messages, chat_context, fn chunk ->
|
||||
@@ -65,17 +69,20 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
|
||||
**Token savings:** ~9,000 → ~100 input tokens (~99% reduction)
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Simplest possible change; < 10 lines of code
|
||||
- No architectural changes needed
|
||||
- No streaming infrastructure changes
|
||||
- The LLM can still answer statistical questions ("what's my most common genre?", "how many jazz records do I have?")
|
||||
|
||||
**Cons:**
|
||||
|
||||
- LLM loses ability to answer specific questions ("do I have Kid A?", "which Radiohead albums do I own?", "show me my 90s electronic albums")
|
||||
- User experience degrades for record-specific queries
|
||||
- The LLM will hallucinate or say "I don't have access to your specific collection" frequently
|
||||
|
||||
**Impact on code:**
|
||||
|
||||
1. `CollectionChat.build_instructions/2` — remove `#{collection_summary}` interpolation, keep only stats
|
||||
2. `Collection.collection_summary/0` — could be simplified to return only stats (or keep as-is, the function is also tested independently)
|
||||
|
||||
@@ -88,7 +95,9 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
|
||||
**Token savings:** ~9,000 → ~100 input tokens base + tool call overhead + tool results (~200-500 tokens when actually searching)
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Add a tool definition to the Responses API request:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "function",
|
||||
@@ -97,7 +106,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query"}
|
||||
"query": { "type": "string", "description": "Search query" }
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
@@ -114,6 +123,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
|
||||
3. The Chat component's streaming architecture needs to handle this multi-turn flow.
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Maximal token efficiency — only pay for what's actually needed
|
||||
- LLM can answer arbitrary specific questions with real data
|
||||
- Scales to any collection size
|
||||
@@ -121,6 +131,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
|
||||
- Leverages the existing `tools` infrastructure in the Responses API request
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **Significantly more complex** — requires:
|
||||
- Changes to `OpenAI.API.chat_stream/6` to support function calls in streaming mode
|
||||
- Changes to `decode_responses_event/2` to handle function call SSE events
|
||||
@@ -132,6 +143,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
|
||||
- More error states to handle (function execution failures, parse errors)
|
||||
|
||||
**Implementation scope:**
|
||||
|
||||
1. **Tool definition module** — New module ~30 lines defining the OpenAI function tool schema
|
||||
2. **Function executor** — New module or function in `CollectionChat` that executes the tool call (~20 lines)
|
||||
3. **Streaming changes in `OpenAI.API`** — New `chat_stream_with_tools/7` or modify `chat_stream/6` to accept tools and handle function call events (~80 lines)
|
||||
@@ -150,12 +162,14 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
|
||||
**Token savings:** ~9,000 → ~400 tokens (summary text + stats)
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Good middle ground — compact but informative
|
||||
- The LLM has a narrative understanding of the collection
|
||||
- No streaming architecture changes needed
|
||||
- Single one-time cost to generate (amortized across many chats)
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Still doesn't give the LLM ability to answer specific queries ("do I have Kid A?")
|
||||
- Summary can go stale if not regenerated on collection changes
|
||||
- Requires an initial LLM call to generate the summary (token cost + latency)
|
||||
@@ -164,6 +178,7 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
|
||||
- Adds a new background job / async concern
|
||||
|
||||
**Implementation scope:**
|
||||
|
||||
1. Store the cached summary (new DB field on a canonical collection `Chat` record, or a new schema)
|
||||
2. A function/worker to generate the LLM summary (calls OpenAI, stores result)
|
||||
3. Invalidation triggers (PubSub on record add/edit/delete, regenerate async)
|
||||
@@ -179,11 +194,13 @@ The callback sends `[chunk: chunk]` updates; `update/2` in the Chat component ac
|
||||
**Token savings:** ~9,000 → ~100 tokens base + tool results (0-500 tokens per use)
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Best of both worlds: statistical awareness always available, specific lookup on demand
|
||||
- Token-efficient — base cost is minimal
|
||||
- LLM knows to reach for the tool when appropriate
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Same implementation complexity as Alternative B for the tool infrastructure
|
||||
- Slightly more prompt engineering to ensure the model uses the tool appropriately
|
||||
|
||||
@@ -198,6 +215,7 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us
|
||||
**Token savings:** ~9,000 → ~100 tokens base + retrieval overhead (~200-500 when model searches)
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Format collection catalog as text (same as current `collection_summary/0` output)
|
||||
2. Upload to OpenAI: `POST /v1/files` with `purpose: "assistants"` → `file_id`
|
||||
3. Create vector store: `POST /v1/vector_stores` → `vector_store_id`
|
||||
@@ -208,6 +226,7 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us
|
||||
**Critical difference from Alt B:** No SSE event handling changes, no orchestration loop, no custom tool execution. `file_search` works exactly like `web_search_preview` (already in use) — OpenAI handles retrieval automatically.
|
||||
|
||||
**Pros:**
|
||||
|
||||
- ~150 lines vs ~250 for Alt B
|
||||
- No changes to `decode_responses_event/2` or Chat streaming loop
|
||||
- Semantic search (finds "upbeat 80s rock" not just keywords)
|
||||
@@ -215,6 +234,7 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us
|
||||
- Reusable for artist bios, notes, etc.
|
||||
|
||||
**Cons:**
|
||||
|
||||
- File can go stale if not updated on collection change
|
||||
- 2-3 new API endpoints needed (Files API, Vector Stores API)
|
||||
- Vector store indexing is async (may need brief poll)
|
||||
@@ -226,13 +246,13 @@ Upload the collection catalog as a file to OpenAI, create a vector store, and us
|
||||
|
||||
## Comparison
|
||||
|
||||
| Criterion | A (stats-only) | B (custom func) | C (cached) | E (file_search) |
|
||||
|---|---|---|---|---|
|
||||
| Implementation complexity | ★☆☆☆☆ | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ |
|
||||
| Response quality | ★★☆☆☆ | ★★★★★ | ★★★☆☆ | ★★★★★ |
|
||||
| Token efficiency | ★★★★★ | ★★★★★ | ★★★★☆ | ★★★★★ |
|
||||
| Infrastructure risk | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★★★☆ |
|
||||
| Reusability | ★☆☆☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ |
|
||||
| Criterion | A (stats-only) | B (custom func) | C (cached) | E (file_search) |
|
||||
| ------------------------- | -------------- | --------------- | ---------- | --------------- |
|
||||
| Implementation complexity | ★☆☆☆☆ | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ |
|
||||
| Response quality | ★★☆☆☆ | ★★★★★ | ★★★☆☆ | ★★★★★ |
|
||||
| Token efficiency | ★★★★★ | ★★★★★ | ★★★★☆ | ★★★★★ |
|
||||
| Infrastructure risk | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★★★☆ |
|
||||
| Reusability | ★☆☆☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
id: doc-2
|
||||
title: Prevent pi from accessing sensitive files
|
||||
type: other
|
||||
created_date: '2026-05-03 13:32'
|
||||
updated_date: '2026-05-04 06:55'
|
||||
created_date: "2026-05-03 13:32"
|
||||
updated_date: "2026-05-04 06:55"
|
||||
---
|
||||
|
||||
# Implementation Analysis: Prevent pi from accessing sensitive files
|
||||
|
||||
## Summary
|
||||
@@ -13,14 +14,14 @@ The task requires a **declarative** way to intercept pi tool calls that would ac
|
||||
|
||||
## Sensitive File Categories (for this project)
|
||||
|
||||
| Category | Patterns | Examples |
|
||||
|----------|----------|----------|
|
||||
| Environment secrets | `.env`, `.env.*`, `.envrc` | `.env`, `.env.production` |
|
||||
| Key files | `*.pem`, `*.key`, `*.key.pub` | SSH keys |
|
||||
| Secret/credential files | `*secret*`, `*credential*`, `*credentials*` | `secrets.yml`, `credentials.json` |
|
||||
| Config dirs | `.ssh/`, `.aws/`, `.gnupg/` | SSH config, AWS credentials |
|
||||
| Production config | Any path known to contain secrets | `rel/`, production env vars |
|
||||
| Pi session files | `~/.pi/agent/sessions/` | Session JSONL files (contain tool outputs) |
|
||||
| Category | Patterns | Examples |
|
||||
| ----------------------- | ------------------------------------------- | ------------------------------------------ |
|
||||
| Environment secrets | `.env`, `.env.*`, `.envrc` | `.env`, `.env.production` |
|
||||
| Key files | `*.pem`, `*.key`, `*.key.pub` | SSH keys |
|
||||
| Secret/credential files | `*secret*`, `*credential*`, `*credentials*` | `secrets.yml`, `credentials.json` |
|
||||
| Config dirs | `.ssh/`, `.aws/`, `.gnupg/` | SSH config, AWS credentials |
|
||||
| Production config | Any path known to contain secrets | `rel/`, production env vars |
|
||||
| Pi session files | `~/.pi/agent/sessions/` | Session JSONL files (contain tool outputs) |
|
||||
|
||||
## Available Pi Extension Mechanisms
|
||||
|
||||
@@ -37,6 +38,7 @@ pi.on("tool_call", async (event, ctx) => {
|
||||
```
|
||||
|
||||
**Key capabilities:**
|
||||
|
||||
- `event.toolName` — identifies the tool (`read`, `bash`, `grep`, `find`, `ls`, `write`, `edit`)
|
||||
- `event.input` — tool parameters (mutable, can be patched)
|
||||
- `isToolCallEventType("read", event)` — type-narrows for specific tools
|
||||
@@ -46,15 +48,15 @@ pi.on("tool_call", async (event, ctx) => {
|
||||
|
||||
### Tools that can access sensitive files
|
||||
|
||||
| Tool | Danger | Input fields to check |
|
||||
|------|--------|----------------------|
|
||||
| `read` | **High** — reads file content into LLM context | `input.path` |
|
||||
| `bash` | **High** — can `cat`, `grep`, `curl` any file | `input.command` (regex match) |
|
||||
| `grep` | **High** — searches file contents | `input.path` |
|
||||
| `find` | **Low** — lists filenames only | `input.path` (directory listing) |
|
||||
| `ls` | **Low** — lists directory contents | `input.path` |
|
||||
| `write` | **Medium** — could overwrite sensitive files | `input.path` |
|
||||
| `edit` | **Medium** — could modify sensitive files | `input.path` |
|
||||
| Tool | Danger | Input fields to check |
|
||||
| ------- | ---------------------------------------------- | -------------------------------- |
|
||||
| `read` | **High** — reads file content into LLM context | `input.path` |
|
||||
| `bash` | **High** — can `cat`, `grep`, `curl` any file | `input.command` (regex match) |
|
||||
| `grep` | **High** — searches file contents | `input.path` |
|
||||
| `find` | **Low** — lists filenames only | `input.path` (directory listing) |
|
||||
| `ls` | **Low** — lists directory contents | `input.path` |
|
||||
| `write` | **Medium** — could overwrite sensitive files | `input.path` |
|
||||
| `edit` | **Medium** — could modify sensitive files | `input.path` |
|
||||
|
||||
### `tool_result` event (secondary approach)
|
||||
|
||||
@@ -85,6 +87,7 @@ pi.registerTool({
|
||||
**Description:** Single extension listening on `tool_call`, checking paths/commands against a configuration file of protected patterns. Blocks matching calls.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Create `.pi/extensions/sensitive-file-guard.ts` (or `index.ts` in a directory)
|
||||
2. Create `.pi/sensitive-paths.json` — declarative config listing protected patterns
|
||||
3. In `tool_call` handler:
|
||||
@@ -94,6 +97,7 @@ pi.registerTool({
|
||||
- Block if match, notify in UI
|
||||
|
||||
**Sample config file (`.pi/sensitive-paths.json`):**
|
||||
|
||||
```json
|
||||
{
|
||||
"blocked_paths": [
|
||||
@@ -109,16 +113,12 @@ pi.registerTool({
|
||||
".gnupg/",
|
||||
"config/*.secret.exs"
|
||||
],
|
||||
"blocked_commands": [
|
||||
"cat .env",
|
||||
"cat ~/.ssh",
|
||||
"printenv",
|
||||
"cat ~/.aws"
|
||||
]
|
||||
"blocked_commands": ["cat .env", "cat ~/.ssh", "printenv", "cat ~/.aws"]
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Minimal code (~60 lines of TypeScript)
|
||||
- Declarative configuration file (easy to audit and update)
|
||||
- Works with ALL tools (read, bash, grep, find, ls, write, edit)
|
||||
@@ -128,6 +128,7 @@ pi.registerTool({
|
||||
- Fails safe (blocks before any file access)
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Can't sanitize partial output (but blocking prevents all access)
|
||||
- Regex-based command scanning for bash has edge cases
|
||||
- `find`/`ls` only blocked at directory level, not per-file
|
||||
@@ -141,17 +142,20 @@ pi.registerTool({
|
||||
**Description:** Override the `read` and `bash` tools with custom implementations that include path checking. Delegates to original implementation for allowed paths.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. Create `.pi/extensions/sensitive-file-guard/` directory
|
||||
2. Override `read` tool: check paths, delegate allowed reads to original implementation
|
||||
3. Override `bash` tool: scan commands, delegate allowed commands to original implementation
|
||||
4. Optionally override `grep`, `find`, `ls`, `write`, `edit`
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Full control (can log, can sanitize output, can modify behavior)
|
||||
- No event overhead per tool call
|
||||
- Can add audit logging
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Must reimplement or re-wrap built-in tool logic (~200+ lines)
|
||||
- Must maintain compatibility with pi updates
|
||||
- More complex testing
|
||||
@@ -170,10 +174,12 @@ pi.registerTool({
|
||||
Same as Route A, plus a `tool_result` handler that scans output for sensitive patterns and redacts them.
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Most comprehensive protection
|
||||
- Can allow partial access with sanitization
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Most complex
|
||||
- `tool_result` sanitization is error-prone (false positives/negatives)
|
||||
- Redacted content still consumes context tokens
|
||||
@@ -195,14 +201,14 @@ Route A (pure `tool_call` event interception with declarative config) is the **s
|
||||
|
||||
## Architectural Impact
|
||||
|
||||
| Touchpoint | Impact |
|
||||
|------------|--------|
|
||||
| `.pi/extensions/` | New extension file added |
|
||||
| `.pi/sensitive-paths.json` | New config file (declarative rules) |
|
||||
| `tool_call` event | New subscriber, no breaking changes |
|
||||
| Built-in tools | Unchanged (event interception is non-invasive) |
|
||||
| `ctx.ui` | Used for notification only (no blocking dependency) |
|
||||
| Other extensions | Compatible — tool_call handlers chain in load order |
|
||||
| Touchpoint | Impact |
|
||||
| -------------------------- | --------------------------------------------------- |
|
||||
| `.pi/extensions/` | New extension file added |
|
||||
| `.pi/sensitive-paths.json` | New config file (declarative rules) |
|
||||
| `tool_call` event | New subscriber, no breaking changes |
|
||||
| Built-in tools | Unchanged (event interception is non-invasive) |
|
||||
| `ctx.ui` | Used for notification only (no blocking dependency) |
|
||||
| Other extensions | Compatible — tool_call handlers chain in load order |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
id: doc-3
|
||||
title: Force production logs to single-line format
|
||||
type: other
|
||||
created_date: '2026-05-03 13:53'
|
||||
updated_date: '2026-05-04 06:55'
|
||||
created_date: "2026-05-03 13:53"
|
||||
updated_date: "2026-05-04 06:55"
|
||||
---
|
||||
|
||||
# Research: Force production logs to single-line format
|
||||
|
||||
## Current state
|
||||
|
||||
**Config** (`config/config.exs`):
|
||||
|
||||
```elixir
|
||||
config :logger, :default_formatter,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
@@ -17,18 +19,19 @@ config :logger, :default_formatter,
|
||||
```
|
||||
|
||||
**Override** (`config/prod.exs`):
|
||||
|
||||
```elixir
|
||||
config :logger, level: :info
|
||||
```
|
||||
|
||||
**Log sources in prod**:
|
||||
|
||||
| Source | Produces | Problem |
|
||||
|---|---|---|
|
||||
| `Phoenix.Logger` (telemetry) | `GET /health` + `Sent 200 in Xms` | Two separate lines for one request — can't reverse |
|
||||
| `Phoenix.Logger` (telemetry) | `CONNECTED TO Phoenix.LiveView.Socket...` | One event with embedded `\n` — 4+ lines |
|
||||
| Custom `Logger.info/error/...` | Varies | Some may be multi-line |
|
||||
| OTP / stack traces | Crash reports | Multi-line |
|
||||
| Source | Produces | Problem |
|
||||
| ------------------------------ | ----------------------------------------- | -------------------------------------------------- |
|
||||
| `Phoenix.Logger` (telemetry) | `GET /health` + `Sent 200 in Xms` | Two separate lines for one request — can't reverse |
|
||||
| `Phoenix.Logger` (telemetry) | `CONNECTED TO Phoenix.LiveView.Socket...` | One event with embedded `\n` — 4+ lines |
|
||||
| Custom `Logger.info/error/...` | Varies | Some may be multi-line |
|
||||
| OTP / stack traces | Crash reports | Multi-line |
|
||||
|
||||
`Phoenix.Logger` is auto-attached by Phoenix.Endpoint and handles telemetry events including `[:phoenix, :endpoint, :start]`, `[:phoenix, :endpoint, :stop]`, and `[:phoenix, :socket_connected]`.
|
||||
|
||||
@@ -46,14 +49,14 @@ v2 (`~> 2.0.0-rc.5`) uses `Logster.attach_phoenix_logger()` in `application.ex`
|
||||
|
||||
### Architecture impact
|
||||
|
||||
| Touchpoint | Change |
|
||||
|---|---|
|
||||
| `config/prod.exs` | Add `config :phoenix, :logger, false` + Logster config + formatter config |
|
||||
| `lib/music_library/application.ex` | Add conditional `Logster.attach_phoenix_logger()` |
|
||||
| `lib/music_library_web/telemetry_log_handler.ex` | **New module** — handles socket_connected telemetry |
|
||||
| `lib/music_library/logger/single_line_formatter.ex` | **New module** — Logger.Formatter format function |
|
||||
| `mix.exs` | Add `{:logster, "~> 2.0.0-rc.5"}` |
|
||||
| Docs | Update `docs/production-infrastructure.md` with logging config |
|
||||
| Touchpoint | Change |
|
||||
| --------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `config/prod.exs` | Add `config :phoenix, :logger, false` + Logster config + formatter config |
|
||||
| `lib/music_library/application.ex` | Add conditional `Logster.attach_phoenix_logger()` |
|
||||
| `lib/music_library_web/telemetry_log_handler.ex` | **New module** — handles socket_connected telemetry |
|
||||
| `lib/music_library/logger/single_line_formatter.ex` | **New module** — Logger.Formatter format function |
|
||||
| `mix.exs` | Add `{:logster, "~> 2.0.0-rc.5"}` |
|
||||
| Docs | Update `docs/production-infrastructure.md` with logging config |
|
||||
|
||||
### Performance
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
id: doc-4
|
||||
title: GitHub Actions → Tangled Spindles
|
||||
type: other
|
||||
created_date: '2026-05-03 16:57'
|
||||
updated_date: '2026-05-04 06:54'
|
||||
created_date: "2026-05-03 16:57"
|
||||
updated_date: "2026-05-04 06:54"
|
||||
---
|
||||
|
||||
# CI Pipeline Porting Analysis: GitHub Actions → Tangled Spindles
|
||||
|
||||
## Current State (GitHub Actions)
|
||||
@@ -22,6 +23,7 @@ Key GHA features used: `jdx/mise-action`, `actions/cache`, concurrency control,
|
||||
Tangled provides **Spindles** — a CI runner service. Workflows are YAML files in `.tangled/workflows/`.
|
||||
|
||||
**Available features:**
|
||||
|
||||
- Triggers: `push`, `pull_request`, `manual` — with branch/tag glob filtering
|
||||
- Engine: `nixery` (uses Nixpkgs packages via Nixery at `nixery.tangled.sh`)
|
||||
- Dependencies: declared from nixpkgs (or custom registries)
|
||||
@@ -34,6 +36,7 @@ Tangled provides **Spindles** — a CI runner service. Workflows are YAML files
|
||||
- Timeout: configurable via `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT` (default 5m)
|
||||
|
||||
**Missing features (vs GitHub Actions):**
|
||||
|
||||
- ❌ No job dependencies (`needs`) — all steps in a workflow are strictly sequential
|
||||
- ❌ No parallel jobs — single linear pipeline per workflow file
|
||||
- ❌ No inter-run caching primitive — no equivalent to `actions/cache`
|
||||
@@ -51,20 +54,20 @@ Tangled also provides **Webhooks** (push events, HMAC-signed, 3 retries with bac
|
||||
|
||||
Nixery builds OCI images with each nixpkgs package in a **separate Docker layer**. Docker reuses pre-existing layers, so any nixpkgs dependency declared in the `dependencies` block — `mise`, `nodejs`, `shellcheck`, `hurl`, `sqlite` — is cached at the Docker level between runs.
|
||||
|
||||
This is confirmed by the Tangled team: *"caching for commonly used packages is free thanks to Docker (pre-existing layers get reused)."* — [Introducing Spindle blog post](https://blog.tangled.org/ci/)
|
||||
This is confirmed by the Tangled team: _"caching for commonly used packages is free thanks to Docker (pre-existing layers get reused)."_ — [Introducing Spindle blog post](https://blog.tangled.org/ci/)
|
||||
|
||||
### What is NOT cached (fetched/compiled fresh every run)
|
||||
|
||||
| Artifact | Reason |
|
||||
|----------|--------|
|
||||
| Elixir 1.20.0-rc.4, Erlang 28.5 | Not in nixpkgs; `mise install` downloads fresh each run |
|
||||
| `mix deps` + `_build` | No directory-level cache primitive in Spindle |
|
||||
| `npm` packages + asset builds | Same — no cache primitive |
|
||||
| Fluxon private Hex repo packages | Fetched each run |
|
||||
| Artifact | Reason |
|
||||
| -------------------------------- | ------------------------------------------------------- |
|
||||
| Elixir 1.20.0-rc.4, Erlang 28.5 | Not in nixpkgs; `mise install` downloads fresh each run |
|
||||
| `mix deps` + `_build` | No directory-level cache primitive in Spindle |
|
||||
| `npm` packages + asset builds | Same — no cache primitive |
|
||||
| Fluxon private Hex repo packages | Fetched each run |
|
||||
|
||||
### Within a run
|
||||
|
||||
Per the blog post: *"the `/tangled/workspace` and `/nix` volumes persisted across steps."* So within a single workflow run, compiled artifacts are available to subsequent steps. But between runs, everything resets except the Nixery Docker layers.
|
||||
Per the blog post: _"the `/tangled/workspace` and `/nix` volumes persisted across steps."_ So within a single workflow run, compiled artifacts are available to subsequent steps. But between runs, everything resets except the Nixery Docker layers.
|
||||
|
||||
### Potential caching strategy: Custom Nix derivation for Elixir 1.20.0-rc.4
|
||||
|
||||
@@ -74,20 +77,21 @@ Similarly, Erlang 28.5 could be pinned via a custom derivation if not in nixpkgs
|
||||
|
||||
## Dependency Availability in Nixpkgs
|
||||
|
||||
| Tool | In nixpkgs stable? | Version | Notes |
|
||||
|------|-------------------|---------|-------|
|
||||
| `elixir` | ✅ | 1.18.4 / 1.19.5 | **Not 1.20.0-rc.4** — must use mise or custom Nix derivation |
|
||||
| `erlang` | ✅ | via beam packages | OTP versions tied to package sets |
|
||||
| `nodejs` | ✅ | Yes | |
|
||||
| `mise` | ✅ | 2025.11.7 | Can install exact Elixir/Erlang/Node from mise.toml |
|
||||
| `shellcheck` | ✅ | Yes | |
|
||||
| `hurl` | ✅ (likely) | | |
|
||||
| `sqlite` | ✅ | Yes | |
|
||||
| `credo` | ❌ | Elixir mix task | Must install via mix after Elixir is available |
|
||||
| `sobelow` | ❌ | Elixir mix task | Same |
|
||||
| `mix_audit` | ❌ | Elixir mix task | Same |
|
||||
| Tool | In nixpkgs stable? | Version | Notes |
|
||||
| ------------ | ------------------ | ----------------- | ------------------------------------------------------------ |
|
||||
| `elixir` | ✅ | 1.18.4 / 1.19.5 | **Not 1.20.0-rc.4** — must use mise or custom Nix derivation |
|
||||
| `erlang` | ✅ | via beam packages | OTP versions tied to package sets |
|
||||
| `nodejs` | ✅ | Yes | |
|
||||
| `mise` | ✅ | 2025.11.7 | Can install exact Elixir/Erlang/Node from mise.toml |
|
||||
| `shellcheck` | ✅ | Yes | |
|
||||
| `hurl` | ✅ (likely) | | |
|
||||
| `sqlite` | ✅ | Yes | |
|
||||
| `credo` | ❌ | Elixir mix task | Must install via mix after Elixir is available |
|
||||
| `sobelow` | ❌ | Elixir mix task | Same |
|
||||
| `mix_audit` | ❌ | Elixir mix task | Same |
|
||||
|
||||
The workflow pattern with mise:
|
||||
|
||||
1. Add `mise` as a nixpkgs dependency (cached via Docker layers ✅)
|
||||
2. `mise install` to fetch Elixir 1.20.0-rc.4, Erlang 28.5, Node 25.9.0 (NOT cached ❌)
|
||||
3. Run existing mise tasks
|
||||
@@ -95,30 +99,35 @@ The workflow pattern with mise:
|
||||
## Implementation Routes
|
||||
|
||||
### Route A: Pure Spindle — Single Linear Workflow
|
||||
|
||||
One `.tangled/workflows/ci.yml` that runs lint → test → deploy sequentially.
|
||||
|
||||
**Pros:** Simplest — one file, fully self-contained on Tangled.
|
||||
**Cons:** Slowest (no parallelism between lint+test), deploy runs on every PR push to main, no separation of concerns.
|
||||
|
||||
### Route B: Multiple Spindle Workflows — Split CI/CD
|
||||
|
||||
Separate files: `lint.yml`, `test.yml`, `deploy.yml`. Deploy is manual-trigger only.
|
||||
|
||||
**Pros:** Lint and test can trigger on PRs, deploy is intentional. Better separation of concerns.
|
||||
**Cons:** No enforcement that test passes before manual deploy. Lint and test still run sequentially within each workflow.
|
||||
|
||||
### Route C: Spindle for CI + Webhook for Deploy
|
||||
|
||||
Spindle handles lint+test on PRs. A Tangled webhook on push to main triggers Coolify deployment via an external endpoint.
|
||||
|
||||
**Pros:** Deploy runs independently. Can add gating logic in webhook receiver.
|
||||
**Cons:** Requires a webhook receiver (external service). More infrastructure.
|
||||
|
||||
### Route D: Spindle for All — Lean Pipeline (Accept Gaps)
|
||||
|
||||
Full pipeline on Spindle but accept the missing features. Configure longer timeout, accept sequential runs, use manual deploy trigger.
|
||||
|
||||
**Pros:** Fully native Tangled experience.
|
||||
**Cons:** All gaps accepted as-is. May frustrate PR workflow.
|
||||
|
||||
### Route E: Mirror to GitHub + Keep Actions
|
||||
|
||||
Mirror the Tangled repo to GitHub (or use Tangled as a mirror of GitHub) and keep existing Actions.
|
||||
|
||||
**Pros:** Zero CI changes. All existing features preserved.
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
id: doc-5
|
||||
title: Log Line Copy in Prod-Logs Extension
|
||||
type: other
|
||||
created_date: '2026-05-03 21:06'
|
||||
updated_date: '2026-05-04 06:55'
|
||||
created_date: "2026-05-03 21:06"
|
||||
updated_date: "2026-05-04 06:55"
|
||||
---
|
||||
|
||||
# Implementation Analysis: Log Line Copy in Prod-Logs Extension
|
||||
|
||||
## Problem Summary
|
||||
@@ -28,13 +29,15 @@ The `/prod-logs` extension (`.pi/extensions/prod-logs/index.ts`) displays log li
|
||||
## Route A: Cursor + Toggle Selection + setEditorText
|
||||
|
||||
### Concept
|
||||
|
||||
Add a visual cursor line. Enter to copy single line. Space to toggle line selection (multi-select mode). `y` to copy all selected lines and exit. After exit, call `ctx.ui.setEditorText()` with the copied text.
|
||||
|
||||
### Changes to LogViewer
|
||||
|
||||
1. Add `cursorIndex: number` (absolute line index into `this.lines`)
|
||||
2. Add `selectedIndices: Set<number>` (absolute indices)
|
||||
3. New key bindings:
|
||||
- `Enter` → copy cursor line, `done(line)`
|
||||
- `Enter` → copy cursor line, `done(line)`
|
||||
- `Space` → toggle selection of cursor line
|
||||
- `y` → copy selected lines (or cursor line if none selected), `done(text)`
|
||||
4. Render changes:
|
||||
@@ -44,6 +47,7 @@ Add a visual cursor line. Enter to copy single line. Space to toggle line select
|
||||
5. Help text update: add "y copy · Space select · Enter copy line"
|
||||
|
||||
### Return value handling
|
||||
|
||||
```typescript
|
||||
const copiedText = await ctx.ui.custom<string | null>(...);
|
||||
if (copiedText !== null) {
|
||||
@@ -52,12 +56,14 @@ if (copiedText !== null) {
|
||||
```
|
||||
|
||||
### Pros
|
||||
|
||||
- No external dependencies
|
||||
- `setEditorText` is already available in pi's ExtensionAPI
|
||||
- Aligns with the "paste" use case (text in editor = ready to send or copy)
|
||||
- Simple, contained change to one file
|
||||
|
||||
### Cons
|
||||
|
||||
- Modifies editor content (replaces whatever the user had typed)
|
||||
- Not a true "system clipboard" copy — the user can't paste outside pi without an extra step
|
||||
- Toggle-based selection for multi-line is somewhat unusual UX (vs. range selection)
|
||||
@@ -67,9 +73,11 @@ if (copiedText !== null) {
|
||||
## Route B: Visual Mode (Vim-style) + setEditorText
|
||||
|
||||
### Concept
|
||||
|
||||
Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend the range, `y` copies and exits. Enter still copies single line. Selection is always a contiguous range.
|
||||
|
||||
### Changes to LogViewer
|
||||
|
||||
1. Add `cursorIndex: number` (absolute)
|
||||
2. Add `visualMode: boolean` + `visualAnchor: number` (start of visual selection)
|
||||
3. New key bindings:
|
||||
@@ -84,11 +92,13 @@ Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend t
|
||||
5. Help text: show visual mode help when active
|
||||
|
||||
### Pros
|
||||
|
||||
- Familiar UX for vim users
|
||||
- Contiguous range selection is intuitive
|
||||
- Same `setEditorText` approach — no external deps
|
||||
|
||||
### Cons
|
||||
|
||||
- Slightly more complex state machine (normal mode vs. visual mode)
|
||||
- Doesn't allow non-contiguous selection (but the user said "contiguous")
|
||||
- Still modifies editor content
|
||||
@@ -98,19 +108,23 @@ Vim-inspired visual mode: `v` enters linewise visual selection, `j`/`k` extend t
|
||||
## Route C: System Clipboard + Either Selection Model
|
||||
|
||||
### Concept
|
||||
|
||||
Either selection model (A or B), but instead of `setEditorText`, write to the system clipboard. Requires a package like `clipboardy`.
|
||||
|
||||
### Additional Changes
|
||||
|
||||
- Add `clipboardy` dependency (or use `node:child_process` with `pbcopy`/`xclip`)
|
||||
- On copy: `clipboardy.writeSync(text)` or similar
|
||||
- No need to change return type — can stay `void` since we don't pass text back
|
||||
|
||||
### Pros
|
||||
|
||||
- True "copy" behavior — paste anywhere
|
||||
- Doesn't touch editor content
|
||||
- Works across applications
|
||||
|
||||
### Cons
|
||||
|
||||
- External dependency (`clipboardy` — native module, may have install issues)
|
||||
- Platform-specific behavior (macOS `pbcopy`, Linux `xclip`/`wl-copy`, Windows)
|
||||
- pi extensions run in the pi process; clipboard access may have security implications
|
||||
@@ -121,6 +135,7 @@ Either selection model (A or B), but instead of `setEditorText`, write to the sy
|
||||
## Recommendation: Route B (Visual Mode + setEditorText)
|
||||
|
||||
**Rationale:**
|
||||
|
||||
1. **No external dependencies** — `setEditorText` is already available
|
||||
2. **Familiar UX** — vim-style visual mode is intuitive for developers
|
||||
3. **Contiguous selection** — matches the user's "contiguous lines" description
|
||||
@@ -136,6 +151,7 @@ Either selection model (A or B), but instead of `setEditorText`, write to the sy
|
||||
## Outstanding Question for User
|
||||
|
||||
How should the copied text be placed in the editor?
|
||||
|
||||
- **Append** to current editor content (preserves what's there)
|
||||
- **Replace** current editor content (clean slate)
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
id: doc-6
|
||||
title: Programmatic Access to Production Logs
|
||||
type: other
|
||||
created_date: '2026-05-04 06:43'
|
||||
updated_date: '2026-05-04 06:55'
|
||||
created_date: "2026-05-04 06:43"
|
||||
updated_date: "2026-05-04 06:55"
|
||||
---
|
||||
|
||||
# Implementation Route Analysis: Programmatic Access to Production Logs
|
||||
|
||||
## Decision: Route B — Pi Tool via `pi.registerTool()` ✓ SELECTED
|
||||
@@ -28,19 +29,23 @@ The LLM needs a tool it can call to retrieve production logs directly into its c
|
||||
## Route A: Tidewave MCP Tool (Elixir-side)
|
||||
|
||||
### Description
|
||||
|
||||
Add a new MCP tool `get_production_logs` to Tidewave that calls the Coolify API from Elixir using `Req`.
|
||||
|
||||
### Pros
|
||||
|
||||
- Architecturally consistent with existing MCP tools
|
||||
- Req is battle-tested in this codebase
|
||||
- Accessible by any MCP client
|
||||
|
||||
### Cons
|
||||
|
||||
- Must forward Coolify env vars to Phoenix app runtime
|
||||
- Rewrites the API call in Elixir (no code reuse from existing `fetchLogs`)
|
||||
- New Elixir module to maintain
|
||||
|
||||
### Verdict
|
||||
|
||||
**Rejected** in favor of Route B. The pi tool approach is simpler, reuses existing code and credentials directly, and keeps the change contained to a single file.
|
||||
|
||||
---
|
||||
@@ -48,9 +53,11 @@ Add a new MCP tool `get_production_logs` to Tidewave that calls the Coolify API
|
||||
## Route B: Pi Tool via `pi.registerTool()` ✓ SELECTED
|
||||
|
||||
### Description
|
||||
|
||||
Add a `fetch_production_logs` tool registration to the existing `prod-logs` extension. The tool is called by the LLM directly (just like `read`, `bash`, etc.), fetches logs via the existing `fetchLogs()` function, and returns text content to the LLM.
|
||||
|
||||
### Pros
|
||||
|
||||
- **Direct code reuse**: Same `fetchLogs()` function, same `resolveVar()` helper, same env vars
|
||||
- **No infrastructure changes**: No new Elixir modules, no credential forwarding, no config changes
|
||||
- **First-class LLM tool**: The LLM calls it natively during its agent loop, the result flows through the same context pipeline as any other tool result
|
||||
@@ -58,10 +65,12 @@ Add a `fetch_production_logs` tool registration to the existing `prod-logs` exte
|
||||
- **No external dependencies**: `typebox` is already available as a pi built-in import
|
||||
|
||||
### Cons
|
||||
|
||||
- pi-only (not accessible from other MCP clients — irrelevant for this use case)
|
||||
- Tool definition consumes a small amount of LLM context window space (same as any tool)
|
||||
|
||||
### Architecture Impact
|
||||
|
||||
- **Modified file**: `.pi/extensions/prod-logs/index.ts` (add tool registration, ~50 lines)
|
||||
- **No changes**: No Elixir modules, no config, no schemas, no routes, no UI
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
id: doc-7
|
||||
title: Pi access to production errors
|
||||
type: other
|
||||
created_date: '2026-05-04 08:07'
|
||||
updated_date: '2026-05-04 08:13'
|
||||
created_date: "2026-05-04 08:07"
|
||||
updated_date: "2026-05-04 08:13"
|
||||
---
|
||||
|
||||
# Implementation Routes: pi access to production errors
|
||||
|
||||
## Problem
|
||||
@@ -12,6 +13,7 @@ updated_date: '2026-05-04 08:13'
|
||||
The project captures production errors via the `error_tracker` Elixir dependency (stored in `MusicLibrary.TelemetryRepo` SQLite database). Errors can currently only be viewed through the built-in web dashboard at `/dev/errors` (behind login auth, only with `:monitoring_routes` enabled). There is no programmatic access — no API, no tooling, no pi extension.
|
||||
|
||||
The goal is to enable pi (the coding agent) to fetch and browse production errors. This requires three layers:
|
||||
|
||||
1. A programmatic API to expose error data (behind auth)
|
||||
2. Pi tools the LLM can call to fetch errors
|
||||
3. A pi extension for interactive browsing
|
||||
@@ -22,6 +24,7 @@ The goal is to enable pi (the coding agent) to fetch and browse production error
|
||||
|
||||
**How it works:**
|
||||
Add a new API controller (`MusicLibraryWeb.ErrorsController`) with two endpoints under `/api/v1/errors`:
|
||||
|
||||
- `GET /api/v1/errors` — list errors with optional filtering (status, muted, search) and pagination
|
||||
- `GET /api/v1/errors/:id` — single error detail with its occurrences and stacktraces
|
||||
|
||||
@@ -30,6 +33,7 @@ The controller queries the `error_tracker_errors` and `error_tracker_occurrences
|
||||
Pi tools (`fetch_production_errors` and `fetch_production_error`) make HTTP requests to this API using `fetch()` or `pi.exec("curl", ...)`. The pi extension builds on these tools for an interactive TUI.
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Follows existing patterns exactly (see `CollectionController`, `require_api_token`)
|
||||
- Clean separation of concerns: API layer, tool layer, extension layer
|
||||
- Pi tools work remotely — no need for SSH or filesystem access to the production server
|
||||
@@ -40,6 +44,7 @@ Pi tools (`fetch_production_errors` and `fetch_production_error`) make HTTP requ
|
||||
- The TelemetryRepo already exists and has the tables — no new database work
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Requires a server code change and deployment
|
||||
- Adds two new routes
|
||||
|
||||
@@ -62,11 +67,13 @@ Pi tools execute SQL queries directly against the TelemetryRepo SQLite database
|
||||
No server-side API changes needed. The pi tools would use `pi.exec("sqlite3", ...)` or read the database file directly.
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Zero server code changes
|
||||
- Immediate access to ALL data — no API shape limitations
|
||||
- Can run complex ad-hoc queries without API changes
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **Production access requires SSH or filesystem access** — violates the project's existing API-based pattern for pi access (cf. `fetch_production_logs` which uses Coolify API, not SSH)
|
||||
- No auth layer — tools have full read access to the entire database
|
||||
- Tightly couples pi tools to the error_tracker schema — any migration could break tools
|
||||
@@ -86,10 +93,12 @@ The direct-access approach introduces deployment friction (SSH key management, f
|
||||
Use the existing Tidewave MCP tools (`tidewave_execute_sql_query`) to query the error_tracker tables directly. This is available in dev but would need the Tidewave MCP server to be accessible in production (e.g., via SSH tunnel or a production-side MCP server).
|
||||
|
||||
**Pros:**
|
||||
|
||||
- No new code at all — uses what's already there
|
||||
- `tidewave_execute_sql_query` already understands the repo structure
|
||||
|
||||
**Cons:**
|
||||
|
||||
- **Tidewave MCP server does not run in production** — it's a development-only tool
|
||||
- Even if it did, running a dev tool against production is architecturally wrong
|
||||
- No browsing UX, no filtering, no pagination — basic SQL results only
|
||||
@@ -106,9 +115,11 @@ Tidewave is a development tool. Exposing it to production would be an architectu
|
||||
Add an endpoint to the existing Coolify-like API pattern used by `fetch_production_logs`. This would require Coolify to expose error_tracker data, which it doesn't natively support. Could potentially parse production logs for error patterns, but that's unstructured and duplicative.
|
||||
|
||||
**Pros:**
|
||||
|
||||
- Consistent with the existing `fetch_production_logs` pattern
|
||||
|
||||
**Cons:**
|
||||
|
||||
- Coolify doesn't expose error_tracker data
|
||||
- ErrorTracker already has structured data — parsing it from raw logs is backwards and lossy
|
||||
- Would require Coolify API changes or custom Coolify plugin
|
||||
|
||||
Reference in New Issue
Block a user