Skip to content

feat(composio/slack): port Gmail-style ingest pipeline, retire memory/slack_ingestion - #1354

Merged
senamakel merged 4 commits into
tinyhumansai:mainfrom
senamakel:feat/mem-v3
May 8, 2026
Merged

senamakel merged 4 commits into
tinyhumansai:mainfrom
senamakel:feat/mem-v3

Conversation

@senamakel

@senamakel senamakel commented May 8, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Ports the Gmail-style memory ingestion pipeline shape onto Slack: new composio/providers/slack/{ingest,post_process,types,rpc,schemas}.rs mirror Gmail's directory layout (per-message ingest, raw-archive write, set_chunk_raw_refs linkage, slug-dispatched post-process).
  • Retires the parallel memory/slack_ingestion/ module entirely (7 files, ~960 LOC removed). The slack_memory.* JSON-RPC namespace moves to composio/providers/slack/; core/all.rs registration updated accordingly.
  • Drops the 6h UTC bucketer — the L0 seal cascade from feat(memory_tree): scale summariser for cloud — bigger batches, parallel workers, prose output #1348 handles batching now. Provider feeds messages straight into ingest_page_into_memory_tree per channel.
  • Adopts the kind-aware raw layout from feat(memory): kind-aware raw layout, _source.md mirror, source/<slug> tag #1351: Slack messages tagged as RawKind::Chat, archive lands under raw/<source_slug>/chats/<ts_ms>_<ts_raw>.md.

Problem

Slack memory ingestion lived in its own memory/slack_ingestion/ module that didn't share shape with Gmail's composio/providers/gmail/{ingest,post_process}.rs. The split meant:

  • Two different "Composio response → memory tree" pipelines (Gmail's slim-envelope + per-message + raw archive vs. Slack's bucket-batched + no archive) — anyone reading both had to context-switch between conventions.
  • Slack post-processing was inlined into sync.rs::extract_messages rather than a dedicated slug-dispatched step like Gmail's post_process.rs.
  • Slack had no on-disk raw archive, so debugging / Obsidian browsing of ingested chat history was impossible.
  • The 6h UTC bucketer was an L0-seal workaround from before feat(memory_tree): scale summariser for cloud — bigger batches, parallel workers, prose output #1348's count-fallback landed; on cloud-backend Slack workspaces it artificially delayed leaf formation by waiting for whole 6h windows to close.

Solution

Composio is the source path for both providers (the webview path in channels/providers/slack.rs is agent-channel-only; never fed memory). So this is a layout/shape parity job, not an integration-source change.

