feat: Slack workspace archiving (user-token sync + search-based reply sweep) - #494
Conversation
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
bef7524 to
6deea57
Compare
roborev: Combined Review (
|
roborev: Combined Review (
|
roborev: Combined Review (
|
|
looking |
Design for a Slack importer following the Teams (kenn-io#398) and Beeper (kenn-io#458) patterns: internal/slack package, user-token Web API sync via a user-created internal app, existing chat schema with no new core tables, per-conversation ts cursors in sync_runs.cursor_after, files.slack.com-only media fetches, and explicit load-bearing items to validate live. - docs: record Slack ingestion load-bearing probe results - feat: Slack workspace ingestion (user-token Web API sync) - test+docs: Slack ingestion fake-server suite and user docs - fix: skip gone Slack conversations instead of failing the run - test: CLI-level Slack coverage, private channels, and --limit semantics - chore: resolve golangci-lint findings in the slack packages - feat: index legacy bot payloads (attachments/blocks) for search - test: env-gated live throttle probe for 429/Retry-After handling - chore: enforce the client's method allowlist at runtime - docs: record LB-2 verdict — full history depth verified live - fix: address review findings on incremental sync correctness - fix: address second-round review findings - fix: cursor-merge atomicity, scoped-run rescan, deferred-media markers - feat: replace thread-lookback polling with a search-based reply sweep - Merge origin/main (Discord importer) into feat/slack-ingestion-v2 - fix: address roborev review of the reply sweep - test: adopt local testify helpers in remaining slack tests - fix: address roborev round-2 findings on the reply sweep - fix: address roborev round-3 findings (link-row preservation, DST day boundaries) - fix: address roborev round-4 findings — limited runs converge on every path - fix: address roborev round-5 findings — pins, overlap floors, repair sessions - fix: address roborev round-6 findings — every reply fetch is drain debt - fix: address roborev round-7 finding — store failures are fatal, not counters - fix: address roborev round-8 findings — delete state merging entirely - fix: three holes found by adversarial scenario analysis of the defect classes - fix: reply-ts anchors serve only themselves — drain re-anchors via thread_ts - fix: address roborev round-9 findings — unconditional no-threads debt, fatal checkpoints - fix: address roborev round-10 findings — debt seniority, self-stamping walks, reply maintenance - fix: address roborev round-11 findings — tombstone preservation, coverage-floor merges - fix: truncated sweep days convert to catch-up debt instead of parking forever - fix: address roborev round-13 findings — consumption-based sweep certification, backfill media contract - fix: address roborev round-14 findings — stable sweep pagination snapshot, interrupted sync exit - fix: address roborev round-15 findings — archive-driven maintenance thread selection, media download timeout
Tombstones preserve archived originals by skipping completed snapshots, but message persistence spans several fatal auxiliary writes. Treating a message row alone as completion made an interrupted placeholder permanently immune to repair. Use mandatory raw JSON as the final completion marker for both tombstone and thread-parent skips. Completion-probe failures now abort and hold thread debt instead of refreshing an archived parent on uncertainty. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
Fail closed when durable Slack resume state cannot be read or decoded, expose the Slack daemon job through the shared source scheduler mapping, and align Slack message lifecycle data with the existing chat importers. Slack tombstones now preserve archived content while marking messages deleted, live reappearances clear stale deletion markers, and sender rows are persisted through the shared from-recipient model. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
Retain terminal metadata for source-removed files, treat missing archived raw as a repairable media item, and reject contradictory Slack pagination instead of certifying incomplete walks. Add rotating canonical thread audits so unbounded search-index lag cannot permanently omit replies. Align per-run add/update metrics and direct-chat recipient snapshots with the shared Teams and Discord data model, and update the Slack documentation to match. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
Continue history and reply pagination whenever Slack returns a next_cursor, even if the legacy has_more flag is false. Reject repeated continuation tokens so malformed provider responses park progress instead of looping or certifying an incomplete walk. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
5d1e4a7 to
737961b
Compare
roborev: Combined Review (
|
Direct-chat recipient snapshots and owner identity evidence must survive every ingestion path. Late reply sweeps previously dropped DM/MPIM recipients, while MPIM history could advance through a membership outage and become ineligible for repair. Carry the resolved membership snapshot into sweep drains, hold MPIM progress until transient membership failures recover, and confirm Slack's namespaced workspace/user identifier so relationship analytics can resolve the owner participant. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
Slack media repair mutates attachment metadata that feeds the Parquet analytics cache. Returning before a refresh left file counts and sizes stale, especially when useful work preceded a workspace failure or cancellation. Make cache publication part of every completed backfill attempt and preserve both operation and refresh failures so callers can act on the full outcome. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
Configuration edits must reject invalid cron expressions before atomic replacement. Slack was missing from the shared schedule validation set, allowing the daemon to accept a config it could not schedule and silently leave workspace synchronization inactive. Keep Slack on the same validation path as the other scheduled ingestion sources so invalid edits preserve the last runnable configuration. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
A truncated search day is intentionally converted into canonical thread debt, but the ten-minute sweep overlap queried that same day again. Under a standing limit of one, the retry consumed the entire sweep budget and restarted the catch-up cursor forever, so neither mechanism could advance. Persist the day boundary each affected conversation must cover, preserve cursor-valid walks already in flight, and treat converted overlap days as budget-free certification work. A later-pinned follow-up remains queued when needed, retaining completeness for arrivals through the truncated day. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
The no-threads option promises that a run will not traverse replies, but maintenance bypassed every existing guard and could page every recent thread without a limit. This made the combined flags unexpectedly expensive and violated the operator's explicit fetch constraint. Keep top-level edit and reaction repair active while suppressing both archive-selected and history-discovered thread rescans, so the flags remain useful together without weakening no-threads semantics. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
Reply-sweep certification depends on associating every search hit with its conversation. The observed search.messages payload exposes that identity as top-level channel_id, while the decoder and fake server agreed on a nested shape that left production matches unowned and silently discarded. Prefer the observed field, retain nested compatibility, and pin both the fake and live probe so channel identity cannot disappear while the sweep watermark continues advancing. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
Durable Slack pagination state and hosted-file downloads can both become invalid after they are checkpointed. Treating those failures as generic fetch errors made scheduled syncs retry impossible work forever without advancing or repairing the archive. Restart expired opaque cursors inside their original timestamp pins, and preserve permanently missing file metadata while terminating only 404/410 download debt. This keeps coverage bounds stable and archive provenance available without hiding transient failures. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
Full synchronization is the documented escape hatch for repairing Slack archives, but resume decoding ran before the repair reset and could permanently block that path on a malformed checkpoint. Classify invalid durable blobs separately from store access failures so full repair can safely start fresh without weakening the normal fail-closed behavior or concealing database faults. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
JSON decoding accepts null values for pointer-valued conversation state, but resume normalization assumes every map entry is concrete. A crafted or damaged checkpoint could therefore panic before the full-repair fallback had an error to classify. Validate the decoded state before normalization so malformed entries follow the same recoverable invalid-state path as other corrupt checkpoints. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
Slack workspace and profile names are untrusted provider data. Rendering them directly allowed ANSI and OSC controls to alter terminal state, spoof progress, or target terminal features such as clipboard handling. Sanitize only at the CLI writer boundary so interactive output is safe while archived participant names and source display metadata retain their original values. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
|
This appears to be good now, running CI checks and will merge |
Recent Slack regression coverage passed SQLite locally but missed repository lint conventions and PostgreSQL placeholder syntax. That left otherwise-correct repair behavior blocked behind test-harness failures in the required CI lanes. Keep the assertions compatible with the testify checks, preserve wrapped resume-state causes, and route direct SQL through the store dialect so both supported backends exercise the intended regressions. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
|
thank you! Will release soon. Let me know if you encounter any problems syncing from main |
Archive your own view of a Slack workspace — public/private channels you're a member of, group DMs, and 1:1 DMs — via the Web API, searchable alongside mail and other chat sources through the existing TUI / FTS / Parquet analytics.
Supersedes #489. Since that PR opened, live probing established that Slack's search API reliably returns thread replies by creation time (
threads:replies, day-granular bounds, stable ascending pagination) — enabling a simpler and more correct reply-capture architecture than the lookback-window thread polling reviewed there. Design docs:docs/internal/slack-ingestion-design.md(base) anddocs/internal/slack-reply-sweep-design.md(reply sweep, with the full probe ledger).Usage
Each workspace membership is its own source (
slack,<team-id>:<user-id>); multiple workspaces coexist and stay separately filterable in the TUI. A[slack]config block covers daemon scheduling, channel include/exclude filters, and a media size cap.How replies are captured
search.messages threads:repliesquery over the days since a per-source UTC watermark discovers replies by creation time — a reply to a thread of any age is found (no tracking window, no blind spot). Hits are pointers only; archiving always goes through canonicalconversations.repliesfetches into the standard upsert path.sync-slack --maintenancerepairs the recent window,--fullrepairs everything. A backfill run under--no-threadsleaves a recorded debt that the next threaded run pays automatically.What's stored
Message text with FTS (mrkdwn rendered, mentions resolved to names, bot
attachments/blockspayloads extracted), threads (root+reply viareply_to_message_id), reactions, @mention recipient rows, conversation membership, files in the content-addressed attachment store, and the verbatim API JSON per message (raw_format = slack_json). Members' profile emails unify their Slack identity with their archived mail. Reuses the existing chat schema — no new core tables.Safety and correctness properties
files.slack.com(no redirects); external/off-host files are metadata + permalink; failed/deferred downloads (incl.--no-media) stay discoverable bybackfill-slack-media.Store/query changes
Slack attachment-row methods share prefix-parameterized helpers with the Beeper ones;
"slack"registered inKnownMessageTypes/TextMessageTypes.Validated against live workspaces: full backfill (1,100+ message channel), interrupt/resume, threads incl. cross-run late replies, reactions (incl. custom emoji), media into CAS packs, a real 429 absorbed via Retry-After, history depth past 8 years, and the sweep's search behaviors (replies-only modifier, unjoined-channel leakage → filtered, tz re-filing, query caching, page clamping — each with an env-gated live pin-test or fake-server regression test).
Planned follow-ups (separate PRs): opt-in archiving of unjoined public channels via an explicit config allowlist; concurrent fetch/persist pipeline.