Skip to content

fix(steer): stop a steered Slack turn from posting "no response" and losing its answer - #655

Merged
lidge-jun merged 1 commit into
devfrom
codex/slack-steer-noresponse
Sep 9, 2026
Merged

fix(steer): stop a steered Slack turn from posting "no response" and losing its answer#655
lidge-jun merged 1 commit into
devfrom
codex/slack-steer-noresponse

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Problem

Steering a running turn in Slack posted the literal 응답 없음 ("no response") placeholder into the thread, and the follow-up run's real answer never arrived. The thread then looked dead even though the agent had finished the work.

Observed live on suji (cli: cursor, perCli.cursor.transport: print, midRunPolicy: steer). A print-transport run exposes neither steerTurnInBand nor replaceTurn, so every steer there takes the kill path:

[slack:in]  아니 15일이 다음주 화요일인데 월요일로 대답했잖아
[slack:in]  ㅎㅇ                                  <- steers the running turn
[jaw:kill]  reason=steer scope=jaw:slack:channel:C0BRBKVRWCV:... cli=cursor
[jaw:main]  exited code=143, text=0 chars         <- killed turn
[slack:out] 응답 없음                              <- posted to the thread
[jaw:main]  exited code=0, text=12 chars          <- real answer, never sent

Two independent defects produce that pair.

The killed turn resolved to the placeholder. Its collector was still waiting when the steer killed the process, so the terminal carried no text and collect.ts fell through to its t('tg.noResponse') default. That default exists for a turn that genuinely produced nothing, not for one deliberately retired mid-flight.

The follow-up answer had no route back. The kill path built its orchestrate meta from origin/scope/chatSessionId/requestId only, dropping target, chatId, remoteKey and replyViaTarget — so the follow-up terminal was unaddressed. Nothing else could recover it: ingress.ts returns early unless the disposition is new_run, so a steered submission leaves no dispatch waiter, and the standing target-reply forwarder delivered fromQueue turns exclusively.

Change

The collector watches steer_started for its own scope. When a different request retires the turn it resolves empty and marks the payload superseded; the Slack dispatch path stays silent on that marker rather than posting an empty body. Identity is checked on both scope and session, and a steer carrying the turn's own request id is not a supersession, so an unrelated conversation cannot retire this one. An ordinary empty terminal keeps its existing diagnostic.

The kill path now carries the owning conversation through to the follow-up run and tags it fromSteer. The Slack target-reply forwarder treats that as the same kind of orphan it already handles for queued turns, so the answer lands in the thread it came from.

Behaviour

before after
steered turn posts 응답 없음 posts nothing
follow-up answer never delivered delivered to the originating thread
genuinely empty turn posts 응답 없음 unchanged

Validation

  • npm run typecheck — clean.
  • The five existing steer suites (cursor-acp-steer, native-steer-handler, codex-app-steer, queue-steer-main-wait, retired-busy-steer) — 48/48 passing.
  • New tests/unit/steer-superseded-delivery.test.ts drives the real collector through supersession, an ordinary empty terminal, and a steer belonging to another scope.
  • Full npm test compared against an origin/dev baseline worktree: no new failures. Head shows 23 failures, baseline 22; the delta is two load-sensitive timing tests that pass in isolation, plus one file that fails identically on the untouched baseline (browser-web-ai-headed-policy, ENOENT on skills_ref/web-ai/SKILL.md — the submodule is not initialised in either worktree). All are environmental and in files this PR does not touch.

…losing its answer

Steering a running turn in Slack posted the literal "응답 없음" placeholder into
the thread, and the follow-up run's real answer never arrived. Observed on suji
(cli: cursor, transport: print, midRunPolicy: steer), where every steer takes the
kill path because a print-transport run exposes neither steerTurnInBand nor
replaceTurn:

    [slack:in]  아니 15일이 다음주 화요일인데 월요일로 대답했잖아
    [slack:in]  ㅎㅇ                                  <- steers the running turn
    [jaw:kill]  reason=steer cli=cursor
    [jaw:main]  exited code=143, text=0 chars         <- killed turn
    [slack:out] 응답 없음                              <- posted to the thread
    [jaw:main]  exited code=0, text=12 chars          <- real answer, never sent

Two independent defects produced that pair.

The killed turn resolved to the placeholder. Its collector was still waiting when
the steer killed the process, so the terminal carried no text and collect.ts fell
through to its t('tg.noResponse') default. That default is meant for a turn that
genuinely produced nothing, not for one deliberately retired mid-flight. The
collector now watches steer_started for its own scope and, when a different
request retires the turn, resolves empty and marks the payload superseded; the
Slack dispatch path stays silent on that marker instead of posting an empty body.
An ordinary empty terminal keeps the existing diagnostic.

The follow-up answer had no route back. The kill path built its orchestrate meta
from origin/scope/chatSessionId/requestId only, dropping target, chatId, remoteKey
and replyViaTarget, so the follow-up terminal was unaddressed. Nothing else could
recover it either: ingress returns early unless the disposition is 'new_run', so a
steered submission leaves no dispatch waiter, and the standing target-reply
forwarder delivered fromQueue turns exclusively. The kill path now carries the
owning conversation through to the follow-up run and marks it fromSteer, and the
forwarder treats that as the same kind of orphan it already handles for queued
turns.

