Skip to content

fix(core): list_bots marks a Bot whose send waits on the owner - #646

Merged
Birfy merged 2 commits into
ThinkFlowLab:mainfrom
rahul05ranjan:fix/list-bots-approval-tag
Oct 22, 2026
Merged

Birfy merged 2 commits into
ThinkFlowLab:mainfrom
rahul05ranjan:fix/list-bots-approval-tag

Conversation

@rahul05ranjan

@rahul05ranjan rahul05ranjan commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #587

What changed

A Bot whose outbound message is held for the owner's approval looked idle to a
coordinator: \list_bots\ showed no sign that the Bot is blocked on a decision.
It now tags the waiting Bot with a count — \Writer (Write, 1 to approve)\ — read
from that Bot's pending held sends in \state.flybot.approvals. The tag is a
count only (never the held message's text) and clears once the owner decides.
Docs note it in \docs/subsystems/core-host.md\ (Team state and access) and
\docs/subsystems/plugin-runtime.md\ (held-send surface); changelog fragment
\changelog.d/587-list-bots-approval-tag.fixed.md. Core only, no new state.

Regenerated \patches/ds-bot/\ using
ode scripts/ds-bot-patches.mjs generate\ so
\

@rahul05ranjan
rahul05ranjan force-pushed the fix/list-bots-approval-tag branch from bbba2f1 to d0f1aef Compare October 21, 2026 15:17
@github-actions github-actions Bot added the risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff label Oct 21, 2026
@Birfy

Birfy commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Verdict: INC at d0f1aef
Written by: Reviewer. Posted by: Chief (proxy — Reviewer's GitHub write channel is down; nothing below is edited).

Tip SHA: d0f1aef

Ran:

  • node --import ./core/test/harness-resolve.mjs --test core/test/flybot.test.js → 40/40 pass
  • mutation: revert the core/src/host/tools.js hunk → 39/40, red = "list_bots marks a Bot whose send waits for approval #587: list_bots marks a Bot whose send waits on the owner with a count, never the text"
  • code read: state.flybot.approvals[id] carries { status, request.from }, the tag counts only status === 'pending' per sender Bot, emits a count and never the held text, and disappears once the owner decides (the test asserts both the count and the absence of the message text)
  • CI at the head: no run has completed. GET commits/d0f1aef7/check-runs → total 0; combined status "pending", 0 statuses; the three workflows exist but every one is action_required (Risk tier, Test, Review gate) — a first-time contributor's runs waiting for maintainer approval

Not run: a real approval card in a live session; npm run verify end to end

INC for the CI gate, not for the code. The change itself looks right to me — the approval record shape matches (status/request.from), the tag is a count only, it clears after the decision, and the docs sentence is accurate. Nothing in it needs rework.

Two process facts that must be fixed before any verdict can allow a merge:

  1. No CI has run at the head. d0f1aef7 has zero completed checks; the three runs are action_required. Per the gate, a tip with no green CI cannot pass.
  2. The PR body names the wrong commit. It says Tip SHA: bbba2f1f, which is not the head d0f1aef7, and bbba2f1f has no workflow runs at all. A conclusion written against the body's sha would name a commit that was never tested; I judged the head, as instructed.

What clears it: a maintainer-approved run that finishes green at d0f1aef7, plus the body corrected to that sha (or a force-push that makes them agree). Then I re-review and, if the code is unchanged, it should pass on one look.

@Birfy

Birfy commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Verdict: INC at d0f1aef
Written by: Reviewer. Posted by: Chief (proxy — Reviewer's GitHub write channel is down; the block below is verbatim).

This supersedes the reason given in the INC at this same sha earlier today (that one said no CI had run). The verdict is unchanged — INC — but the cause is different and now has evidence: after a maintainer re-ran the three action_required workflows at this head, package came back failure, on a step that is green on main. Read only this comment for the reason; the earlier one is kept for the record, not as the current explanation.


Tip SHA: d0f1aef

Ran:

  • checked the re-run of the three action_required workflows at this head: risk tier success, reviewed tip success, package failure (job package, step 4 "ds-bot/ matches the pin, patches apply, core/ is ds-bot/ + patches (Decouple the social and tree plugins from ds-bot so the core can track upstream #34)"), the four Test jobs still in_progress when I looked
  • node --import ./core/test/harness-resolve.mjs --test core/test/flybot.test.js → 40/40 pass locally (so the unit work itself is fine; the red is not in the tests)
  • reproduced the cause from the tree without the CI log: core/src/host/tools.js contains heldSends (3 hits), patches/ds-bot/0002-flybot-api-host.patch, which is what generates that file, contains it 0 times; core/test/flybot.test.js differs from its whole-file mirror patches/ds-bot/files/test/flybot.test.js. Both edits exist only in the generated core/, so ds-bot-patches.mjs check rebuilds core/ without them and the comparison fails
  • compared against main fe79de66: its package run is completed/success, so this is not an environment failure

Not run: a green Test run (it had not finished); npm run verify end to end

Judged head d0f1aef7; the PR body still says Tip SHA: bbba2f1f, which is not the head and has no workflow runs at all.

Why INC, and it is not the CI flakiness you were worried about: the PR edits the generated core/ tree without regenerating the patches/ds-bot/ that it is built from. tools.js is produced by patches/ds-bot/0002-flybot-api-host.patch; the new #587 test belongs in the whole-file mirror patches/ds-bot/files/test/flybot.test.js. Neither was touched, so ds-bot-patches.mjs check — the first step of the package job, and a genuine repo invariant (#34) — fails on this head while it passes on main. That is a real defect of the change set, not an environmental red and not action_required.

The code itself still looks right to me: shape check against state.flybot.approvals[id] (status, request.from), count only, never the held text, tag clears after the decision, docs sentence accurate. Only the generated-tree bookkeeping is missing.

How to clear it:

  1. In a checkout with git, run node scripts/ds-bot-patches.mjs generate so patches/ds-bot/0002-flybot-api-host.patch gains the tools.js hunk and patches/ds-bot/files/test/flybot.test.js becomes byte-identical to core/test/flybot.test.js; commit those with core/, then node scripts/ds-bot-patches.mjs check must exit 0.
  2. Correct the body's Tip SHA: to the head (or push so they agree).
  3. Push a new commit — I re-review at the new head; a green Test plus a green package at that same sha is what the gate needs. A green Test at d0f1aef7 would not be enough on its own, because package is red there.

@Birfy Birfy added agent-pr Pull request opened by an agent area:automation Scheduled tasks and automation area:ui Web/desktop UI and copy bug Something isn't working labels Oct 21, 2026
@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Maintainer note — the package failure is mechanical: core/src/host/tools.js and
core/test/flybot.test.js changed without regenerating patches/ds-bot/, so step 4
(node scripts/ds-bot-patches.mjs check) reports both as differing from ds-bot/ + patches.

What I verified locally at d0f1aef7 (a clean copy of this head):

  • node scripts/ds-bot-patches.mjs check → exactly those two files, the same message the package job prints.
  • node scripts/ds-bot-patches.mjs generate → only two files change:
    patches/ds-bot/0002-flybot-api-host.patch (the 8 added lines for the held-send count) and
    patches/ds-bot/files/test/flybot.test.js (the whole-file copy that carries the new test). check then passes.
  • node --test core/test/flybot.test.js → 40/40. With the to approve tag line removed from
    core/src/host/tools.js and nothing else changed → 39/40, red exactly on
    #587: list_bots marks a Bot whose send waits on the owner with a count, never the text.

So please commit the regenerated files and push:

node scripts/ds-bot-patches.mjs generate
node scripts/ds-bot-patches.mjs check
git commit -am 'Regenerate patches/ds-bot for the list_bots approval tag'
git push

I could not push it for you: the Birfy account has pull-only access to the fork
(permissions.push: false on rahul05ranjan/FlyBot, and POST /…/git/blobs answers 404), so the branch
is yours to update.

Two smaller things on this pull request: the body's Tip SHA: bbba2f1f does not name any tip of this
branch (the current head is d0f1aef7) — the body is yours, I am not editing it; and there are two
Verdict: INC comments at d0f1aef7, which a new commit will supersede anyway.

@rahul05ranjan

Copy link
Copy Markdown
Contributor Author

Regenerated \patches/ds-bot/\ using
ode scripts/ds-bot-patches.mjs generate\ and verified that
ode scripts/ds-bot-patches.mjs check\ exits 0. Pushed new commit \

@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Takeover note.

This branch no longer merges: its base is fe79de66, main is far ahead, and GitHub reports it conflicted. Your last push (0f9b144f) also has no check runs, so the queue cannot evaluate it.

Your fork is read-only to us, so rather than asking you to resolve the conflicts we will open a successor pull request that carries your seven files rebased onto current main, with Co-authored-by: rahul05ranjan and a body that points back here. Once it has passed review and merged, this one will be closed with a link to it.

If you would rather push the rebase yourself, say so here and we will wait instead — nothing is lost either way, and the changelog fragment (changelog.d/587-list-bots-approval-tag.fixed.md) and the regenerated patches/ds-bot/ files come along unchanged.

rahul05ranjan and others added 2 commits October 22, 2026 16:17
A Bot whose outbound message is held for the owner's approval looked
idle to a coordinator reading the roster. list_bots now tags such a
Bot with a count ("Writer (Write, 1 to approve)"), a count only and
never the held message's text, and the tag clears once the owner
decides.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
Signed-off-by: rahul05ranjan <rahulsingh.ranjan06@gmail.com>
Signed-off-by: rahul05ranjan <rahulsingh.ranjan06@gmail.com>
@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Correction to my note above: this branch is editable by us — the pull request has maintainer_can_modify: true, and a ref update against fix/list-bots-approval-tag is accepted.

So no successor pull request is needed. We will push the rebase onto this branch instead: one commit whose parent is current main, carrying your seven files exactly as you wrote them, with Co-authored-by: rahul05ranjan and patches/ds-bot/ regenerated so node scripts/ds-bot-patches.mjs check passes. Your authorship, this thread and Fixes #587 all stay where they are.

If you would rather own the rebase yourself, say so here before the push lands and we will hand it back.

@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Successor opened: #752 (fix/587-list-bots-approved-tag, tip 8fd25e65, base current main). It carries your change, not your branch: tools.js gets the same heldSends block and ${count} to approve tag, and core/test/flybot.test.js the same #587 case, Co-authored-by: rahul05ranjan in the commit; docs and the changelog fragment are yours too.

One thing worth knowing rather than redoing: the branch could not be merged as-is even after a rebase, because it was cut before several main changes — a wholesale copy would have reverted #645/#699 (create_bot wording), #712 (the bot.created announcements and the emptyTeam harness helper) and a large part of both docs files. So only your change was re-applied on top of current main; the diff against main is your #587 change and nothing else.

Evidence at the tip: test/flybot.test.js 42/42; full core suite 1239/1236/3, the same three environment-limited failures as pristine main (1238/1235/3); ds-bot-patches check exits 0; arch-docs OK on 7 changed files; lint 0 errors. Reverting tools.js to main and keeping the new test turns exactly that one case red (41/42).

This stays open until #752 lands; it is then closed pointing there, and #587 is the issue both of them fix.

Written-by: Dev3

@rahul05ranjan
rahul05ranjan force-pushed the fix/list-bots-approval-tag branch from 0f9b144 to 528313b Compare October 22, 2026 10:55
@Birfy
Birfy added this pull request to the merge queue Oct 22, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 22, 2026
@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Status update, and a correction to my two notes above.

You already did what we were about to push: 528313bb is rebased onto d5fefe01 and regenerates patches/ds-bot/ itself, so the branch now carries exactly the seven files (tools.js, the #587 case, both docs, the changelog fragment and the stored mirror), 2 commits ahead of main, 0 behind, mergeable: true, and 9/9 checks green.

So I pushed nothing, and the successor pull request is closed as a duplicate — this pull request is the one to merge. Nothing else is needed from you.

I also checked the merge against the current main (430a99a8, three commits after your rebase; only docs/subsystems/plugin-runtime.md is touched by both sides and the two edits are on different lines):

$ node scripts/ds-bot-patches.mjs check
ok   ds-bot/ = Fei2411/DeepSeek-Bot@2b10c70 (tree 5834c00, v0.1.5)
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/

$ node --import ./test/harness-resolve.mjs --test test/flybot.test.js
# tests 42   # pass 42   # fail 0        (includes the new #587 case)

$ node --import ./test/harness-resolve.mjs --test --test-concurrency=4 "test/*.test.js"
# tests 1243  # pass 1241  # fail 2      (pristine main, same harness: 1242/1240/2)

The two reds (composer-label, read-page) are the known environment ones and are the same on pristine main. Reviewer judges at 528313bb.

@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 528313b
Written-by: Reviewer

The rebase rebuilt the change on current main, the tag is pinned, and nothing about the held message reaches the roster: the test holds a send whose text is secret plan, asserts the roster line for the waiting Bot and that the text never appears, then approves and asserts the tag is gone. Two INCs older than this tip (d0f1aef7) are superseded by this one.

Checked beyond the test: the count reads only status === 'pending' records, so a superseded hold (a record kept with next after a later rule held again, which decide() leaves as approved) is not counted twice; the tag is appended to the existing comma tags, so the roster line keeps its old shape for every Bot with nothing waiting; and the change keeps request.from only as a key — no text, no recipient, no reason.

CI at this tip is 9/9 green, mergeable: clean, and patches/ds-bot/ was regenerated for this rebase (0002 plus files/test/flybot.test.js); the package job — which runs scripts/ds-bot-patches.mjs check — is green on this head.

Ran:

  • cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/flybot.test.js → 42 tests / 42 pass / 0 fail
  • cut the ${heldSends[bot.id]} to approve fragment from the tags array → 41/42, the only red being #587: list_bots marks a Bot whose send waits on the owner with a count, never the text; restored → 42/42

Not run: the full core suite and ds-bot-patches check locally (no git/esbuild here); the green package job covers the latter at this tip.

@Birfy
Birfy added this pull request to the merge queue Oct 22, 2026
Merged via the queue into ThinkFlowLab:main with commit 2053dfb Oct 22, 2026
9 checks passed
@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 528313b
Written-by: Reviewer2

Ran (head 528313b tarball over base d5fefe0; Node 22.23.3; DSH_HARNESS=/opt/flybot):

  • node --import ./core/test/harness-resolve.mjs --test core/test/flybot.test.js → 42/42 pass
  • reverted core/src/host/tools.js to its base copy (the whole source change) and re-ran the same file → 41/42, the only red is #587: list_bots marks a Bot whose send waits on the owner with a count, never the text; restored the file → 42/42
  • node --import ./core/test/harness-resolve.mjs --test --test-concurrency=4 "test/*.test.js" → tip 1239 tests / 1236 pass / 3 fail; pristine base 1238 / 1235 / 3, the same three (empty diff passes, composer-label, read-page)
  • cmp core/test/flybot.test.js patches/ds-bot/files/test/flybot.test.js → identical, and patches/ds-bot/0002-flybot-api-host.patch carries the heldSends hunk

What I checked:

  • CI at 528313b: 9/9 completed and green (tests-ok, package, plugins (tree|social|timeline), ubuntu Node 24, merge-gate, reviewed tip, risk tier); mergeable_state: clean; no hold
  • record shape: state.flybot.approvals[id] is written with status: 'pending' and request.from (core/src/host/hooks.js:403-407), so the count keys the waiting Bot correctly
  • the tag counts only that Bot's pending Holds, prints a count (1 to approve) and never the held text, and leaves no tag after decide()
  • core-host.md and plugin-runtime.md say what the code does; changelog.d/587-list-bots-approval-tag.fixed.md is one honest line; the body has Fixes #587

Not run: a live approval card in a real session; node scripts/ds-bot-patches.mjs check locally (no git in this container — the green package job at this tip is that check)

Non-blocking:

  • The body is cut mid-sentence ("…ds-bot-patches.mjs generate so \") and carries no Tip SHA: line; it is the author's text, and on squash it becomes the commit message.
  • The count is viewer-independent: a Bot of another owner sitting in a shared room shows its pending count, while flybot/approvals narrows to the actor's own holds. Count only, never text, and coordination is the point of list_bots marks a Bot whose send waits for approval #587 — noted, not blocking.

Gate note: this report is signed Reviewer2, which the writers list of .github/agent/review-policy.json (read from the base commit at queue time) does not name yet, so merge-gate will refuse it until that one-line T3 change lands. Once the list names Reviewer2, this same PASS at this tip counts — no re-review and no second conclusion here.

@Birfy

Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Correction from Reviewer2: my PASS on this tip crossed Reviewer’s, and I should have caught it. Reviewer posted Verdict: PASS at 528313bb at 11:11:06Z, it was enqueued at 11:11:20Z, and the queue merged it at 11:13:24Z (2053dfb5). I wrote mine at 11:14:43Z without re-reading the thread immediately before posting, so this tip carries two conclusions with the same verdict; that is a process error on my side, not a second judgement of the change. The merge is Reviewer’s and is unaffected: same tip, same evidence, nothing in the code, docs or gate changes. My comment stays only as an independent re-run of the red evidence.

github-merge-queue Bot pushed a commit that referenced this pull request Oct 22, 2026
) (#759)

Written-by: Dev2

Fixes #727

Tip SHA: fe42f4c

**Takeover of #728.** The author's branch cannot be pushed from here. A
no-op ref update on `HugoXK/FlyBot` answers 200, which looks like push
access, but creating a blob in that repository is 404 — the probe was a
false positive, and the same holds for `rahul05ranjan/FlyBot` on #646.
So this branch carries the same change (the seven files of #728, byte
for byte) on a repository we own, with the review's two missing cases
added and the pickup case fixed; `Co-authored-by: HugoXK` is in the
commit. The author's PR stays the record of the review.

## Reproduce

A turn that **finished** while FlyBot was not listening was replayed on
the next start. The note was kept for every end that was not a local
`completed` one, and the restart resumed it: a Bot that had already
answered was asked again ("restarted while you were working"), and the
group got a second copy. Two situations reach it — DSH recording a
restart as an `interrupted` end, and a turn whose end never reached the
plugin while a **new** turn already started on that Session.

Reproduction is the durable log the fix reads:
`test/follow-through.test.js` starts a turn, writes the `turn/end` that
DSH kept (or none at all), restarts the plugin, and asserts the Session
is **not** asked again. On main the note survives and the turn is
resumed.

## What changed

- `core/src/host/resume.js`: an `interrupted` end keeps the note (a
genuinely cut turn is still owed a pickup); a `turn/start` this process
sees proves the noted turn ended, because a turn cannot start until the
previous one ends; and a note whose turn the durable log already shows
as finished (`endedNormally`, bounded by the noted `turn`) is dropped
instead of resumed.
- `core/test/follow-through.test.js`: the four behaviour changes are
pinned. Two of the cases come from the review on #728 (`6337505721`): *a
live interrupted end does not drop the note of a turn still owed a
pickup*, and *a turn/start this process sees proves the noted turn
ended, even when the log cannot*. The third — *a pickup that fails keeps
its note* — reads `inflight.json` only after the host stops, which waits
for the note writes, so it reads the last write rather than racing it:
the merge queue caught this test at `64c4217b95` reading the file
between the clearing write and the restoring one. The fourth (a
`completed` end reached only through the durable log is not replayed)
was already pinned on #728.
- A failed pickup no longer drops its note at all: it used to be removed
just before the delivery and put back when that failed, so the file
showed the note gone for the length of a write and a crash in that
window lost it. The note now stays until the pickup is handed over, and
only a delivered pickup drops it — and only while it is still the note
that was picked up (`current === record`), so a resumed turn that has
written its own is left alone. That makes a pickup at-least-once: a
crash between "handed over" and the clearing write repeats it once, a
repeated turn the user can see instead of work the user was told about
going silently dead. The trade is written down in
`docs/subsystems/core-host.md`.
- A message sent again is noted once. The notes are what a restart still
owes, so a resend delivers the `exchangeId` they already hold — but the
delivery wrapper noted it a second time and `forgetQueued` dropped only
the first entry, so a turn taking the message up left one copy on disk
and the start after that sent the same message again. On the previous
tip `inflight.json` held 1 entry for the exchange where `main` holds 0;
`resent` also bypasses the 60 s Bot de-duplication and the notes are
kept for 24 h (`QUEUED_KEPT_MS`), so every restart in that window
re-answered the message — "sent again once" did not survive a restart.
Now an exchange the notes already hold is not noted twice, and a turn
taking a message up forgets every note of that exchange (a Session that
moved to a new part can have it noted under two Session ids).
- `core/test/follow-through.test.js` now observes **inside** the pickup
rather than after it: *a pickup that fails keeps its note* holds the
delivery open and reads `inflight.json` at that moment (a pickup that
clears first has an empty file there), *a resumed turn that writes its
own note keeps it* starts the resumed turn during the pickup and checks
that note survives the hand-over, *stop waits for the note it just
wrote* stops without waiting for the write so the cleanup that returns
`writing` is what puts it on disk, and *a message sent again is
forgotten everywhere once a turn takes it up* resends a noted message,
takes it up, reads the file and starts a third time (nothing may be
sent).
- `docs/subsystems/core-host.md` describes the rule, the at-least-once
window and the one-note-per-exchange rule;
`changelog.d/resume-finished-turn.fixed.md` and
`changelog.d/resume-note-kept.fixed.md` are the two entries #728
carried; `patches/ds-bot/` regenerated from `core/` on the current base.

## What I ran

At this tip `fe42f4cc581e7e1df554ec227ec61fefb082c37d` (base `0b8aac84`,
7 files, +710/−35):

- On CI's own pair — Node 24.10.0 and DSH `0.2.0-rc.2` installed from
npm — `node --import ./test/harness-resolve.mjs --test
--test-concurrency=6 test/follow-through.test.js` → **17 tests, 17 pass,
0 fail**, three runs in a row; `test/turn-origin.test.js` 4/4,
`test/history.test.js` 7/7, `test/bot-messages.test.js` 20/20 on the
same tree.
- The whole `core` suite on that pair: **1261 tests, 1258 pass, 3 fail**
— the three this box also fails on `main` (`empty diff passes`,
`test/composer-label.test.js`, `test/read-page.test.js`, all
image/client environment).
- The patch series after regenerating `core/src/host/resume.js` and
`core/test/follow-through.test.js`: applying `patches/ds-bot/files/` +
all ten patches to `Fei2411/DeepSeek-Bot@2b10c70` reproduces `core/`
byte for byte (111 patched files; only this box's local `node_modules`
is absent).

## Merge queue: `Test` red at `64c4217b95` (and `ecbb19b90f`)

Both queue entries failed `core/test/follow-through.test.js` → *a pickup
that fails keeps its note for the next start*: `the failed pickup is
still noted`, `actual: undefined`. The PR page was 9/9 green because it
never merges the tip with main.

The run's log has `ds-bot: picking up the turn of a failed: the Session
is not ready yet` right before the assertion, so the pickup did fail and
the restore did run — but a fixed number of `setImmediate` rounds is not
a write barrier: on the queue's disk the file read back without the
note, with the clearing write landed and the restoring one still behind
it. That window was real, not only a test artifact: between removing the
note and putting it back the file has no note at all, so a crash there
loses the work the pickup was about to hand over.

Both halves are fixed on this tip: the note is no longer removed before
a pickup (`core/src/host/resume.js`), and the case reads `inflight.json`
after `second.stop()`, which waits for the note writes
(`core/test/follow-through.test.js`).

## Red without the source change

Six cuts on `core/src/host/resume.js`, each restored afterwards
(**17/17** again after each):

- The pickup loop back to the old order (`update(sessionId, () =>
undefined)` before `resumeTurn`) → *a pickup that fails keeps its note*
is red on `the note is on disk while the pickup is in flight`: the
clearing write lands while the delivery is held open, so the file is
empty there. This is the cut that was **green** at `e4f7da66`.
- The identity guard dropped (`update(sessionId, () => undefined)` on
success) → *a resumed turn that writes its own note keeps it* is red on
`the note the resumed turn wrote is the one that stays`.
- The cleanup no longer returning `writing` → *stop waits for the note
it just wrote* is red on `the note of the running turn is on disk after
stop` (`a resumed turn that writes its own note keeps it` reads the file
after `stop()` too, so it goes red with it).
- The double note back (wrapper notes every delivery, `forgetQueued`
drops only the first entry) → *a message sent again is forgotten
everywhere once a turn takes it up* is red on `the exchange a turn took
up is still noted on disk`. This is the cut for this tip.
- Cut `|| reason?.kind === 'interrupted'` → **16/17**, the only red is
*a live interrupted end does not drop the note of a turn still owed a
pickup*.
- Cut `|| first === 'turn/start'` → **16/17**, the only red is *a
turn/start this process sees proves the noted turn ended…*.

## Not run

- `npm run lint` (oxlint) — this box has no oxlint (`npm run lint` exits
`no oxlint found`); CI's `package` job runs it at this tip.
- `node scripts/ds-bot-patches.mjs check` — it shells out to `git`
(`spawnSync git ENOENT` on this box); the series was verified by
applying `patches/ds-bot/files/` + all ten patches and comparing with
`core/` instead, and CI runs the real check.
- `arch-docs` — needs a git checkout for its diff (`cannot diff against
origin/main` here); `docs/subsystems/core-host.md` is in this diff, and
CI's `package` job runs it.
- `build-client` / `client.js`: no client file changes.
- `npm run verify` end to end.

## Risk

**T2** (`core/src/host/**` is host code in
`.github/agent/risk-rules.json`). The change is narrow: it decides
*whether* to resume a noted turn, not how a turn runs. It can make
FlyBot resume **less** — that is the fix — and the direction of a miss
is safe by construction: an end we cannot prove is treated as "still
owed a pickup", the pre-fix behaviour. What it must not do is lose a
genuinely cut turn, and that is the second case above (a durable
`interrupted` end still resumes).

Two inherited notes from the review that are not addressed here, both
non-blocking: `endedNormally` asks `sessionController.page` for 200
messages bounded by the noted seq, so a very long Session could fall
outside the window and be resumed (pre-fix behaviour, safe direction),
and `record.turn` is only written when the event carries a turn number,
which is also the fallback for a DSH build without it.
Birfy added a commit that referenced this pull request Oct 23, 2026
)

Written-by: Dev

Tip SHA: 517be9b

Fixes #767

## What changed

- `.github/scripts/pass-label.mjs`: a PASS whose `Written-by:` names
someone outside `writers` now leaves **one comment per tip** naming the
author, the tip and the file to edit (`.github/agent/review-policy.json`
→ `writers`), carrying the marker `<!-- pass-label: writer-not-listed
--> tip:<sha>` for idempotency. New pure functions: `strangerPassOf()`
(the refused PASS as a value of its own), `strangerCommentBody()`,
`planStrangerComment()` and `planCommentRun()` (the whole decision for
one `--comment` run: label, note, or nothing); `main()` only executes
that decision.
- `.github/scripts/pass-label.test.mjs`: five cases — a stranger's PASS
leaves one note naming author, tip and file while still adding no label;
the note is idempotent per tip and a moved tip may speak once; the same
commit written as a 12-character tip and then as a 40-character one is
one note, not two; everything that is not a stranger's PASS stays silent
(a listed writer, an INC, a passer-by, a body with no `Written-by:`, a
correction); one run picks label, note or silence, and the thread is
read only when a stranger's PASS is on the table.
- `changelog.d/767-pass-label-stranger.fixed.md`.

## Why

`review-gate` refuses the conclusion and `merge-gate` refuses the
enqueue, both naming the defect — but the reviewer who wrote the PASS is
told nothing, and nobody enqueues a T2/T3 pull request that "has a
PASS". That is exactly what happened to #646: Reviewer2's independent
PASS was discarded and the pull request sat with every check green.
`planComment()` already made this exact decision and threw it away as `{
add: false, reason: 'not a PASS by a writer the policy lists' }`, so the
loud failure belongs in the job that owns the answer.

The note does not claim the pull request cannot merge: a listed writer's
PASS still can. It says the conclusion does not count and names the one
edit that makes the next one count, including that the policy file is
T3. No label is added or removed, `reviewed tip` stays report-only (the
workflow's `continue-on-error` is untouched), `review-policy.json` is
not edited, and `tests-ok`'s `needs` are unchanged.

## What I ran

At this tip (base `fd205c40`, Node 24.10.0, `TZ=UTC`), in
`.github/scripts`:

- `node --test pass-label.test.mjs` → **11/11**.
- `node --test pass-label.test.mjs review-gate.test.mjs
merge-gate.test.mjs enqueue-guard.test.mjs` → **104/104** (run in a tree
that also carries `main`'s `.github/workflows`, `.github/agent` and
`AGENTS.md`, which those cases read).
- `node .github/scripts/changelog.mjs check` → well formed; `node
.github/scripts/testing-layers.mjs --check` → 315 test files in 12
workflows, all classified.

Red without the change, each cut alone at this tip:

- the note path returns `write: 'none'` instead of the comment →
**10/11**, only red *one run picks the label, the note, or silence — and
never both*.
- the marker check removed from `planStrangerComment()` → **9/11**, red
*the note is idempotent per tip, and a moved tip may speak once* and
that same run case.
- a listed writer treated as a stranger (`if (listed.has(...)) return
null` removed) → **10/11**, only red *everything that is not a stranger
PASS stays silent*.
- the marker's tip compared as a whole string instead of through
`namesHead()` → **10/11**, only red *one note per commit, whether the
tip is written 12 characters or 40*.

Not run on this box: `npm run lint` (no oxlint here), the whole
`.github/scripts` suite (its other cases read `core/`, `docs/` and
`CHANGELOG.md`, which this working tree does not carry — I ran the four
files above), smoke and Desktop. CI runs them at this tip; the
repository's Actions are currently blocked by the account billing lock,
so this tip will be re-run once that is lifted.

## Risk

**T1** (`.github/scripts/**` only — a comment from a job that already
holds `issues: write`). The label path is unchanged; the new path writes
one comment, never a label, and only for a PASS that the gates already
refuse.
@Birfy Birfy removed the agent-pr Pull request opened by an agent label Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:automation Scheduled tasks and automation area:ui Web/desktop UI and copy bug Something isn't working risk:T2 Risk tier T2 (host/plugin code or an unnamed path), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

list_bots marks a Bot whose send waits for approval

2 participants