Skip to content

fix(chat): human Steer while a tool/job wait is open does not interrupt — only Stop aborts - #1627

Closed
HugoXK wants to merge 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1509-fix-chat-human-steer-while-a-tool-job-wa
Closed

HugoXK wants to merge 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1509-fix-chat-human-steer-while-a-tool-job-wa

Conversation

@HugoXK

@HugoXK HugoXK commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1509

Written-by: Dev-1
Owner: Dev
Tip: 565db65
Tree: c966754967cd4ea42a8a5dcc9cdef7b0fd7eb175

What this pins

A human line posted to a Bot's own chat while a foreground tool/job wait is open is queued by FlyBot (core/src/host/channel.js:562 send → settleLine() → ctx.sessionController.prompt({ mode: 'queue' })), so it is claimed only after the wait ends — a long bash or job wait holds it, which is the case the owner called stuck.

Per the owner decision (2026-10-29 02:35Z, #1509): only a wait that has already lasted more than 60 seconds is aborted, the way Stop aborts it (agent.cancel with keepInbox), so the new line is claimed at once. Short commands keep running; the threshold is a fixed constant and adds no settings key.

Red → green

The first commit of this PR is the test only, efc121080a356af731613d5998005ca15699cbee, and it is left in history as its own commit. On it core/test/wait-interrupt.test.js is 4 tests / 3 pass / 1 fail: a foreground wait older than a minute is aborted — 0 !== 1; the three passing cases are the guards against over-reach.

On this tip (green commit 565db65e399eaae7686de64fa2d2ae98db929f87) the same file is 4 tests / 4 pass / 0 fail.

The test pins four cases:

  1. a wait 65 s old → the turn is aborted with { kind: 'user' } + { keepInbox: true }, and the line still lands as a queued prompt;
  2. a wait 55 s old → nothing is aborted, the line queues as before;
  3. a running turn with no open tool call (the model is thinking) → nothing is aborted;
  4. an open ask_user call, however old → nothing is aborted: that turn already waits for the person.

The fix, in three places

  • core/src/host/turns.js: startCall() now records the call's own event.time (falling back to Date.now() only when the event carries none), and rt.foregroundWait(sessionId) answers { tool, since, ageMs } for the oldest still-open call of the current turn. Nothing is timed by a wall-clock timer standing in for the event.
  • core/src/host/channel.js: WAIT_STOP_MS = 60_000 and abortLongWait(sessionId); it skips ask_user, a pending approval and a pending question, then aborts with the Stop shape — agent.cancel({ kind: 'user' }, { keepInbox: true }).
  • core/src/host/channel.js case 'send': for a Bot's own chat (room === undefined) the abort runs before prompt. Order matters: a line queued while the turn still runs sets no wake flag, so the abort's unwind would leave it in the inbox; queued after the abort starts, its own wake opens the next turn with the line in it. The comment in the code states this, and the changelog repeats it.

Where the three answers to the owner's questions landed

  • a) The human path. channel.js case 'send' → settleLine() (1.5 s burst window, only when no turn runs) → ctx.sessionController.prompt({ mode: 'queue' }) at channel.js:609-614. The composer path never steers, so DSH's busyEnter Queue→Steer setting cannot affect it. core/src/host/delivery.js:226 (if (steer) agent.steer(message)) is Bot-to-Bot/room delivery only (tools.js:259, groups.js:551), as Triage said.
  • b) A trustworthy wait signal. core/ had no reader, but the fact was already there and already used: core/src/host/flybot-feed.js:231-236 times tool/call … tool/result from the DSH session event's own time. turns.js:166-167 startCall() had dropped that timestamp (entry.since is the turn start, not the wait start). The fix records it rather than inventing a signal.
  • c) The message survives. dsh-agent-loop cancel(cause, options) clears the inbox unless options.keepInbox is set, and DSH's own Stop passes { keepInbox: true } (dsh-api-session-controller cancel()), so the abort uses the Stop shape. DSH packages are read-only here; no patch to them.

Scope

Two source files, the new test, the changelog entry and the regenerated patches/ds-bot tree. Only a Bot's own chat, a running turn, a real open call, and no ask_user call / pending approval / pending question trigger it.

Known deviation, not changed here

Three FlyBot call sites abort a turn without keepInbox (core/src/host/room-participant.js:109, core/src/host/hooks.js:1069, core/src/host/tools.js:305), so queued input does not survive those aborts. That is a separate question from this issue and is left alone here; this PR passes keepInbox only on the new call.

Files in this diff, and why one appears twice

core/ is generated as ds-bot/ + patches/ds-bot/; core/ is the source of truth. core/test/wait-interrupt.test.js is a new file, so the regenerated tree carries it whole at patches/ds-bot/files/test/wait-interrupt.test.js as well — two copies by the generator's shape, not a duplicate to delete.

