Skip to content

Releases: nikrich/poltergeist

v0.2.8 — stop the sidecar fork-bomb

21 May 06:59

Choose a tag to compare

Bug Fixes

  • sidecar: call multiprocessing.freeze_support() at the top of ghostbrain/api/__main__.py so the PyInstaller bundle doesn't fork-bomb itself. The bundled ML stack (torch, transformers, sentence-transformers, joblib) touches multiprocessing at module load, and macOS defaults to the spawn start method, which re-execs sys.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 orphaned ghostbrain-api processes, each still firing scheduler jobs and shelling out to claude -p, swamping CPU and RAM. PyInstaller's runtime hook installs a _freeze_support that detects the spawn argv and sys.exit()s in the helper — but only if freeze_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

20 May 08:40

Choose a tag to compare

Features

  • answer: audit-log every /v1/answer call 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 through claude -p separately. The second caller now returns a fast no-op skipped_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

20 May 06:25

Choose a tag to compare

Bug Fixes

  • sidecar: point SSL_CERT_FILE and REQUESTS_CA_BUNDLE at certifi's bundled cacert.pem at 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_test was 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) ship certifi internally so they were fine, but slack_sdk uses stdlib urllib, which calls ssl.create_default_context() and dies without SSL_CERT_FILE. The catch-all in fetch() ate the exception as a log.warning that Electron then dropped, so the UI just kept showing last_run_ok=true, queued=0. Promoting certifi from transitive to direct dep so the bundle can't lose cacert.pem to an upstream reshuffle. The fetch sentinel stays in place for this release so we can confirm in the packaged app that auth_test now passes.

v0.2.5 — slack fetch sentinel + transcript context cap

19 May 11:39

Choose a tag to compare

Bug Fixes

  • answer: per-type context cap so meeting transcripts reach the LLM uncapped. The previous flat 16 KB PER_NOTE_CHAR_CAP chopped 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 under transcripts/ 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) yet slack_cursors.<slug>.json is never advanced and queued=0, which means an exception is firing inside the full-pull path and being swallowed by the catch-all in fetch() as a log.warning that 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

18 May 12:01

Choose a tag to compare

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

18 May 09:45

Choose a tag to compare

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)

18 May 09:03

Choose a tag to compare

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

18 May 07:45

Choose a tag to compare

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

14 May 11:53

Choose a tag to compare

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

14 May 09:56

Choose a tag to compare

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 of HTTP 412: {"detail":"..."}.
  • meetings.tsx start handlers wrap the call in try/catch and push toast.error on 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