Repository navigation
Feat/slack sesssion router prod - #1772
Closed
knechtionscoding wants to merge 2 commits into
Closed
knechtionscoding wants to merge 2 commits into
knechtionscoding wants to merge 2 commits into
Conversation
Gravity starts over between Slack 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 on turn.completed. It handles a turn already in flight, a prompt merged into a queued turn, 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. 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
marked this pull request as ready for review
September 25, 2026 13:46
knechtionscoding
had a problem deploying
to
ok-to-test
September 25, 2026 13:46 — with
GitHub Actions
Failure
This was referenced Sep 26, 2026
This branch had an error being 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:
Gravity starts over between Slack 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, butnothing could drive a turn from outside a browser, and nothing connected Slack
to any of it. This PR adds both halves.
A turn driver —
internal/sessionturn. There is no request/response API fora Session turn: the runtime listens on a unix socket inside the Session Pod, and
the console server reaches it by
pods/exec-ing/kelos/bin/kelos-session-runtime.This package speaks that protocol over the same bridge. It subscribes, submits
one
messagerequest with a request ID, learns its turn ID from the echoed usermessage, follows only that turn's events, and returns the assistant reply on
turn.completed. It handles a turn already in flight (the runtime queues behindit), 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 with
githubWebhook, reusing the channel, trigger, exclusionand bot-message rules TaskSpawner's Slack source already has.
sessionTemplate.initialPromptandsessionTemplate.suspendare rejected for aSlack 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 shellcommand 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 withthe 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 — tracked in GRAV-1028.
Special notes for your reviewer:
The ticket proposed this as a stack of two PRs. It is one here because the turn
driver alone cannot be exercised in Slack, and the point of shipping this is to
see what a threaded conversation actually feels like.
Decisions worth a second opinion:
controller. A turn does not survive a Slack server restart; the Session and
its history do, so the next message in the thread picks up where it left off.
The runtime also merges a prompt into an already-queued turn, which the driver
follows rather than fighting.
volumeClaimTemplate, a PVC.idlePolicyis the lever; the example setssuspendAfterSeconds: 1800anddeleteAfterSeconds: 86400. Worth pickingreal numbers before the first deploy rather than after the cluster fills.
spec.when.slack.channelskeeps the first deploy to one channel.producing two replies. Documented; not enforced.
RBAC: the Slack server now needs
createonpods/exec,get/list/watchon
sessionspawners, andcreate/get/list/watch/patchonsessions.The chart grants these.
No UI changes, so no screenshots.
make verify,make testandmake test-integrationall pass locally (166 + 12 integration specs).Does this PR introduce a user-facing change?
🤖 Generated with Claude Code
Summary by cubic
Gives each Slack thread a persistent
Sessionso matching messages in a thread run as turns of one conversation, instead of each message spawning a fresh Task that starts with no history.Slack-driven Sessions
internal/sessionturn, which drives one turn by speaking the Session runtime's socket protocol over thepods/execbridge the console server uses.spec.when.slacktoSessionSpawner, mutually exclusive withgithubWebhook;sessionTemplate.initialPromptandsessionTemplate.suspendare rejected for a Slack source.channel + thread_tsto a deterministic Session name, creates the Session on the thread's first matching message, and reuses it for later ones. Messages arriving mid-turn wait per-thread; up to eight may queue before the bot reports the drop.!-prefixed text as shell commands — so no user-controlled Slack text can reach the runtime first.RBAC
createonpods/exec,get/list/watchonsessionspawners, andcreate/get/list/watch/patchonsessions; the chart grants these.Written for commit 2932424. Summary will update on new commits.