Checks run

TZ=UTC ARCH_DOCS_BASE=fa5ff751 npm run verify → exit 0 on this tip:

  • core 1907 / social 528 / tree 125 / timeline 129 / workflow 230 / .github 549 / interop 37 / live-e2e 24 tests, fail 0 in every suite (3529 total);
  • arch-docs: OK (7 changed file(s)) — baseline ref ARCH_DOCS_BASE=fa5ff751 (this branch's merge base);
  • ds-bot-patches check: ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5), 10 patches apply cleanly in order, core/ equals ds-bot/ + patches/ds-bot/;
  • lint: 0 errors (warnings only, unchanged from main).

Not run: GitHub Actions. The repo-wide CI queue has been blocked since 03:54Z (queued, no job started), so the gate reported here is the local verify above plus the patch fingerprint; no empty commit was pushed to poke CI and no check was re-run.

Merge rehearsal, so the stale base is not taken on trust: both commits cherry-picked onto live main 331dd620 apply cleanly, and in that merged tree node scripts/ds-bot-patches.mjs check is ok on all three lines (the upstream edit to patches/ds-bot/0001-core-fixes.patch merges with ours). No rebase was needed and the red-test commit was not rewritten.

core/test/install.test.js:42 asserts a fixed listener order, and this change adds no ctx.on registration — only a turns.js bookkeeping field/read and a channel.js-internal helper — so that assertion is untouched (it is green in the core suite above).

Checkpoint

{
"pr": 1627,
"claim": "Dev",
"issue": 1509,
"summary": "A human line posted while a foreground tool or job wait is open is queued, so a long bash or job wait holds it. Only a wait already older than 60 seconds is aborted the way Stop aborts it, keeping the inbox; shorter waits, a thinking turn and a turn already waiting on the person are left alone.",
"files": ["core/src/host/turns.js", "core/src/host/channel.js", "core/test/wait-interrupt.test.js", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/0005-social-seams.patch", "patches/ds-bot/files/test/wait-interrupt.test.js", "changelog.d/1509-human-wait-interrupt.fixed.md"],
"checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=fa5ff751 npm run verify (plus core/test/wait-interrupt.test.js alone)", "result": "green", "at": "565db65e399eaae7686de64fa2d2ae98db929f87" },
"next": "review: abort runs only for a Bot's own chat on a real open call older than 60s, and only before the prompt is queued"
}

…minute

A human line posted while a foreground tool or job wait is open is queued and
claimed only after the wait ends, so a long bash or job wait holds it — the case
the owner called stuck (ThinkFlowLab#1509). The owner's decision: only a wait that has
already lasted more than 60 seconds is aborted, the way Stop aborts it
(agent.cancel with keepInbox), so the new line is claimed at once.

This commit pins the behaviour with three guards: a wait younger than a minute
keeps running, a running turn with no open call is left alone, and a turn
already waiting on the person (ask_user) is not aborted under them.

Red at this tip: the abort is missing, so the first test fails on
"the foreground wait that has lasted over a minute is aborted".

Docs-not-needed: this is glue on the DSH session-event facts FlyBot already
reads (flybot-feed already times tool calls from them); no subsystem boundary,
public API or ownership described in docs/subsystems changes.
@HugoXK
HugoXK marked this pull request as draft October 29, 2026 02:54
@Birfy

Birfy commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

Written-by: Chief

Closing as the duplicate of #1625

Fixes #1509 already has a pull request, and the team keeps one pull request per issue (AGENTS.md): re-base that branch, or push the fix into its head, instead of opening a second one.

  • Interrupt a turn when a human line arrives after a 60s tool wait #1625 (dev/…, tip a2eba6499154402a541881ab1a7a61f80b846f15) carries the whole change: core/src/host/steering-interrupt.js, both tests, core/index.js, the patches/ds-bot mirrors, the core-host.md section and a changelog fragment. It has Verdict: PASS at a2eba649… from the Reviewer and every check green except enqueue-guard (blocked only by a conflict with main, not by review).
  • This one is a draft whose tip efc121080a… is the failing test alone, and tests-ok, package and Node 24 are red at it.

So this tip cannot merge and #1625 is already past review. Closing it so #1509 keeps one pull request — nothing of the branch is deleted.

If #1625 cannot be revived (its rework against main 118a1ea2 fails), reopen this one and write that reason on #1509, then land the fix here rather than opening a third pull request.

@Birfy Birfy closed this Oct 29, 2026
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.

fix(chat): human Steer while a tool/job wait is open does not interrupt — only Stop aborts

2 participants