Verified with npm run typecheck, the five existing steer suites (48 passing), and
a new regression that drives the real collector through supersession, an ordinary
empty terminal, and a steer belonging to another scope.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T04:27:11.257337Z 254b700 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 9adce7cd-604d-4682-87a6-ddc979e11184

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 254b700e35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agent/spawn.ts
Comment on lines +938 to +940
broadcast('steer_started', stripUndefined({ prompt: newPrompt, origin: source || 'web', scope: scopeKey,
sessionId: chatSessionId, target: meta?.target, chatId: meta?.chatId, requestId: meta?.requestId,
remoteKey: meta?.remoteKey, replyViaTarget: meta?.replyViaTarget, mode: 'kill-steer' }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mark the old turn superseded before awaiting exit settlement

This event is emitted only after waitForExitSettled, but the standard CLI exit handler resolves the old spawn promise before its settleExit callback runs; that lets the old pipeline broadcast orchestrate_done and the Slack collector post tg.noResponse before superseded can be set. Consequently ordinary kill-steer requests still exhibit the reported placeholder regression. Emit a separate retirement signal before the exit wait while retaining the exit-settle/salvage barrier for starting the follow-up.

AGENTS.md reference: AGENTS.md:L285-L289

Useful? React with 👍 / 👎.

Comment thread src/agent/spawn.ts
Comment on lines +949 to +952
const steerMeta = stripUndefined({ origin, scope: scopeKey, chatSessionId, requestId: meta?.requestId,
target: meta?.target, chatId: meta?.chatId, remoteKey: meta?.remoteKey,
replyViaTarget: meta?.replyViaTarget, _fromSteer: true,
_skipInsert: true, _steerContext: steerContext || undefined });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Propagate steer delivery through reset and continue terminals

The new _fromSteer marker is consumed only by orchestrate(), although the kill-steer branch routes reset prompts to orchestrateReset() and idle continue prompts to the direct terminal in orchestrateContinue(). Those terminals propagate only fromQueue, so Slack's new forwarder rejects their orchestrate_done; because ingress returned without a live waiter for the steered submission, users never receive Reset complete. or No pending work to continue.. Propagate fromSteer from both direct terminal paths as well.

AGENTS.md reference: AGENTS.md:L287-L289

Useful? React with 👍 / 👎.

Comment on lines +124 to +128
if (type === 'steer_started'
&& data['scope'] === binding.scope
&& (data['sessionId'] === undefined || data['sessionId'] === binding.chatSessionId)
&& (!requestId || data['requestId'] !== requestId)) {
superseded = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict supersession to kill-steer events

This condition also matches native-input and cancel-reprompt events, although those modes keep the existing collector as the owner of the replacement answer rather than creating an orphaned _fromSteer run. If such a native turn ends absent/empty or with no renderable diagnostic, the collector now returns empty with superseded: true, and Slack treats that as success without sending the prior no-response diagnostic; no standing forwarder can replace it. Require data.mode === 'kill-steer' when setting this flag.

AGENTS.md reference: AGENTS.md:L287-L288

Useful? React with 👍 / 👎.

requestId,
replyViaTarget,
...(fromQueue ? { fromQueue: true } : {}),
...(fromSteer ? { fromSteer: true } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Regenerate the documented line counts

Running the repository-required bash structure/verify-counts.sh on this commit fails because structure/str_func.md still records the pre-change sizes for src/agent/spawn.ts, src/orchestrator/pipeline.ts, src/orchestrator/collect.ts, and src/slack/bot.ts. Regenerate those derived counts with the documented --fix command and commit the synchronized architecture file.

AGENTS.md reference: AGENTS.md:L351-L354

Useful? React with 👍 / 👎.

@lidge-jun
lidge-jun merged commit dbe3418 into dev Sep 9, 2026
17 checks passed
@lidge-jun
lidge-jun deleted the codex/slack-steer-noresponse branch September 9, 2026 04:31
lidge-jun pushed a commit that referenced this pull request Sep 9, 2026
Resolves the #660 review before landing:
- Slack tool turns keep the default steer policy; only synthetic thread
  addresses force followup, so a second Slack message during a tool turn
  still steers instead of queueing (regression against #655).
- The A1 tool anchor now states that a grant exists only for a fresh print
  Cursor/Claude/Codex/Grok main turn, so the prompt stops advertising tools
  that 401 on Codex App, Pi, pooled native adapters and workers.
- A live DM whose conversations.info omits sharing fields no longer fails
  closed; the two-person membership and users.info workspace checks are what
  prove the DM.
- The operator credential is minted on first operator use instead of every
  boot, so an install that never uses operator mode holds no operator secret.

docs/slack-tools.md records the accepted deployment model for the residual
#646 limitations, the in-conversation scope of pin/reaction/bookmark creation,
and the requester-visible workspace roster.
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