fix(slackbotv2): stay silent on intentional empty completions - #47
Open
joystick-gg[bot] wants to merge 8 commits into
Open
joystick-gg[bot] wants to merge 8 commits into
joystick-gg[bot] wants to merge 8 commits into
Conversation
Retarget publish-images.yml at ghcr.io/cartridge-gg/centaur on GitHub-hosted runners with docker buildx (this fork has no Depot project), add the release/c7e-centaur-overlay* branch glob and contrib/chart/** path trigger, and auto-dispatch the c7e overlay deploy when canonical-branch images publish. Add tag-release.yml: durable c7e-<upstream-base>-<counter> tags cut from main, so deployed commits can never be orphaned by history rewrites. Gate release-chart.yml's tag minting to the upstream repo so the fork cannot mint colliding centaur-* tags. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ough
Console: add a github_app grant to the broker-credential registry. The
console holds the App private key (client_secret, stored encrypted) and
mints short-lived installation tokens via an RS256 App JWT, rotating
them like any other brokered credential. An idempotent seed migration
provisions the credential from GITHUB_APP_ID / GITHUB_APP_INSTALLATION_ID /
GITHUB_APP_PRIVATE_KEY_B64 when set; the chart wires those env vars into
the console from the shared infra Secret.
api-rs: let a tool's pyproject declare where an HTTP secret's value
comes from (source = {type = "token_broker", credential_id = ...}) and
forward the table verbatim into the generated iron-proxy fragment:
tool_discovery parses it, centaur-perms mirrors it, iron-control accepts
typed sources at startup. Falls back to the {placeholder: <secret_ref>}
shape when no source table is given.
Together these let a tool (the deployment's github_auth_headers) ride
broker-rotated GitHub App tokens: raw tokens never enter sandboxes and
no out-of-band CronJob rotates a shared secret.
Pending upstream as paradigmxyz#793 + companion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 3ccdd79)
The tag-fetch step adds an upstream remote to the checkout, so gh resolved the base repo to paradigmxyz/centaur and the release step failed with "tag has not been pushed" (c7e-0.1.110-2 needed a manual gh release create). GH_REPO pins resolution to this repository. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Chat SDK's event dispatch drops file events (file_shared,
file_change) before any callback fires, so nothing today can react to a
file being created or edited. Add a raw-body sidecar (the
channel_created / lateSlackFiles pattern) that, when enabled, creates a
durable workflow run via POST /api/workflows/runs for matching file
events.
Deployment-neutral and default-off: the target workflow name, event
types, channel allowlist, and files.info title-keyword filter all come
from env config (SLACKBOTV2_FILE_EVENT_*). The bot stays timer-free —
any settle/debounce behavior belongs in the target workflow, which can
sleep durably. Per-edit event storms collapse at spawn time via the
idempotency key slack-file-event:{workflow}:{file_id} (optionally
time-bucketed); the dispatch task never throws into the webhook path.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
centaur_cc_reader_documents_select (0048) admits only source = 'slack' rows, so an agent session searching company context never sees any other document in company_context_documents — including memory notes (source = 'c7e_memory', source_type = 'memory_note'), the facts a user explicitly asked the agent to remember. Verified live: the note ranks first for its own question under the writer role and is invisible to the reader (0 c7e_memory rows visible). 0051 re-creates the policy with a second arm: company-scoped memory notes are visible when centaur.slack_include_public is true — a principal allowed to read public Slack may read company-wide notes — and hidden otherwise, so a connection with no access settings still fails closed. Other c7e_memory document types and user-scoped notes stay hidden. The embeddings policy delegates to this table, so the vector search lane follows without a change. etl_context_rls seeds a company note, a user-scoped note, and a memory update, and pins: public-context readers see the company note; no-public readers do not; centaur_readonly (source <> 'slack') sees all three. Claude-Session: https://claude.ai/code/session_01NfpJ3U9pZHEpGD9nwN7c7h Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Rebase note (0.1.129): renumbered 0051 -> 0053 because upstream landed 0051_hermes_harness and 0052_session_execution_request. Prod databases that already applied this as version 51 must drop that _sqlx_migrations row before the new api-rs boots; the policy DDL is idempotent.
…lete (#43) Three gaps in the calendar surface, found while exercising the tool against a real calendar. No way to create a Meet. calendar_create_event built a plain event body and called events().insert() without conferenceDataVersion, so an agent could book the slot but not the meeting. Meet is minted through the conferencing subresource: the body carries a createRequest with a fresh requestId and the call must opt in with conferenceDataVersion=1, or Calendar drops conferenceData and returns an event with no link. Create and update now take `conference`, return `meet_link`, and re-fetch once when Calendar answers before the mint settles. On update the flag only adds a conference to an event that lacks one; leaving conferenceDataVersion at its 0 default elsewhere is what keeps an existing Meet intact on an ordinary update. Edits reached attendees as silence. calendar_update_event passed sendUpdates="all" only when add_attendees was set, so moving an event, renaming it, or changing where it happens notified nobody: attendees' copies moved, their invite emails did not. Verified against a real event -- the reschedule produced no mail at all. `notify` now defaults to notifying when the event has attendees and the edit is material: when, where, what it is called, who else is coming, or a Meet just added. Description-only edits stay quiet on purpose, so the notices that land keep meaning something. Materiality is deliberately narrow. The field tests are truthiness, matching how the function applies them -- it writes a field only when the value is truthy, so `location=""` edits nothing and must not send mail. Attendee notification keys off whether the merge actually appended somebody, not whether a list was passed, so re-inviting an existing guest stays quiet. That merge also now records the emails it appends, so a repeated address in one call is added once rather than duplicated. No way to cancel. calendar_delete_event is new; the tool could create and update events but never delete one. It notifies by default, since a cancellation nobody hears about just leaves the meeting on every calendar. Submitted upstream as paradigmxyz#1522; the calendar functions, CLI commands, and tests here are byte-identical to that submission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011nvUztTAb5Kvkb9PhCnCbT
When an agent intentionally produces no final reply (e.g. a subscribed
Slack thread where it chose not to respond), the Codex App Server renderer
synthesized EMPTY_FINAL_ANSWER_TEXT ("Execution completed, but no final
text was captured.") and Slack posted it as the assistant's reply. The
slim-fork cutover dropped the fork's prior suppression of empty
completions (see cartridge-gg/agent docs/slim-fork-cutover.md), regressing
this behavior.
Add an opt-in `suppressEmptyFinalAnswer` option to the shared
CodexAppServerRendererEventMapper (default preserves upstream behavior)
that skips the synthesized placeholder on a successful empty completion.
slackbotv2 opts in via rendererOptions/fallbackRendererOptions and skips
posting in the plain-text path when the captured text is empty and the run
was not interrupted. Genuine failures are unaffected: fail() emits its own
"Execution failed" text and an error, so true execution failures remain
visible in the thread.
Tests:
- packages/rendering: default synthesizes the placeholder; suppression
yields empty answerMarkdown; failures still surface "Execution failed".
- slackbotv2 emulate: a successful empty completion posts no placeholder;
a failed run with no final answer still shows the failure text.
joystick-gg
Bot
force-pushed
the
main
branch
from
September 4, 2026 22:01
5530336 to
d8b3766
Compare
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.
Problem
When an agent intentionally produces no final reply in a subscribed Slack thread, Centaur posted the literal placeholder
Execution completed, but no final text was captured.as the assistant's reply instead of staying silent (reported by tarrence).Root cause: the Codex App Server renderer (
packages/rendering/src/codex-app-server.ts) synthesizesEMPTY_FINAL_ANSWER_TEXTon any successful empty completion (ensureFinalAnswerText()inflush()), and slackbotv2 posts it. The slim-fork cutover dropped the fork's prior suppression of empty completions — seecartridge-gg/agentdocs/slim-fork-cutover.md(the cutover checklist explicitly notes the fork "no longer suppresses empty completions"), which regressed this behavior.Fix
suppressEmptyFinalAnswertoCodexAppServerRendererEventMapperOptions. Default is unchanged (upstream still synthesizes the placeholder); when set,ensureFinalAnswerText()skips it on a successful empty completion.rendererOptions/fallbackRendererOptions, and skip posting in the plain-text path when the captured text is empty and the run was not interrupted.Genuine failures stay visible:
fail()emits its ownExecution failed: …text plus anerror, which is not gated by the new option — so true execution failures still surface in the thread. Interrupted runs still renderExecution interrupted.Tests
packages/rendering/src/codex-app-server.test.ts: default synthesizes the placeholder;suppressEmptyFinalAnsweryields emptyanswerMarkdown; aturn.failedstill surfacesExecution failed: …with suppression on.services/slackbotv2/test/chat-sdk-emulate.test.ts: a successful completion with no final answer posts no placeholder (was previously asserted to post it); added a companion test proving a failed run with no final answer still shows the failure text.Validation
bun test— rendering (40 pass) and slackbotv2 (270 pass) suites green.tsc --noEmitclean for all changed files (pre-existingtest/slack-user.test.tspreconnecttype errors are unrelated and untouched).Note:
cartridge-gg/agentdocs/slim-fork-cutover.mdstill states the empty-completion placeholder is "expected"; that note is now stale and should be updated in a follow-up overlay change.Prompted by: tarrence