Annotation-export curation loop, person merge service, and August sync - #346
Merged
Conversation
9999 is a popular default port for other tools, so the test stack's mock-streaming-stt kept colliding with unrelated services on dev hosts. 8879 is unclaimed by anything common. Port changed end-to-end (container, host publish, healthcheck, configs, robot keywords, verify script).
MongoDB returns the newest DeviceInputItem.captured_at as a naive datetime, so subtracting the 48h overlap window produced a naive bound that Immich read in an unintended zone — silently shifting which assets a scan sees. Normalize through _as_utc() before computing the window.
_effective_source_status() aged every online source out to offline once its last_seen_at went stale, which is right for ScreenPipe capture agents that heartbeat continuously. Chronicle polls Immich on a schedule instead, so its last_seen_at means last successful sync — and a healthy library kept flipping to Disconnected between scans. Exempt the immich provider from the staleness check (an explicit error status still wins), and label it in the Timeline as Connected/Disconnected with synced rather than seen.
Merging duplicate People notes was previously an LLM-mediated vault_tools path; the actual mutation now lives in a deterministic, previewable service that both the memory agent and external clients share. - person_merge.py: PersonMergeService — locked merge of metadata, facts, media, and backlinks, with a plan token so apply executes exactly the previewed plan (409 on stale state) and file-level rollback on failure. - person_identity.py: conservative duplicate-person suggestions from name/alias/context evidence, plus a symmetric distinct_from annotation in the People frontmatter that blocks future merge previews. - person_merge_actions.py + memory_routes.py: /api/memories/people/ suggestions, identity, merge/preview, and merge endpoints; preview and apply are separate steps and nothing merges automatically. - vault_tools.py: the agent rename-collision path delegates to the same PersonMergeService instead of its own migrate-and-delete logic. - audit: new obsidian_action cause (human_external actor) so explicit semantic actions are distinguishable from Syncthing sync edits. - agent/__init__.py: lazy exports so deterministic helpers can import section_edit without initializing the LLM agents.
A deliberately small Obsidian community plugin (extras/obsidian-chronicle) for explicit maintenance of a synced Chronicle vault. It runs no LLM and never mutates Markdown itself: it gathers intent plus local file hashes, previews the operation through the backend person-merge API, and submits the exact approved plan. Stale local state surfaces as the same HTTP 409 conflict the server uses for its own plan tokens. First action: Merge current person… from a People/ note, with a preview of fact/link counts and metadata conflicts before confirming. Docs: docs/obsidian-companion.md, linked from docs/README.md and the memory-system page.
Deepgram is called over its HTTP/WebSocket APIs directly; nothing imports deepgram-sdk anymore. Remove the extra from both backends' pyproject/locks and the --extra deepgram flags from every Dockerfile stage.
…octor check docs/backend/compose-stack.md becomes the canonical reference for the advanced backend stack; docker-compose.yml's long inline explanations are slimmed to pointers at its anchors. The functional half: pinning only public resolvers in x-public-dns broke every *.ts.net lookup inside containers — MagicDNS names are served solely by 100.100.100.100, so services addressed by tailnet name (remote ASR, Immich) failed with "Name or service not known" while public and container names kept working. List Tailscale's resolver first; public resolvers stay as fallbacks for tailnet-less hosts. services.py doctor grows check_container_magicdns — resolving this node's own MagicDNS name from inside a container — because check_container_dns passes on public upstreams while every tailnet name fails, so both probes are needed. Documented in podman.md alongside the aardvark caveat, with the asymmetric host-vs-container diagnosis recipe.
uv run --with-requirements caches the environment it builds and reuses it while the requirements text is unchanged, so a non-editable path dependency stays frozen at the sources it was first built from — edits to chronicle_setup/ were silently ignored by the wizard, every init.py, and services.py doctor, which kept running a stale wheel from ~/.cache/uv. Measured on uv 0.6.16, neither `uv cache clean` nor cache-keys invalidates it; only --reinstall-package does. An editable install links to the source tree instead. Rationale recorded in init-system.md with a probe command.
The codex executor shares one account-wide weekly budget with the user's interactive Codex sessions, and Chronicle's background recording is the cheaper consumer to give up: a yielded run still records the conversation via the direct (metered API) memory agent, while a blocked interactive session is stuck for days. - codex_quota.py reads account/rateLimits/read; memory.codex. max_used_percent (default 80) is the share Chronicle may consume before yielding, limit_id selects the metered bucket for non-default models. The probe fails OPEN — it optimises ahead of Codex's own limit error, so an unreadable quota must not stop memory extraction. - Token usage is summed from turn.completed events (the only place the CLI reports cost) into MemoryAgentResult.usage and emitted as a child codex_turn LLM span: current Langfuse drops usage from invoke_agent spans, so the parent span only mirrors it for filtering. - Quota snapshot attributes ride on the agent span for observability.
…lone Discovery self-heals a stale transcription:complete flag by re-attaching when the stream looks alive. Recency alone cannot answer that question: finalize_session flushes residual audio and appends the end marker as its last act, so at the exact moment the flag is set the newest entry is milliseconds old — a closing session is indistinguishable from a resuming one by age. Guessing wrong is expensive in one direction: clearing the flag revokes the handshake open_conversation_job waits on and no replacement ever arrives, stalling the conversation for the job's full 30s wait. _session_resumed now consults two causal facts first — the session must still be ACTIVE (the producer appends inside a WATCH/MULTI conditioned on active status, so a departed session can never write again) and the tail must not carry the end marker (appended strictly before the consumer can set the flag). Only then does recency answer the question it is actually good at: whether audio is flowing right now. Errors decline to re-attach, costing streaming transcription rather than corrupting the handshake.
The agent is the only component that sees host-level faults — dead container DNS, a logged-out Tailscale, a stale socket mount — so silently returning when it cannot be reached meant the exact failure mode the health poller exists to catch produced no signal at all. Record the reachable/unreachable transition as a system event (warning on loss, info resolution on return) keyed under one incident, like every other state this poller tracks, so a persistent outage does not spam the ledger.
observation_curation carried its own copy of the content-addressed _media promotion that vault_media now owns (person photos introduced the shared one). Delete the private copy and call the shared helper for both Immich originals and retained ScreenPipe frames.
Pasting a conversation ID from a log or system event into the search box now finds the conversation: an `id` category (over conversation_id) joins title/summary/speakers in the search endpoint, enabled by default and individually toggleable in the field dropdown. On the detail page the ID copy button gains a non-secure-context fallback (execCommand path for plain-HTTP LAN access, where navigator.clipboard is absent), visible copied/failed feedback, and an accessible name. Search debounce widens 300→800ms so regex queries don't fire per keystroke.
Replace the page's hand-rolled cards, badges, and stat tiles with the Espresso primitives (StatCard, StateBadge, MetadataChip, Alert, Label, IconButton), with dark-mode variants throughout. The dead priority filter is gone — the backend stubs every job's priority to "normal", so it could only ever match everything or nothing. Expanding a conversation adds to the dashboard query key, which used to make the whole page blank into its loading spinner as a cold query; keepPreviousData keeps the previous result rendered while the wider payload loads.
The screenpipe fork's CI publishes prebuilt recorder CLIs under the rolling chronicle-latest prerelease with a manifest carrying the fork commit and per-asset sha256s. recorder_update.py consumes it: download, sha256-verify, swap into ~/.local/lib/screenpipe-cli-chronicle/current/, repoint the screenpipe symlink, restart the service. The prior build stays in previous/, so revert is a directory swap. The tray gains Update recorder… / Revert recorder update actions (download runs on a worker thread; refresh() renders its state); headless nodes use python -m chronicle_tray.recorder_update check|install|revert. Documented in docs/screenpipe.md, including the macOS TCC re-prompt caveat for ad-hoc-signed builds.
- AGENTS.md: Immich integration in the wizard feature list, service- profile testing (make test PROFILE=..., cassettes, no credential-gated tests), and the compose-stack.md pointer. - memories.md: document the person_photos cron job that embeds Immich face-crop thumbnails into People notes via the vault's _media store.
The template hardcoded host.docker.internal:8123, which is wrong the
moment Home Assistant lives on another node; reference ${HA_URL} with
the old value as the default.
The export flow could select and export, but never showed what would actually be in the zip — the user shipped datasets to annotators sight unseen and had no record of what was already sent. Three additions close the curation loop: - Contents preview: POST /export/preview is a synchronous dry-run returning the exact clips (boundaries, durations, sliced transcripts) the current settings would produce. The plan computation moved into utils/export_planning.plan_conversation_clips, shared verbatim by the export job, so the preview cannot drift from the export. Unanalyzed conversations are reported skipped rather than VAD'd inline. - Clip-level curation: the modal's Dataset contents panel renders the plan live (auto-refreshing as params/screen change) with per-clip playback via the gapless player, the transcript slice (a "no transcript" badge marks clips an annotator would receive silent), and an include toggle. Unticked clips go to /export as dropped_ranges — same carving as privacy ranges, separate accounting (params.curated, dropped_seconds): "not worth annotating" vs "too sensitive to share". - Export history: the listing joins the on-disk export.json metadata so each row carries last_export (an "exported" chip) and an exported=never|exported filter scopes curation to un-shipped audio. Deleting an export naturally un-marks its conversations. Verified end-to-end on live data: 19-clip preview, 1 clip dropped, export produced 18 clips with dropped_seconds attributed to the right conversation, badges appeared, delete un-marked them.
Contributor
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Batches the work from both machines (kraken + rainbow, synced to a common tip) plus the new annotation-export curation loop.
Annotation export: verify and curate before shipping (9c8ef71)
The export flow could select and export, but never showed what would actually be in the zip — datasets went to annotators sight unseen, with no record of what was already sent.
POST /api/data-audit/export/previewis a synchronous dry-run returning the exact clips (boundaries, durations, sliced transcripts) the current settings would produce. Plan computation lives inutils/export_planning.plan_conversation_clips, shared verbatim by the export job, so the preview cannot drift from the export.no transcriptbadge for clips an annotator would receive silent), and include toggles. Unticked clips are carved out viadropped_ranges, accounted separately from privacy withholdings (params.curated,dropped_seconds).export.jsonmetadata: rows carrylast_export(anexportedchip) and anexported=never|exportedfilter scopes curation to un-shipped audio. Deleting an export un-marks its conversations.Verified end-to-end on live data: 19-clip preview → 1 dropped → 18-clip zip with correct accounting; badges appear and clear on delete.
Memory: deterministic person identity + merge, Obsidian companion
PersonMergeService: locked, previewable merge of People notes (metadata/facts/media/backlinks) with plan tokens (409 on stale state) and file-level rollback; the memory agent's rename-collision path now delegates to it.distinct_fromdecisions; new/api/memories/people/*endpoints.extras/obsidian-chronicle: a small Obsidian plugin that previews and submits these merges from a synced vault — no LLM in the execution path.Fixes
100.100.100.100first inx-public-dns— public-only resolvers broke every*.ts.netlookup inside containers; newcheck_container_magicdnsdoctor check.chronicle-setupeditable — uv's env cache silently pinned a stale wheel, ignoring source edits.max_used_percent) by falling back to the direct agent, plus token-usage telemetry.Docs / chores
docs/backend/compose-stack.mdas the canonical compose reference; compose comments slimmed to pointers.HA_URLtemplate override.Tests
test_export_planning.py(12),test_person_merge.py/test_person_identity.py(13), Immich/device-input regressions (9).