Skip to content

fix: triage pass — cross-workspace collisions, attachment loss, and text width - #131

Merged
steipete merged 11 commits into
mainfrom
steipete/slacrawl-triage-bugs-f22380
Aug 7, 2026
Merged

fix: triage pass — cross-workspace collisions, attachment loss, and text width#131
steipete merged 11 commits into
mainfrom
steipete/slacrawl-triage-bugs-f22380

Conversation

@steipete

@steipete steipete commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Triage pass over main. Eight confirmed defects, each with a regression test that fails without the fix.

Sync: cross-workspace collisions (completes #130)

#130 demoted cross-workspace channel collisions to a skip, but two other boundaries stayed fatal:

  • A shared user aborted the whole sync. Sync's user loop called UpsertUser bare. Enterprise Grid user IDs are org-wide and Slack Connect surfaces external members, so a second workspace's sync died after channels, DMs and every message had committed but before the checkpoint was written — the run was recorded as never having happened.
  • A shared message killed tail. The upsert error propagated out of HandleEventsAPIEvent through handleSocketModeEvent to Tail, terminating the socket-mode loop. fix: skip cross-workspace channel collisions during bot sync #130 made this more likely, since the shared channel now permanently stays owned by the first workspace.

Both are now skip-with-warning, matching the desktop importer. The history and thread upserts on the same path are covered too.

Media: silent attachment loss

  • MaxBytes+1 overflowed. math.MaxInt64 is the natural "no cap" value; incrementing it for the over-limit probe wrapped negative, io.LimitReader returned EOF immediately, and every attachment was written as a successfully fetched empty file — sha256(""), size 0, status fetched, no error anywhere.
  • A symlinked media root wedged purge permanently. Parking a large attachment cache on another volume behind a symlink is ordinary, and files fetch has always written through one — but every read path (list, remove, share) rejected it. purge --force committed the database transaction and then failed post-commit cleanup on every subsequent run. The root is now resolved once; symlinks inside the tree are still refused, and the existing escape test still passes.
  • publish could emit a media-less manifest. errUnsafeMediaPath was treated as "file absent", so after the repo's media/ had already been wiped, every row was skipped and the manifest simply claimed the archive has no attachments — silently losing the entire media set for subscribers on their next --media import.
  • A dead error check in clearUnmanifestedImportedFileMedia swallowed every non-ErrNoRows query error.

CLI: byte lengths used where display width was meant

  • trimTo sliced bytes, so search/messages output cut multi-byte runes in half and emitted invalid UTF-8; CJK text was also truncated to roughly a third of the intended width.
  • renderTable measured column widths on the colorized cell. formatScalar wraps empty, nil and bool values in ANSI codes, so those invisible bytes padded the column while the header used its plain length — every table with an empty or non-ASCII cell was misaligned.
  • Fetch errors were clamped at 512 bytes mid-rune, and fully non-Latin filenames (写真.png) lost their extension entirely, so a published media/ tree served them as application/octet-stream.

Proof

go build, go test ./..., go test -race ./..., go vet, govulncheck (no vulnerabilities), make lint, make smoke, make snapshot, make tidy-check fmt-check — all clean. Codex autoreview: clean, no accepted findings.

Each fix was verified to fail before the change: the two sync tests, both render tests, and the media tests were run against the reverted logic and observed failing.

One existing test changed rather than being deleted: TestPurgeCommandReportsPostCommitCleanupFailure used a symlinked media root as its failure trigger, which is exactly the wedge being fixed. It now points at a genuinely broken cache (a root symlinked to a regular file) and still asserts that post-commit cleanup failure is reported.

go-runewidth moves from indirect to direct; it was already in the module graph via bubbletea, same version.

The #130 fix covered channels but left two boundaries fatal. A user shared
across workspaces (Enterprise Grid IDs are org-wide, and Slack Connect
surfaces external members) aborted Sync in the user loop, after channels,
DMs and every message had already committed but before the checkpoint was
written, so the run was recorded as never having happened. A message in a
shared channel owned by another workspace propagated out of
HandleEventsAPIEvent and terminated the socket-mode loop, killing tail.

Demote both to skip-with-warning, matching the desktop importer, and cover
the history and thread upserts on the same path.
Three defects in the attachment path:

- readLimit: MaxBytes+1 overflowed to negative for math.MaxInt64, the
  natural way to ask for no cap. io.LimitReader then returned EOF at once,
  so every file was stored as a successfully fetched empty file.
- A symlinked media root is a supported setup for a large cache on another
  volume, and fetch always wrote through one, but every read path rejected
  it, so purge and publish failed permanently. Resolve the root once and
  keep refusing symlinks inside the tree.
- publish treated an unsafe media path as an absent file and could emit a
  manifest claiming no media at all, wiping attachments for subscribers.

Also clamp fetch errors on a rune boundary and keep the extension for
fully non-Latin filenames, which safeFilename strips to bare 'png'.
trimTo sliced bytes, so a message containing emoji or CJK was cut mid-rune
into invalid UTF-8 and wide text was truncated far earlier than the limit
implies. renderTable measured widths on the already-colorized cell, and
formatScalar wraps empty, nil and bool values in ANSI codes, so those
invisible bytes padded the column while the header used its plain length.

Promote go-runewidth to a direct dependency; it was already in the module
graph via bubbletea.
@steipete
steipete requested a review from a team as a code owner August 6, 2026 17:15
The tombstone-propagation SQL existed in both the v6 migration and the
share import path and had already drifted: the migration overwrote
deletion_reason unconditionally while the import preserved an existing
value. store.BackfillDeletedSubordinates now owns the statement with the
preserving semantics and both callers use it.

media.LocalPath/RepoPath were byte-identical twins, and share duplicated
the symlink-rejecting containment walk that media.cachedRegularFile
implements. One containedJoin plus an exported media.VerifiedFile now
carry both invariants; the share-local copy and its error sentinel are
gone. Also drop the dead schemaPragmas const, a dead store flagged by
staticcheck, and stamp Docker builds with a real version.
ExtractMentions html-unescaped the whole message before matching, so a
user literally typing <@u123> — which Slack transmits as &lt;@u123&gt; —
became indistinguishable from a real mention and landed in
message_mentions across every ingest path. normalizeMessageText already
parses tokens before entity decoding (ac6d350) for exactly this reason;
mentions were left behind. Match on the escaped text and unescape only
the display label. Flips a test that asserted the buggy behavior.
ChannelThreadRoots' reply-existence probe had no index on thread_ts, so
it scanned the whole channel per message — O(channel-size squared),
measured at minutes-to-hours for a 50k-message channel and 0.04s with
the index. message_events had no (channel_id, ts) index either, making
per-message event deletes a full scan of the event log. Schema v7 adds
both; migration verified against a copy of a real v6 archive.

replaceUserMentions recompiled the same per-target regex for every
rendered row; the compiled patterns are now cached.
The hourly repair sweep made plain HTTP calls whose failures were fatal:
one network blip during the tick killed a long-running tail daemon with
a nonzero exit. Repair failures now warn and retry next interval; store
errors still surface through the event-handler path.

The socket-mode goroutine ran slack-go's Run(), which is
RunContext(context.TODO()) — the websocket never observed Tail's
context and outlived it. The runner interface now takes ctx.
… cap

- main now installs signal.NotifyContext: Ctrl-C/SIGTERM cancel the
  context so sync/tail/watch unwind through their cleanup paths instead
  of being hard-killed; all the existing ctx plumbing was unreachable.
- runTailTargets raced a buffered error against the canceled context in
  one select; a scratch -race harness showed 14% of failures reported
  'context canceled' instead of the real workspace error. The error
  channel is now drained with priority.
- sync --since documents 'slack ts or RFC3339' but only the MCP backend
  normalized RFC3339; the API path passed the raw string to Slack's
  oldest param. Normalized once at the CLI boundary, with a clear error
  for junk values.
- analytics trends --weeks materializes one bucket per week per channel
  row, so an absurd count was an OOM kill; capped at 520.
mcpclient called cmd.Wait concurrently with the stdout read loop; Wait
closes the pipe, so a server that writes its final response and exits
immediately could have that response turned into a decode error. Wait
now runs after the reader drains, with a bounded post-kill timeout so a
grandchild holding the pipe cannot hang Close.

The provider request write also moved to a goroutine — a request larger
than a pipe buffer against a provider that emits early output could
deadlock both processes — and a provider that dies before reading its
request now gets its captured stderr attached to the error instead of a
bare EPIPE.
@steipete

steipete commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed a second wave from a full-codebase deep review (4 parallel review passes: bugs, performance, duplication, interactive/live surfaces — every finding verified before fixing):

Bugs

  • ExtractMentions unescaped entities before matching, so literally quoted &lt;@U123&gt; text was recorded as a real mention across all five ingest paths. Now matches escaped text and unescapes only labels, mirroring normalizeMessageText (whose sibling fix ac6d350 had left mentions behind).
  • No signal handling existed anywhere — every graceful-cancellation path was unreachable; main now uses signal.NotifyContext.
  • Multi-workspace tail returned bare context canceled instead of the real workspace error in ~14% of failures (measured with a -race harness); the error channel now drains with priority.
  • tail died permanently on one transient network failure during its hourly repair sweep, and its websocket ignored the context (slack-go Run() is RunContext(TODO)).
  • analytics trends --weeks 2000000000 was an OOM kill; capped at 520.
  • sync --since RFC3339 support existed only on the MCP backend despite the flag documenting it for all; normalized at the CLI boundary.
  • mcpclient raced cmd.Wait()'s pipe-close against the stdout reader, turning a final response from a fast-exiting server into a decode error; provider subprocesses that died at startup lost their stderr diagnostics and large requests could deadlock against early output.

Performance

  • Schema v7: messages(channel_id, thread_ts) index — the thread-root probe was O(channel²); benchmarked >120s → 0.038s on a 50k-message channel. Also message_events(channel_id, ts) (per-delete full scans) and a per-target regex cache in the render path. Migration verified against a copy of a real v6 archive.

Dedupe

  • Tombstone-propagation SQL existed in both the v6 migration and share import and had already drifted (deletion_reason semantics); store.BackfillDeletedSubordinates now owns it with the preserving semantics.
  • The media containment invariant (lexical join + symlink-rejecting walk) had three implementations; now one containedJoin + media.VerifiedFile.
  • slack.User → store.User mapping deduped into slackapi.ToStoreUser.

Proof: full suite, -race on the six touched packages, staticcheck (clean except proper-noun error-string style), go vet, govulncheck, make lint, migration tested against a real archive copy, Codex autoreview clean on the full branch diff.

Known follow-ups deliberately not in this PR: batching the per-message transactions in the non-provider ingest paths (~3.4x measured headroom), the double-pass point queries in import, the ChannelSyncCursors CASE-max scan on tail repair ticks, dead sync.full_history config option, and two upstream crawlkit TUI bugs (q quits while typing in the filter; byte-wise backspace).

@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal priority bug or improvement with limited blast radius. labels Aug 6, 2026
@clawsweeper

clawsweeper Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codex review: needs changes before merge. Reviewed August 6, 2026, 7:38 PM ET / 23:38 UTC.

ClawSweeper review

What this changes

The PR fixes multi-workspace Slack sync collisions, media-cache data loss paths, Unicode CLI rendering, cancellation/subprocess handling, and archive lookup performance.

Merge readiness

⚠️ Ready for maintainer review - 1 item remains

Keep this collaborator PR open: current main still has the user-collision failure, and the branch adds a coherent set of regression-tested reliability fixes. The functional patch appears correct; remove the release-owned changelog edits before merge.

Priority: P2
Reviewed head: f92a9f73168de19cce219f6cb9cf1a8963573081

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) The patch is a well-covered reliability pass with one low-risk release-process correction before merge.
Proof confidence 🌊 off-meta tidepool Not applicable: This collaborator PR is not subject to the external-contributor real-behavior-proof gate; its body documents targeted regression and suite validation.
Patch quality 🐚 platinum hermit (4/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Not applicable Not applicable: This collaborator PR is not subject to the external-contributor real-behavior-proof gate; its body documents targeted regression and suite validation.
Evidence reviewed 5 items Current-main gap: Current main still calls the user upsert directly, so a cross-workspace user collision remains fatal; the branch replaces this with collision-aware handling.
Branch implementation and coverage: The branch handles user and message workspace collisions while retaining fatal handling for other errors, with corresponding Slack API regression coverage in the same change set.
Related merged work: The current-main commit fixed only channel collisions; its history confirms this PR retains distinct user/message collision work.
Findings 1 actionable finding [P3] Remove release-owned changelog entries
Security None None.

How this fits together

Slacrawl ingests Slack workspaces into a local SQLite archive, then serves that archive through sync, tail, media, search, share, and CLI commands. The changed code handles incoming Slack data, archive persistence, cached attachments, and command output.

flowchart LR
A[Slack workspaces] --> B[Sync and tail ingestion]
B --> C[Archive store]
C --> D[Media cache]
C --> E[Search and CLI output]
C --> F[Share export]
D --> F
Loading

Before merge

  • Remove release-owned changelog entries (P3) - CHANGELOG.md is release-owned for normal PRs. Move the user-impact summary to the PR body and leave these 17 Unreleased entries for the release process.

Findings

  • [P3] Remove release-owned changelog entries — CHANGELOG.md:8-24
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Code and test delta non-test +444/-252; tests +353/-6 The broad 27-file reliability pass has substantial regression coverage, but merits one cohesive review before landing.
Schema upgrade 1 SQLite schema version change (v6 to v7) The migration adds two lookup indexes and includes focused upgrade coverage.

Root-cause cluster

Relationship: canonical
Canonical: #131
Summary: This PR is the active broad fix for the remaining multi-workspace collision work after the channel-only repair merged.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Technical review

Best possible solution:

Remove the release-owned changelog entries, then land the tested reliability fixes after normal maintainer review.

Do we have a high-confidence way to reproduce the issue?

Yes, by seeding a user or message under another workspace and running bot sync or event ingestion; current main’s direct user upsert establishes the failing path and the branch adds focused regression tests.

Is this the best way to solve the issue?

Yes, after removing the release-owned changelog edits: the fixes preserve existing collision integrity while handling legitimate shared-workspace inputs at the ingestion boundary.

Full review comments:

  • [P3] Remove release-owned changelog entries — CHANGELOG.md:8-24
    CHANGELOG.md is release-owned for normal PRs. Move the user-impact summary to the PR body and leave these 17 Unreleased entries for the release process.
    Confidence: 0.97

Overall correctness: patch is correct
Overall confidence: 0.83

AGENTS.md: not found in the target repository.

Codex review notes: model internal, reasoning high; reviewed against 2fdce12b5057.

Labels

Label changes:

  • add P2: This addresses real sync, attachment, and CLI reliability defects with bounded user impact rather than an active emergency.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🌊 off-meta tidepool and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Not applicable: This collaborator PR is not subject to the external-contributor real-behavior-proof gate; its body documents targeted regression and suite validation.

Label justifications:

  • P2: This addresses real sync, attachment, and CLI reliability defects with bounded user impact rather than an active emergency.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🌊 off-meta tidepool and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Not applicable: This collaborator PR is not subject to the external-contributor real-behavior-proof gate; its body documents targeted regression and suite validation.

Evidence

Acceptance criteria:

  • [P1] git diff --check 2fdce12..HEAD.

What I checked:

  • Current-main gap: Current main still calls the user upsert directly, so a cross-workspace user collision remains fatal; the branch replaces this with collision-aware handling. (internal/slackapi/api.go:280, 2fdce12b5057)
  • Branch implementation and coverage: The branch handles user and message workspace collisions while retaining fatal handling for other errors, with corresponding Slack API regression coverage in the same change set. (internal/slackapi/api.go:280, f92a9f73168d)
  • Related merged work: The current-main commit fixed only channel collisions; its history confirms this PR retains distinct user/message collision work. (internal/slackapi/api.go:1170, 2fdce12b5057)
  • Release status: The branch head is not in a local release tag; v0.8.1 points to an earlier commit, so these fixes are proposed rather than released. (f92a9f73168d)
  • Diff hygiene: The reviewed branch has no whitespace errors; its persistence migration adds only two indexes and includes a v6-to-v7 migration test. (internal/store/store.go:275, f92a9f73168d)

Likely related people:

  • steipete: Introduced the current-main channel-collision behavior and authored the relevant Slack/archive history and this continuation. (role: recent feature owner; confidence: high; commits: 2fdce12b5057, 04145cd12466; files: internal/slackapi/api.go, internal/store/store.go, internal/media/cache.go)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Remove the release-owned CHANGELOG.md entries; retain release context in the PR body.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@steipete
steipete merged commit b4d5fec into main Aug 7, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix other P2 Normal priority bug or improvement with limited blast radius. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants