Skip to content

fix(server): pass files from earlier thread messages with a mention - #466

Open
bimgeek wants to merge 13 commits into
opencoredev:mainfrom
bimgeek:fix/channel-mention-context-files
Open

bimgeek wants to merge 13 commits into
opencoredev:mainfrom
bimgeek:fix/channel-mention-context-files

Conversation

@bimgeek

@bimgeek bimgeek commented Oct 19, 2026 •

Copy link
Copy Markdown

Stacked on #465. GitHub can't stack PRs from a fork, so the diff below includes #463, #464, and #465. This PR's own change: compare.

What Changed

  • When someone @mentions the bot in a Slack or Discord thread, files shared in the earlier messages it reads now come along. Each context line names its files, for example Maya: [shared build-error.png].
  • A context file is attached only while its line fits the context limit. The mention's own files come first.
  • Context now stops at the mention. Messages sent after it no longer slip in.

Why

"Screenshot, then @bot what's wrong here?" is a common way to ask. The bot got the question without the screenshot.

Before / after

Mention context before and after

The before and after columns are captured from the real Slack adapter and channel runtime, on #465 and on this branch.

Verification

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I linked the accepted plugin or provider proposal in Why, or this PR does not add one
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Written with Claude Opus 5.5 in Claude Code.


Devin Review

@vercel

vercel Bot commented Oct 19, 2026

Copy link
Copy Markdown

@bimgeek is attempting to deploy a commit to the OpenCore Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL labels Oct 19, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 5 potential issues.

Devin Review

Comment on lines +196 to +199
: recent.filter(
(candidate) =>
candidate.metadata.dateSent.getTime() <= message.metadata.dateSent.getTime(),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Later replies enter mention context

When history omits a mention, mentionWithContext includes later Slack replies sent within its millisecond. dateSent.getTime() loses Slack's microsecond ordering, so their text and files reach the earlier turn.

Learn more

A mention absent from recentMessages triggers the timestamp fallback. JavaScript Date values only store milliseconds, while Slack message IDs use microsecond timestamps. A later message can therefore have the same dateSent.getTime() as the mention and pass the inclusive comparison. Its content and files are then included by contextLine in the earlier turn.

Example: A mention at Slack timestamp 1710000000.000100 is missing from history; a later reply at 1710000000.000101 remains. Both dates produce the same millisecond, so the reply and its screenshot enter the mention's context.

Recommended fix: Compare provider message IDs at their native precision when both are comparable Slack timestamps, and use a strict timestamp comparison otherwise. Exclude the mention itself by ID in the fallback.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 9fe004d (#466). When history no longer holds the mention, the fallback now keeps only messages from an earlier millisecond. A later reply at .100400 behind a mention at .100000 is now left out. The cost is that an earlier message in the mention's own millisecond is dropped too, which is safer than pulling a later reply into the earlier turn. The new test failed before the change and passes now, and the internals doc says the same.

Comment on lines +34 to +35
const cappedName = (name: string) =>
Array.from(oneLine(name)).slice(0, ATTACHMENT_NAME_MAX_CHARS).join("").trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Emoji file names prevent channel turns

For long emoji names, cappedName retains 255 code points exceeding the attachment schema's 255-character limit. The turn then fails validation after downloading and saving the file.

Learn more

The channel adapter normalizes attachment names before they become ChatAttachment values. The schema enforces a maximum string length of 255, but the new cap counts Unicode code points instead of UTF-16 string units. Astral characters such as emoji consume two UTF-16 units each. A name that passes this cap can thus fail validation when dispatchInboundChannelMessage sends the turn, after the file has already been stored.

Example: A file named with 200 emoji remains 200 code points after normalization but occupies 400 UTF-16 units. The channel writes the file and the turn cannot accept the attachment.

Recommended fix: Cap names using the same length measure as ChatAttachment while avoiding a trailing orphaned surrogate; test a name containing many astral characters through the full dispatch boundary.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 3bbbcf1, which belongs to #465 where this code was added. cappedName now counts UTF-16 units, the same measure as the schema's isMaxLength(255), and stops before a character that would not fit, so it never splits a surrogate pair. A new test sends a 200-emoji name through the Slack dispatch path and gets exactly 127 emoji (254 units). It failed before the fix. An older test that encoded the code-point cap was updated to the new rule.

Comment on lines +136 to +140
const onInbound: InboundCallback = (message) =>
ctx.runSdkCallback(
downloadInboundAttachments(ctx.inboundDownloadPermits, message.attachments).pipe(
Effect.flatMap((attachments) => dispatchUnderProviderLock(message, attachments)),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Disconnected channels still fetch files

When a retired transport calls onInbound, downloadInboundAttachments fetches its files before the current-binding check. The message is discarded afterward, but the unnecessary downloads still run.

Learn more

The callback can remain reachable after a failed shutdown or a replacement. Before the change, dispatchUnderProviderLock checked that the runtime still owns the connection and its binding is connected before doing inbound work. The new callback downloads files before reaching those checks, so a retired transport can consume network time and bandwidth for messages that will not be handled.

Example: A Telegram transport fails to shut down during disconnect and invokes its old callback with eight PNG files. All eight files are fetched before the binding check discards the message.

Recommended fix: Perform a cheap current-runtime and connected-binding check before downloading, then keep the existing under-lock check to cover changes while the download is in progress.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in ed1e4d2, in #465. The "this transport still owns a connected binding" check is now one connectedRuntime effect. onInbound runs it before any download, and the dispatch runs it again under the provider lock in case the state changes during a download. The retired and replaced transport test now counts fetches. It saw 2 before the fix, and now sees 0 for retired callbacks and 1 for the current one.

"akeru-bot": patch
---

Bots connected to Slack and Discord now see where links point, even when a link shows a shortened label.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Unrelated changes share one pull request

This PR combines link rewriting, file ingestion, atomic writes, and mention context. AGENTS.md requires one concern per PR, so these independent changes need separate reviews.

Learn more

The repository's PR rule requires one concern per PR. This diff includes independent behavior changes to link rewriting, channel file ingestion, atomic file writes, and mention context, each described by its own changeset. Bundling them prevents independent review and rollback even though the immediate mention-context fix does not require the unrelated link feature.

Example: A regression in link rewriting requires reverting the same merged change that adds mention context files, even though the two features do not depend on each other.

Recommended fix: Separate independent features into focused PRs with their respective changesets; keep only changes needed for the mention-context behavior in this PR.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is a stack of four single-concern PRs: links #463, atomic write #464, shared files #465, mention-context files #466. PRs from a fork can't target another PR's branch, so each PR's diff against main includes the PRs below it. The description of each PR links to its own change, and they merge bottom-up.

Comment on lines +252 to +254
if (!(yield* fileSystem.exists(path).pipe(Effect.mapError(fail)))) {
yield* writeFileAtomically(path, input.bytes).pipe(Effect.mapError(fail));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Concurrent writes need an ownership check

writeAttachmentIfMissing checks existence separately from rename. Confirm whether independent writers can target one deterministic ID; on Windows, the second rename can fail.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No independent writers can target the same id. writeAttachmentIfMissing is only called from saveDownloadedFiles, inside dispatchInboundChannelMessage, which runs under withChannelOperation, the channel:<provider> lock. The id derives from the bot, project, provider, external thread, message, content hash, and occurrence, so two writes for one id would need the same provider and are serialized by that lock. Within one message, saves run with concurrency: 1, and the occurrence index keeps identical files apart. Composer uploads use random ids. So the existence check and the rename never race. Leaving this as is.

@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; the startup-message issue is fixed and no new blocking issue was found.

Summary

The PR brings files from earlier Slack and Discord thread messages into a bot mention’s context.

  • Slack and Discord history links show their full destination.
  • A mention brings along files from earlier thread messages.
  • Channel messages can send shared files into an Akeru turn.
  • Messages in the same channel thread wait in arrival order.
  • Attachment uploads use one atomic write path.

Review was based on code and existing tests; tests were not run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Platform message] --> B[Read earlier thread context]
  B --> C[Keep text and complete file markers]
  C --> D[Wait for connected runtime]
  D --> E[Download files outside provider lock]
  E --> F[Recheck runtime under provider lock]
  F --> G[Save files atomically]
  G --> H[Start bot turn]
Loading

Reviews (6) · Last reviewed commit: "fix(server): keep same-millisecond repli..." · Reviewed by Greptile

Comment thread apps/server/src/channels/ChannelInboundAttachments.ts Outdated
Comment thread apps/server/src/channels/ChannelInboundAttachments.ts Outdated
Comment thread apps/server/src/channels/ChannelInbound.ts Outdated
@bimgeek
bimgeek force-pushed the fix/channel-mention-context-files branch from 7e720d3 to 2603e11 Compare October 19, 2026 14:57
Comment thread apps/server/src/channels/ChannelConnectionLifecycle.ts Outdated
@bimgeek
bimgeek force-pushed the fix/channel-mention-context-files branch from 2603e11 to 9fe004d Compare October 19, 2026 15:05
Comment thread apps/server/src/channels/ChannelConnectionLifecycle.ts Outdated
@bimgeek
bimgeek force-pushed the fix/channel-mention-context-files branch from 9fe004d to 0e7c8a4 Compare October 19, 2026 15:11
@bimgeek
bimgeek force-pushed the fix/channel-mention-context-files branch from 0e7c8a4 to 054c278 Compare October 19, 2026 15:24
@bimgeek
bimgeek force-pushed the fix/channel-mention-context-files branch from 054c278 to 80bc3a0 Compare October 19, 2026 15:32

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant