Releases: nikrich/poltergeist
v0.2.8 — stop the sidecar fork-bomb
Bug Fixes
- sidecar: call
multiprocessing.freeze_support()at the top ofghostbrain/api/__main__.pyso the PyInstaller bundle doesn't fork-bomb itself. The bundled ML stack (torch,transformers,sentence-transformers,joblib) touchesmultiprocessingat module load, and macOS defaults to thespawnstart method, which re-execssys.executable -B -S -I -c "from multiprocessing.resource_tracker import main; main(N)"to launch the resource_tracker helper. The PyInstaller bootloader silently ignores-c, so the "helper" was running a full second uvicorn + scheduler on a new random port — which then imported the same ML stack and spawned its own "helper", recursively. After a morning of restarts there were ~10 orphanedghostbrain-apiprocesses, each still firing scheduler jobs and shelling out toclaude -p, swamping CPU and RAM. PyInstaller's runtime hook installs a_freeze_supportthat detects the spawn argv andsys.exit()s in the helper — but only iffreeze_support()is actually called. Adding the call short-circuits the helper subprocess immediately and stops the runaway re-spawn. In dev (non-frozen) Python it's a no-op.
v0.2.7 — answer audit, transcript boost, sync debounce, sentinel cleanup
Features
- answer: audit-log every
/v1/answercall with the query text, source paths actually loaded, answer length, error string, and duration. When the user says "the vault isn't finding anything" we now have one log line per call instead of guessing phrasings — same fix shape as the Slack fetch_debug sentinel, applied to the chat path.
Bug Fixes
- scheduler: skip-if-already-running on
_invoke. The v0.2.6 fetch_debug log showed two concurrent slack syncs running in parallel (manual sync + cron loop firing on the same job), each scoring 8 batches of Slack messages throughclaude -pseparately. The second caller now returns a fast no-opskipped_reason="already_running"and the in-flight run keeps going. - search: path-prefix boost (+0.08) for notes under
*/transcripts/*. Pure cosine ranking was putting content-light calendar event notes above the actual transcripts, and on phrasings like "yesterday's workshop" the transcripts were falling out of the top-K entirely because the word "yesterday" lexically anchors to other notes that literally say "yesterday". With the boost, workshop transcripts move from #5/#6 to #1/#2 on the user's actual reported query.
Chores
- slack: strip the v0.2.3 + v0.2.4 diagnostic sentinels (
allowlist_debug.log,fetch_debug.log) now that v0.2.6 confirmed the SSL fix works end-to-end.
v0.2.6 — slack SSL cert verification in the bundled sidecar
Bug Fixes
- sidecar: point
SSL_CERT_FILEandREQUESTS_CA_BUNDLEatcertifi's bundledcacert.pemat sidecar startup. The v0.2.5 fetch sentinel surfaced the actual root cause behind the long-running "Slack syncs but queues nothing" problem:slack_sdk.auth_testwas failing with[SSL: CERTIFICATE_VERIFY_FAILED] unable to get local issuer certificate. PyInstaller-bundled Python has no system CA store; requests/httpx-based connectors (Gmail, Confluence, Jira, GitHub) shipcertifiinternally so they were fine, butslack_sdkuses stdliburllib, which callsssl.create_default_context()and dies withoutSSL_CERT_FILE. The catch-all infetch()ate the exception as alog.warningthat Electron then dropped, so the UI just kept showinglast_run_ok=true, queued=0. Promotingcertififrom transitive to direct dep so the bundle can't losecacert.pemto an upstream reshuffle. The fetch sentinel stays in place for this release so we can confirm in the packaged app thatauth_testnow passes.
v0.2.5 — slack fetch sentinel + transcript context cap
Bug Fixes
- answer: per-type context cap so meeting transcripts reach the LLM uncapped. The previous flat 16 KB
PER_NOTE_CHAR_CAPchopped the back half off a ~40 KB workshop transcript — the LLM was reading the first 40 minutes of a 90-minute session and the user would ask about something said at minute 70 and get "the sources don't cover this". Notes undertranscripts/now get a 48 KB budget; everything else keeps 16 KB. Worst-case two transcripts in the top-8 = 192 KB, inside Sonnet's 200 KB window.
Diagnostics
- slack: extend the file-based sentinel into
_fetch_workspace_full. v0.2.3 confirmed the allowlist resolves correctly in the bundled sidecar (11 channels) yetslack_cursors.<slug>.jsonis never advanced andqueued=0, which means an exception is firing inside the full-pull path and being swallowed by the catch-all infetch()as alog.warningthat never reaches disk. This release writes a one-line step trace (imports,load_token,client_factory,auth_test,_list_channels, channel loop,score_messages,cursors.save) plus the exception type + 8-frame traceback to~/.ghostbrain/state/slack.<slug>.fetch_debug.log. Removed in a later release once the failing step is identified.
v0.2.3 — diagnostic build for slack allowlist resolution
Diagnostic only
This release is a one-shot diagnostic to figure out why `_resolve_allowed_channels` returns an empty allowlist in the bundled v0.2.2 sidecar while the same code path with the same input files returns 11 channels in dev.
After installing, every slack sync writes one line to `~/.ghostbrain/state/slack..allowlist_debug.log` recording which source resolved (state file / env var / yaml), whether each file existed at the expected path, and the count from each. That'll tell us what the bundled runtime is actually doing.
v0.2.4 will revert the sentinel once the root cause is fixed.
v0.2.2 — slack allowlist via state file + safe full-pull fallback
Bug Fixes
- slack: full-pull allowlist now reads from `~/.ghostbrain/state/slack..allowed_channels.json` (JSON array of channel names) before falling back to env var or `routing.yaml`. The env-var route turned out to be unreliable on the packaged sidecar — the slack token from `.env` reached `os.environ` but the allowlist var from the same `.env` didn't, with no error logged. The state-file path is a plain JSON read that doesn't depend on env-var propagation through the launcher, and it lays the groundwork for an in-app channel picker.
- slack: when `mode: full` is configured without any resolvable allowlist, the connector now falls back to mentions-mode for that run with a log warning. Previously the connector silently iterated every channel, exhausted Slack's Tier 3 rate limit, swallowed every per-channel exception as a warning, and reported `last_run_ok=true, queued=0` indefinitely — no signal that anything was wrong.
Setup
After installing, drop a JSON array of channel names at `~/.ghostbrain/state/slack..allowed_channels.json`. Example:
```json
["general", "engineering", "product"]
```
Leading `#` is stripped and matching is case-insensitive. If you don't write this file, full-pull mode automatically downgrades to mentions-only — no setup required for that path.
v0.2.1 — fix mic entitlement (recordings were silent)
Bug Fixes
- build: add `com.apple.security.device.audio-input` entitlement (6ace5c1). v0.2.0 and earlier are signed with hardened-runtime but don't declare this entitlement, so macOS silently denies the bundled sidecar mic access. The recorder's ffmpeg child captured pure silence (-91 dB) on every take — whisper then produced empty transcripts and recordings showed up as "saved / done" with no body and no entry in the meetings list.
If you installed v0.2.0 and saw 0-byte transcripts or silent recordings, upgrade to v0.2.1 and re-record. After installing, macOS will prompt for microphone access on the first recording — accept it.
v0.2.0 — recorder stop, slack allowlist, joplin connector
Features
- connectors: add joplin connector (#8, 5288037)
- slack: `allowed_channels` whitelist for high-volume workspaces (#9, 8427ccc). Set via `SLACK_ALLOWED_CHANNELS_<UPPER_SLUG>` in `~/.ghostbrain/.env`, or via `allowed_channels:` in `routing.yaml`. Needed for workspaces with hundreds of channels — the previous full-pull burned through Slack's Tier 3 rate limit and silently returned zero events. New `.env.example` documents the env vars and the lookup order.
Bug Fixes
- recorder: unblock daemon-owned recordings (#9, a793033). Two compounding bugs — `scheduler_jobs.recorder_daemon` called a non-existent `state_mod.RecorderState.load` classmethod and raised `AttributeError` on startup (the daemon never ticked, so recordings ran past their scheduled_end indefinitely), and `repo/recorder.py:stop()` only inspected `manual.state` so the UI Stop button silently 409'd for calendar-driven recordings. Stop now SIGINTs the daemon's ffmpeg pid and the daemon's normal finalize path runs on the next tick.
Notes
- Cut manually because release-please skipped opening a release PR for this batch — its `include-paths` config is dropping `ghostbrain/`-only commits for reasons we haven't debugged yet. Tracked as a follow-up.
v0.1.17 — capture filter no longer gets confused after second click
Targeted fix for a real-world report: "the first chip click filters correctly, then subsequent clicks get confused and only filter some things".
Root cause
The captures query had both a 30s `staleTime` and a 30s `refetchInterval`, so a background refetch for the previous filter could land after a fresh click on a new filter, leaking the old source's items into the visible list. Filter chip + connectors panel + sidebar badge all share this query, so the cross-pollution showed up in multiple places.
What changed
- AbortSignal piped through to the IPC bridge. When React Query cancels an in-flight fetch (queryKey changed mid-flight, e.g. user clicked another chip), the promise now rejects with AbortError instead of resolving with the wrong data.
- `staleTime: 0` on captures. Every chip click triggers a guaranteed fresh fetch. Cache is still used to dedupe within a single render — just not across user-driven filter changes.
- `refetchInterval` removed from `useCaptures`. Captures naturally refresh on screen visits (component remount); background polling competed with active filtering.
Side effects worth noting
- The sidebar unread badge updates immediately on new captures instead of waiting up to 30s.
- No more spinner during background polling on the connectors tile.
🤖 Generated with Claude Code
v0.1.16 — surface recorder errors as a toast
Small but important fix: when the audio-routing gate (introduced in v0.1.14) refuses to start a recording — e.g. because macOS Output is set to AirPods/speakers and won't reach BlackHole — the renderer now surfaces the message as a toast instead of silently swallowing the rejection.
What changed
- api-forwarder unwraps FastAPI's
{"detail": "..."}envelope before passing the error to the renderer. Every API call site now sees the clean message instead ofHTTP 412: {"detail":"..."}. - meetings.tsx start handlers wrap the call in try/catch and push
toast.erroron failure. Both the calendar-event path and the manual-start path are covered.
Before vs after
Before (v0.1.15): click "start recording" with AirPods active → button click does nothing visible. Only the devtools console shows Uncaught (in promise) Error: HTTP 412: {"detail":"..."}.
After: same click → a red toast appears:
macOS audio output is 'Jannik's AirPods', which does not route system audio through BlackHole. Recording would only capture the microphone. Fix: open the Sound menubar item and switch the Output to 'Ghost Brain'...
🤖 Generated with Claude Code