Repository navigation
Give Slack threads a persistent Session - #1859
Open
knechtionscoding wants to merge 1 commit into
Open
knechtionscoding wants to merge 1 commit into
knechtionscoding wants to merge 1 commit into
Conversation
knechtionscoding
had a problem deploying
to
ok-to-test
October 29, 2026 17:59 — with
GitHub Actions
Error
knechtionscoding
force-pushed
the
feat/slack-session-threads
branch
from
October 29, 2026 18:07
1f2c7d1 to
7a29489
Compare
knechtionscoding
had a problem deploying
to
ok-to-test
October 29, 2026 18:07 — with
GitHub Actions
Error
An agent driven from Slack starts over between messages. Each message spawns a fresh Task with no conversation history, so iterating in a thread means re-explaining context every turn. Session and SessionSpawner already covered a conversation's lifecycle, but nothing could drive a turn from outside a browser, and nothing connected Slack to any of it. internal/sessionturn drives one turn. The Session runtime exposes no request/response API for a turn: it listens on a unix socket inside the Session Pod, which the console server reaches by pods/exec-ing kelos-session-runtime. This package speaks that protocol over the same bridge — subscribe, submit one message, follow only that turn's events, return the assistant reply. It mirrors the runtime's own history projection when collecting that reply, because OpenCode streams deltas and never sends a closing assistant.message. It handles a turn already in flight, a prompt merged into a queued turn, a queued prompt withdrawn from the console, a dropped stream, and a bound on how long a turn may run. A provider question is declined, because a thread cannot answer one mid-turn and an unanswered question would stall the turn while holding the runtime. A turn the driver abandons is withdrawn — interrupted if it started, removed if it is still queued — unless it carries another client's prompt too, in which case it is left alone. SessionSpawner.spec.when gains a slack source, mutually exclusive with githubWebhook, reusing the Slack matching rules TaskSpawner already has. initialPrompt and suspend are rejected there: every matching thread message already runs as its own turn, and a suspended Session cannot answer the thread that created it. An idle policy is required, because every matching thread gets a Pod and a volume that outlive the conversation. The Slack server maps channel and thread timestamp to a deterministic Session name, creates the Session on the thread's first matching message, and reuses it for every later matching message. Each thread has its own serial queue, so a message arriving mid-turn waits rather than opening a second stream into the same Session. While a turn runs the bot posts a placeholder into the thread and edits it in place with the result; a graceful shutdown resolves any placeholder left behind rather than leaving the thread showing work that is no longer running. Every prompt opens with a literal. The runtime parses a submitted prompt for its own commands before the agent sees it — text beginning with "!" runs as a shell command inside the Session Pod with the Session's credentials — so no part of a Slack message that its author controls, including their display name, may come first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
knechtionscoding
force-pushed
the
feat/slack-session-threads
branch
from
October 29, 2026 18:11
7a29489 to
2edfccd
Compare
knechtionscoding
requested a deployment
to
ok-to-test
October 29, 2026 18:11 — with
GitHub Actions
Waiting
This branch is waiting to be deployed
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.
What type of PR is this?
/kind api
What this PR does / why we need it:
A Slack-driven agent starts over between messages. Each message spawns a fresh Task that runs the agent with no conversation history, so iterating in a thread does not work the way iterating in a Claude Code conversation does: you re-explain context every turn.
SessionandSessionSpawneralready cover a conversation's lifecycle, but nothing could drive a turn from outside a browser, and nothing connected Slack to any of it. This PR adds both halves. It is the bottom of a two-PR stack; the next PR names the channel and thread in every prompt the bridge submits.A turn driver,
internal/sessionturn. There is no request/response API for a Session turn: the runtime listens on a unix socket inside the Session Pod, and the console server reaches it bypods/exec-ing/kelos/bin/kelos-session-runtime. This package speaks that protocol over the same bridge. It subscribes, submits onemessagerequest with a request ID, learns its turn ID from the echoed user message, follows only that turn's events, and returns the assistant reply onturn.completed. It handles a turn already in flight (the runtime queues behind it), a prompt merged into an already-queued turn, a stream that drops mid-turn, and a 60-minute bound on one turn.WaitReadyresolves a Session to its Pod, requesting a resume when the idle policy suspended it.The Slack ingress.
SessionSpawner.spec.whengains aslacksource, mutually exclusive withgithubWebhook, reusing the channel, trigger, exclusion and bot-message rules TaskSpawner's Slack source already has.sessionTemplate.initialPromptandsessionTemplate.suspendare rejected for a Slack source: every matching thread message already runs as its own turn, so an initial prompt would only delay the message that asked for something, and a suspended Session cannot answer the thread that created it.Every prompt the bridge submits begins with a fixed envelope naming the author. That is load-bearing, not cosmetic: the runtime parses a prompt for its own commands before the agent sees it, and text beginning with
!runs as a shell command in the Session Pod with the Session's credentials.The Slack server maps
channel + thread_tsto a deterministic Session name (hashed, since a Slack timestamp contains a dot), creates the Session on the thread's first matching message and reuses it for every later matching message in that thread. An unmatched reply is ignored and never reaches the Session. Each thread gets its own serial queue, so a message arriving mid-turn waits rather than opening a second stream into the same Session; up to eight may wait before the bot says so in the thread.What it looks like in Slack. The bot posts a
:hourglass: Working on your request...reply into the thread, then edits that same message in place with the answer once the turn finishes. A reply too long for one Slack message continues in further thread replies. A failed turn, an interrupted turn, or a Session that never becomes ready reports the reason in the same place rather than leaving the thread waiting.Which issue(s) this PR is related to:
N/A. This is #1772 rebased onto current
main; that PR was opened from our fork's production branch by mistake and closed.Special notes for your reviewer:
Decisions worth a second opinion:
volumeClaimTemplate, a PVC.idlePolicyis the lever; the example setssuspendAfterSeconds: 1800anddeleteAfterSeconds: 86400.spec.when.slack.channelskeeps the first deploy to one channel.RBAC: the Slack server now needs
createonpods/exec,get/list/watchonsessionspawners, andcreate/get/list/watch/patchonsessions. The chart grants these.Since #1772 the Session provider contract moved turn completion onto the event sink (
SetEventSink+StartTurn), so the echo provider ininternal/sessionturn/integration_test.gowas ported to it; the driver itself needed no change, since it speaks the socket protocol rather than the provider interface.No UI changes, so no screenshots.
make updateproduces no diff, andmake verify,make testandmake test-integration(181 + 12 install specs) pass locally.Does this PR introduce a user-facing change?
🤖 Generated with Claude Code
Summary by cubic
Slack threads now keep a persistent Session instead of starting a fresh Task per message, so the agent retains conversation history across turns instead of re-explaining context each time.
Adds
internal/sessionturn, which drives one conversation turn over the samepods/execbridge the console server uses, and aspec.when.slacksource onSessionSpawnerthat reuses the existing Slack matching rules fromTaskSpawner. The Slack server maps channel and thread to a deterministic Session, creates it on the thread's first matching message, and runs every later matching message as another turn in the same conversation. Provider questions are declined so a turn cannot stall mid-ask, and an abandoned turn is withdrawn — interrupted if running, removed if queued — unless it also carries another client's prompt.Slack behavior
!run as a shell command in the Session Pod, so the bridge's fixed prompt envelope — which keeps nothing user-controlled first — is load-bearing.Validation and operations
spec.whennow requires exactly one ofgithubWebhookorslack;initialPromptandsuspendare rejected for a Slack source, an idle policy is required, and a later message in the thread resumes a suspended Session.pods/execcreate plus reads and writes on Sessions and SessionSpawners); the chart grants it.Written for commit 2edfccd. Summary will update on new commits.