New under composio/providers/slack/:

  • types.rs — SlackMessage (now carries channel_id, channel_name, is_private, author_id, permalink so canonicalisation has full context without a separate SlackChannel plumbed through), SlackChannel. Old Bucket struct dropped.
  • ingest.rs — pub async fn ingest_page_into_memory_tree(config, owner, connection_id, &[SlackMessage]) -> Result<usize>. Per-message ingest (one ingest_chat call per Slack message, single-message ChatBatch), raw-archive write via write_raw_items with RawKind::Chat, then set_chunk_raw_refs linking each chunk to its raw entry. Helpers: bucket_by_channel, channel_label, slack_message_to_chat_message, write_raw_archive.
  • post_process.rs — slug-dispatched data reshaper for SLACK_FETCH_CONVERSATION_HISTORY, SLACK_LIST_CONVERSATIONS, SLACK_SEARCH_MESSAGES. Pure data; user-id resolution stays in the ingest layer where the per-sync SlackUsers cache lives (matches Gmail's "post_process is data-only" pattern).
  • post_process_tests.rs — 9 cases covering all three slugs, nested vs top-level envelopes, edge cases.
  • rpc.rs + schemas.rs — moved verbatim from memory/slack_ingestion/; namespace slack_memory preserved; exports renamed to all_slack_memory_*.

Updated:

  • provider.rs — post_process_action_result impl added; process_channel and run_backfill_via_search now call post_process → extract_messages → ingest_page_into_memory_tree (no buckets).
  • sync.rs — slimmed to thin enrichers over the post-processed shape.
  • memory/mod.rs, core/all.rs — slack_memory controllers now register from composio::providers::slack.

Deleted: src/openhuman/memory/slack_ingestion/ (entire directory).

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per docs/TESTING-STRATEGY.md
  • N/A: changes are pure-Rust under src/openhuman/composio/providers/slack/...; ≥ 80% diff coverage is enforced via cargo-llvm-cov and lcov merge in CI.
  • N/A: refactor of an existing feature (Slack memory ingestion); no new feature row in the coverage matrix.
  • N/A: no feature-matrix IDs touched.
  • No new external network dependencies introduced (mock backend used per docs/TESTING-STRATEGY.md)
  • N/A: doesn't touch release-cut smoke surfaces (core-internal ingest pipeline).
  • N/A: not linked to a tracked issue.

Impact

  • Runtime / platform: desktop only (core sidecar). No UI changes.
  • Performance: per-channel ingest no longer waits for 6h windows to close — leaves form as soon as L0 seal cascade fires (post-feat(memory_tree): scale summariser for cloud — bigger batches, parallel workers, prose output #1348 count fallback at 10 chunks). On a fresh Slack workspace, visible leaf-creation latency drops from "next 6h+grace period" to "next sync tick + first ingested chunk".
  • Compatibility: slack_memory.sync_trigger / sync_status JSON-RPC methods unchanged in shape; only registration site moved. Source id slack:{connection_id} preserved (workspace-wide). Existing RawRef paths from prior runs still resolve (per feat(memory): kind-aware raw layout, _source.md mirror, source/<slug> tag #1351's parity contract).
  • Migration: no schema changes. Chunk IDs are content-hashed so re-ingest after the layout change is an idempotent UPSERT.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/mem-v3
  • Commit SHA: 3d19407

Validation Run

  • Focused tests: cargo test --lib composio::providers::slack → 43 passed; cargo test --lib memory::tree:: → 534 passed.
  • Rust fmt/check: cargo fmt --check clean; cargo check clean (only pre-existing warnings).
  • N/A: pnpm typecheck — no app-side changes in this PR.
  • N/A: Tauri shell unchanged.

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: Slack memory ingestion now follows Gmail's per-message + raw-archive shape; memory/slack_ingestion/ module retired; bucketer dropped in favor of L0 seal cascade.
  • User-visible effect: Slack chat history appears in Obsidian under raw/<source_slug>/chats/; leaf summaries form within a sync tick instead of waiting for 6h windows to close.

Parity Contract

  • Legacy behavior preserved: slack:{connection_id} source_id unchanged; slack_memory.* RPC shape unchanged; chunk-id content-hashing makes the layout change a no-op UPSERT for already-ingested data; existing RawRef paths still resolve (feat(memory): kind-aware raw layout, _source.md mirror, source/<slug> tag #1351 parity).
  • Guard/fallback/dispatch parity checks: SlackUsers soft-fallback to raw ids preserved; execute_with_retry rate-limit retry preserved; extract_with_retry's daily budget accounting preserved.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): N/A
  • Canonical PR: N/A
  • Resolution (closed/superseded/updated): N/A

Summary by CodeRabbit

  • New Features

    • Added RPC endpoints to trigger and view Slack memory sync; Slack responses are now normalized into stable message/channel shapes and raw archives are preserved during ingestion.
  • Refactor

    • Reorganized Slack provider surface and switched from bucketed to per-channel page-based ingestion for more reliable chunking and cursor handling.
  • Tests

    • Added unit tests covering message parsing, post-processing, and ingestion helpers.

senamakel added 3 commits May 7, 2026 17:59
…/slack_ingestion

- New `composio/providers/slack/{ingest,post_process,types,rpc,schemas}.rs`
  mirror Gmail's directory shape: per-message ingest, raw-archive write,
  `set_chunk_raw_refs` linkage, slug-dispatched post-process.
- Drop the 6h UTC bucketer entirely — L0 seal cascade (PR tinyhumansai#1348) handles
  batching now. Provider feeds messages straight to
  `ingest_page_into_memory_tree` per channel.
- Source id stays `slack:{connection_id}` (workspace-wide); channel
  context lives on `SlackMessage` and in `ChatBatch.channel_label`.
- `slack_memory.*` JSON-RPC namespace moves from `memory/slack_ingestion/`
  into `composio/providers/slack/`; `core/all.rs` registration updated.
- `memory/slack_ingestion/` deleted (7 files, ~960 LOC).

Validated: cargo check clean; `composio::providers::slack` 43 tests pass;
`memory::tree::` 527 tests pass; cargo fmt clean.
…merge

PR tinyhumansai#1351 added a `kind` field to `RawItem` and a `raw_rel_path` helper
that routes per-kind subdirs (`emails/`, `chats/`, …). Update Slack's
ingest to:

- Tag `RawItem` with `RawKind::Chat` (raw archive now lands under
  `raw/<source_slug>/chats/<ts_ms>_<ts_raw>.md`).
- Use `raw_rel_path` instead of building paths by hand — drops the
  local `sanitize_uid_for_path` helper since `write_raw_items` does
  the sanitization centrally now.
- Drop the `slugify_source_id` import (no longer needed).

Validated: cargo check clean; 43 slack tests pass; 534 memory-tree
tests pass; cargo fmt clean.
@senamakel
senamakel requested a review from a team May 8, 2026 01:21
@coderabbitai

coderabbitai Bot commented May 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b46a637-3be8-4700-88a5-d53bafa302ff

📥 Commits

Reviewing files that changed from the base of the PR and between 3d19407 and 996606b.

📒 Files selected for processing (2)
  • src/openhuman/composio/providers/slack/provider.rs
  • src/openhuman/composio/providers/slack/sync.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/openhuman/composio/providers/slack/provider.rs
  • src/openhuman/composio/providers/slack/sync.rs

📝 Walkthrough

Walkthrough

This pull request migrates Slack ingestion from 6‑hour UTC buckets to per‑channel page ingestion, consolidates Slack logic under composio::providers::slack, adds post-processing that normalizes provider responses, introduces canonical Slack types, updates RPC/schemas, and removes legacy bucketing code.

Changes

Slack Ingestion Refactoring

Layer / File(s) Summary
Canonical Type Definitions
src/openhuman/composio/providers/slack/types.rs
Introduces SlackMessage and SlackChannel structs with channel context, author resolution, parsed timestamps, and optional thread/permalink fields, decoupled from API payload shapes.
API Response Post-Processing
src/openhuman/composio/providers/slack/post_process.rs
New module normalizes Composio Slack responses by parsing nested envelopes, filtering empty entries, and reshaping into canonical slim JSON shapes (messages[], channels[], pages) for three supported slugs (history, conversations, search).
Page-Based Ingestion
src/openhuman/composio/providers/slack/ingest.rs
Replaces bucket-based ingestion with ingest_page_into_memory_tree() entry point that accumulates messages per channel, writes raw archive markdown files with metadata headers, ingests per-message via ChatBatch entries, and associates chunks with raw references.
Response Extraction Refactoring
src/openhuman/composio/providers/slack/sync.rs
Adapts sync helpers (extract_channels, extract_messages, extract_search_messages) to work with post-processed slim shapes using direct field access instead of multi-pointer envelope walking; receives and injects channel metadata for enrichment.
Provider Sync Implementation
src/openhuman/composio/providers/slack/provider.rs
Rewrites process_channel to accumulate paginated history, post-process each page, and ingest via ingest_page_into_memory_tree with cursor advancement on success; refactors run_backfill_via_search to group by channel instead of time buckets and ingest per channel; adds post_process_action_result hook.
JSON-RPC Endpoints & Schemas
src/openhuman/composio/providers/slack/rpc.rs, schemas.rs
Defines sync_trigger_rpc and sync_status_rpc endpoints with request/response types; wires JSON-RPC dispatchers to parse typed requests, delegate to RPC functions, and return CLI-compatible outcomes; renames public entry points to all_slack_memory_* naming pattern.
Module Visibility & Wiring
src/openhuman/composio/providers/slack/mod.rs, src/core/all.rs
Exports Slack provider submodules (ingest, post_process, rpc, schemas, sync, types, users) as pub mod; updates core controller registry to use all_slack_memory_registered_controllers() and all_slack_memory_controller_schemas().
Legacy Code Removal
src/openhuman/memory/slack_ingestion/*, src/openhuman/memory/mod.rs
Deletes bucketer.rs (6-hour UTC bucket boundaries and split logic), removes slack_ingestion module declarations and re-exports from memory/mod.rs, removes documentation for deprecated bucket-based approach.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • graycyrus

Poem

A rabbit hops through bundles of code,
Where Slack messages left the bucketing road,
Pages flow in, neat and swift as stream,
Post-processed shapes and a metadata dream,
Provider consolidated — the pipeline gleams! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main change: porting a Gmail-style ingest pipeline to Slack and retiring the legacy memory/slack_ingestion module.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@senamakel

Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/openhuman/composio/providers/slack/ingest.rs (1)

163-183: 💤 Low value

Consider hoisting the tags allocation outside the loop.

The tags vector is recreated identically for every message iteration. Moving it before the loop would avoid repeated allocations:

 async fn ingest_per_message(
     config: &Config,
     source_id: &str,
     owner: &str,
     page_messages: &[SlackMessage],
 ) -> usize {
     let mut total_chunks = 0usize;
+    let tags: Vec<String> = DEFAULT_TAGS.iter().map(|s| (*s).to_string()).collect();
 
     for m in page_messages {
         // ...
-        let tags = DEFAULT_TAGS.iter().map(|s| (*s).to_string()).collect();
+        let tags_clone = tags.clone();
 
-        match ingest_chat(config, source_id, owner, tags, batch).await {
+        match ingest_chat(config, source_id, owner, tags_clone, batch).await {

This is minor given the API rate-limited throughput.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/composio/providers/slack/ingest.rs` around lines 163 - 183, The
tags vector is allocated anew for each iteration inside the for m in
page_messages loop; hoist its creation outside the loop by moving let tags =
DEFAULT_TAGS.iter().map(|s| (*s).to_string().collect() before the for so the
same tags Vec<String> is reused for every message. Update code to remove the
inner let tags = ... and reference the outer tags inside the loop; keep the
original mapping logic and the same variable name to minimize changes (symbols:
DEFAULT_TAGS, tags, page_messages, ChatBatch).
src/openhuman/composio/providers/slack/provider.rs (1)

753-755: 💤 Low value

Consider avoiding the clone in backfill path.

The current code clones each message when converting from Vec<&SlackMessage> to Vec<SlackMessage>. While acceptable for a one-shot backfill operation, you could restructure to pass references if ingest_page_into_memory_tree accepted &[&SlackMessage] or similar.

This is minor given the backfill context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/composio/providers/slack/provider.rs` around lines 753 - 755,
The backfill currently clones each SlackMessage into a new Vec<SlackMessage>
before calling ingest_page_into_memory_tree; change ingest_page_into_memory_tree
to accept a slice of references (e.g., &[&SlackMessage]) so you can pass
msgs_for_channel.as_slice() (or construct a Vec<&SlackMessage> by mapping
references) instead of cloning, and update all call sites of
ingest_page_into_memory_tree and its signature accordingly (refer to
ingest_page_into_memory_tree, msgs_for_channel, buckets, page, connection_id,
and ctx.config to locate the changes).
src/openhuman/composio/providers/slack/rpc.rs (1)

45-108: ⚡ Quick win

Add debug/trace instrumentation around the new Slack RPC flows.

These handlers only log failures plus the terminal single_log, so connection filtering, provider discovery, and state-load skips are hard to reconstruct when a sync behaves unexpectedly. Please add entry/exit and key branch diagnostics with stable [composio:slack][rpc]-style prefixes and correlation fields.

As per coding guidelines, Use log / tracing at debug or trace level on RPC entry/exit, error paths, state transitions, and any branch that is hard to infer from tests; include stable prefixes (e.g. [domain], [rpc]) and correlation fields.

Also applies to: 136-183

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/composio/providers/slack/rpc.rs` around lines 45 - 108, Add
debug/trace instrumentation to sync_trigger_rpc: log an entry message at start
with prefix "[composio:slack][rpc]" and include correlation fields (e.g.,
req.connection_id and total connections), debug the provider discovery result
(provider existence), the Composio client creation, the list_connections result
(count), the candidate filtering steps (before/after filtering and when
req.connection_id narrows results), each skipped/no-op branch (no candidates
found), and an exit message with outcomes summary; use log::debug or log::trace
for these messages and keep existing error/warn handling for provider.sync
errors (log when provider.sync is invoked and when it returns Err), referencing
the symbols sync_trigger_rpc, provider, build_composio_client/client,
connections, candidates, req.connection_id, outcomes, ProviderContext, and
provider.sync so the logs can reconstruct the flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/composio/providers/slack/sync.rs`:
- Around line 202-205: parse_ts currently truncates the Slack ts fractional part
and ChannelCursors persists only unix-seconds, causing duplicate replays; change
the cursor handling so you persist the full Slack ts string (or another lossless
representation) instead of seconds, stop converting to whole seconds in
parse_ts, and use the exact stored ts as the resume/oldest value in the sync
logic (references: parse_ts, ChannelCursors, and the code paths that set/consume
oldest around the sync fetch logic and the other parse_ts usage at the second
location noted).
- Around line 168-176: The code currently allows an empty channel_id and maps it
to a blank channel, causing malformed SlackMessage records; change the logic
around channel_id (the raw.get("channel_id") -> as_str() -> to_string()
sequence) to treat missing or empty channel_id as an invalid hit and skip/return
early instead of creating a SlackMessage: after extracting channel_id, if it's
None or channel_id.trim().is_empty() then bail out (e.g., return None or
continue) so you never consult channel_map or construct a SlackMessage for empty
channels; update any calling code that expects an Option to handle the skipped
case accordingly.

---

Nitpick comments:
In `@src/openhuman/composio/providers/slack/ingest.rs`:
- Around line 163-183: The tags vector is allocated anew for each iteration
inside the for m in page_messages loop; hoist its creation outside the loop by
moving let tags = DEFAULT_TAGS.iter().map(|s| (*s).to_string().collect() before
the for so the same tags Vec<String> is reused for every message. Update code to
remove the inner let tags = ... and reference the outer tags inside the loop;
keep the original mapping logic and the same variable name to minimize changes
(symbols: DEFAULT_TAGS, tags, page_messages, ChatBatch).

In `@src/openhuman/composio/providers/slack/provider.rs`:
- Around line 753-755: The backfill currently clones each SlackMessage into a
new Vec<SlackMessage> before calling ingest_page_into_memory_tree; change
ingest_page_into_memory_tree to accept a slice of references (e.g.,
&[&SlackMessage]) so you can pass msgs_for_channel.as_slice() (or construct a
Vec<&SlackMessage> by mapping references) instead of cloning, and update all
call sites of ingest_page_into_memory_tree and its signature accordingly (refer
to ingest_page_into_memory_tree, msgs_for_channel, buckets, page, connection_id,
and ctx.config to locate the changes).

In `@src/openhuman/composio/providers/slack/rpc.rs`:
- Around line 45-108: Add debug/trace instrumentation to sync_trigger_rpc: log
an entry message at start with prefix "[composio:slack][rpc]" and include
correlation fields (e.g., req.connection_id and total connections), debug the
provider discovery result (provider existence), the Composio client creation,
the list_connections result (count), the candidate filtering steps (before/after
filtering and when req.connection_id narrows results), each skipped/no-op branch
(no candidates found), and an exit message with outcomes summary; use log::debug
or log::trace for these messages and keep existing error/warn handling for
provider.sync errors (log when provider.sync is invoked and when it returns
Err), referencing the symbols sync_trigger_rpc, provider,
build_composio_client/client, connections, candidates, req.connection_id,
outcomes, ProviderContext, and provider.sync so the logs can reconstruct the
flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3135c403-6fed-4c57-ad51-68bba8d32164

📥 Commits

Reviewing files that changed from the base of the PR and between fb6f1e3 and 3d19407.

📒 Files selected for processing (16)
  • src/core/all.rs
  • src/openhuman/composio/providers/slack/ingest.rs
  • src/openhuman/composio/providers/slack/mod.rs
  • src/openhuman/composio/providers/slack/post_process.rs
  • src/openhuman/composio/providers/slack/post_process_tests.rs
  • src/openhuman/composio/providers/slack/provider.rs
  • src/openhuman/composio/providers/slack/rpc.rs
  • src/openhuman/composio/providers/slack/schemas.rs
  • src/openhuman/composio/providers/slack/sync.rs
  • src/openhuman/composio/providers/slack/types.rs
  • src/openhuman/memory/mod.rs
  • src/openhuman/memory/slack_ingestion/README.md
  • src/openhuman/memory/slack_ingestion/bucketer.rs
  • src/openhuman/memory/slack_ingestion/mod.rs
  • src/openhuman/memory/slack_ingestion/ops.rs
  • src/openhuman/memory/slack_ingestion/types.rs
💤 Files with no reviewable changes (6)
  • src/openhuman/memory/slack_ingestion/README.md
  • src/openhuman/memory/slack_ingestion/mod.rs
  • src/openhuman/memory/slack_ingestion/types.rs
  • src/openhuman/memory/slack_ingestion/bucketer.rs
  • src/openhuman/memory/slack_ingestion/ops.rs
  • src/openhuman/memory/mod.rs

Comment thread src/openhuman/composio/providers/slack/sync.rs
Comment thread src/openhuman/composio/providers/slack/sync.rs
…channel_id

Two CodeRabbit findings on PR tinyhumansai#1354:

1. **Cursor precision** (Major). `ChannelCursors` was `BTreeMap<String, i64>`
   storing whole unix seconds, and `process_channel` advanced the cursor
   from `m.timestamp.timestamp()`. Slack `ts` is `<seconds>.<micro>` per-
   workspace sequence — channels with multiple messages in the same wall-
   clock second would replay that whole second on the next incremental
   fetch (`oldest = "<sec>.000000"` with `inclusive=false`).

   Switch the map value to the **raw Slack `ts` string** so full
   microsecond precision round-trips. Cursor lookup uses the stored
   string verbatim as `oldest`. New `parse_ts_components(ts) -> (i64, u64)`
   helper picks the latest message by `(seconds, micros)` tuple instead
   of relying on lexicographic-equals-numeric for the raw string.

   Migration: persisted state from before this commit fails to deserialize
   the new shape; `decode_cursors` already soft-falls to an empty map, so
   the next sync re-walks the backfill window once. No data loss — chunk
   IDs are content-hashed.

2. **Empty `channel_id` in search hits** (Minor). `parse_search_match`
   accepted an empty channel id and emitted a `SlackMessage` with
   `channel_id=""`, which the new ingest path would then funnel into a
   single empty-channel bucket. Added a `?` filter so malformed matches
   are dropped at parse time.

Tests: +2 (`extract_search_messages_drops_match_with_missing_channel_id`,
`parse_ts_components_preserves_microseconds`). 45 slack tests pass.
@senamakel
senamakel merged commit 319afbe into tinyhumansai:main May 8, 2026
20 checks passed
AusAgentSmith pushed a commit to AusAgentSmith/openhuman that referenced this pull request May 23, 2026
senamakel added a commit to nocstah/openhuman that referenced this pull request Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant