From 1e50a6e50bfb880cf20a983ef62b0578e7905781 Mon Sep 17 00:00:00 2001 From: Day Dreamer <87640074+HugoXK@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:42:05 +0800 Subject: [PATCH] chore: regenerate client bundle and ds-bot patches pnpm-free path: client.js from src/client/inbox.js, and the 0008 bundle, per the generated-file contract. --- .../2534-inbox-stale-failed-card.fixed.md | 1 + core/client.js | 2 ++ core/src/client/inbox.js | 4 ++++ core/test/inbox-workflow-failure.test.js | 21 +++++++++++++++++++ docs/subsystems/core-client.md | 2 +- patches/ds-bot/0008-client-bundle.patch | 4 +++- patches/ds-bot/files/src/client/inbox.js | 4 ++++ .../files/test/inbox-workflow-failure.test.js | 21 +++++++++++++++++++ 8 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2534-inbox-stale-failed-card.fixed.md diff --git a/changelog.d/2534-inbox-stale-failed-card.fixed.md b/changelog.d/2534-inbox-stale-failed-card.fixed.md new file mode 100644 index 000000000..8184bbe76 --- /dev/null +++ b/changelog.d/2534-inbox-stale-failed-card.fixed.md @@ -0,0 +1 @@ +A workflow task that failed and was then run again to a green finish no longer keeps its **Failed** card in the Inbox, or its place in the sidebar's *Needs you* count, for the whole seven-day window: the success retires the card ([#2534](https://github.com/ThinkFlowLab/FlyBot/issues/2534)). diff --git a/core/client.js b/core/client.js index 0302a97ec..aa2d7c99e 100644 --- a/core/client.js +++ b/core/client.js @@ -9037,6 +9037,8 @@ function inboxModel({ roster, work = {}, tasks = [], sessions, workflows = [], n } if (latest === void 0) continue; const at = endedAt(latest); + const taskId = latest.task?.id; + if (taskId !== void 0 && [...graph.runs ?? [], ...graph.history ?? []].some((run) => run?.status === "done" && run.task?.id === taskId && endedAt(run) > at)) continue; const name = typeof graph.name === "string" && graph.name.trim() !== "" ? graph.name.trim() : String(graph.id ?? t("A workflow")); const nodeName = typeof latest.failure?.nodeName === "string" ? latest.failure.nodeName.trim() : ""; const reason = typeof latest.failure?.text === "string" && latest.failure.text !== "" ? latest.failure.text : typeof latest.failure?.code === "string" ? latest.failure.code : ""; diff --git a/core/src/client/inbox.js b/core/src/client/inbox.js index eea25aebb..de1d40793 100644 --- a/core/src/client/inbox.js +++ b/core/src/client/inbox.js @@ -321,6 +321,10 @@ export function inboxModel({ roster, work = {}, tasks = [], sessions, workflows } if (latest === undefined) continue const at = endedAt(latest) + // A task re-run that ended green answers its failure (#P-599): the success is in the same + // per-graph history under the same `task.id`, so no Failed card outlives the work. + const taskId = latest.task?.id + if (taskId !== undefined && [...(graph.runs ?? []), ...(graph.history ?? [])].some(run => run?.status === 'done' && run.task?.id === taskId && endedAt(run) > at)) continue const name = typeof graph.name === 'string' && graph.name.trim() !== '' ? graph.name.trim() : String(graph.id ?? t('A workflow')) const nodeName = typeof latest.failure?.nodeName === 'string' ? latest.failure.nodeName.trim() : '' const reason = typeof latest.failure?.text === 'string' && latest.failure.text !== '' diff --git a/core/test/inbox-workflow-failure.test.js b/core/test/inbox-workflow-failure.test.js index 2140d01a6..227bc12d8 100644 --- a/core/test/inbox-workflow-failure.test.js +++ b/core/test/inbox-workflow-failure.test.js @@ -341,3 +341,24 @@ test('#1706: failed workflow rows share the one Needs you list', async () => { assert.deepEqual(plain(heads), plain(GROUPS.map(group => group.title))) assert.equal(all(tree).filter(node => node.type === 'section').length, GROUPS.length) }) + +// P-599: a run that failed and was then re-run to a green finish kept its Failed card for the +// whole 7-day window, and the sidebar's Need-you count with it, because the card was chosen +// from the failed runs alone. The success is in the same per-graph history under the same +// `task.id`, and later than the failure. +test('P-599: a later successful run of the same task retires its failed card', () => { + const task = { id: 'issue-2447-run', title: 'issue #2447: a room-share envelope', callerBotId: 'b1' } + const failed = { ...failedRun, id: 'run-failed', task, finishedAt: NOW - 2 * 60 * MIN, updatedAt: NOW - 2 * 60 * MIN } + const done = { ...failedRun, id: 'run-done', task, status: 'done', failure: undefined, finishedAt: NOW - 30 * MIN, updatedAt: NOW - 30 * MIN } + const graph = { id: 'delivery', name: 'Delivery pipeline', runs: [], history: [{ ...failed, at: failed.finishedAt }, { ...done, at: done.finishedAt }] } + const model = inboxModel({ roster, work, tasks: [], workflows: [graph], now: NOW }) + const listed = [...model.needs, ...model.progress, ...model.done].filter(card => card.kind === 'workflow') + assert.deepEqual(listed.map(card => card.run.id), [], 'the run that finished green answers the failure') + assert.equal(needsCount(roster, work, [], NOW, [graph]), model.needs.length, 'the badge counts the same cards') + // Control: another task's failure on the same graph still lists, so the graph itself did not + // simply stop reporting failures. + const other = { ...failed, id: 'run-other', task: { ...task, id: 'issue-2400-run' } } + const still = inboxModel({ roster, work, tasks: [], workflows: [{ ...graph, history: [{ ...other, at: other.finishedAt }, { ...done, at: done.finishedAt }] }], now: NOW }) + const cardsStill = [...still.needs, ...still.progress, ...still.done].filter(card => card.kind === 'workflow') + assert.deepEqual(cardsStill.map(card => card.run.id), ['run-other']) +}) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index 2bb2c5798..9b0ce6511 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -49,7 +49,7 @@ The browser side of the patched `ds-bot` core (`core/`): sources in `core/src/cl | `core/src/shared/approval-waiting.js` | The one rule for "this hold still waits for the owner's decision" (#734, #754): `isApprovalWaiting(status)` is true for `pending` and `pending_approval` and false for every settled spelling (`approved`, `auto`, `failed`, `rejected`, `stale`, missing). The host writes `pending`; Bot Social's own outbound hold arrives as `pending_approval`. Host and client both import this one file — the held-reply card, the approvals overlay and the chat bubble on this side, `hooks.js` (`decide()`, `heldOf()`, the cap, the in-flight filter, the cleanup) and `channel.js` (a row's wait badge) on the other — and the overlay asks the host for `[...WAITING_STATUSES, 'failed']`, so the two spellings cannot drift apart again on one side only. Tests: `core/test/approval-waiting.test.js`, `core/test/host-approval-status.test.js`. | | `core/src/shared/held-retry.js` | The one rule for "this failure can still be retried" (#968): `canRetry(record)` is false for `retry: false`, for `noRetry: true`, and for a record whose stored `error`/`code` names a terminal reason (`no such remote bot` / `is not shared with your Bots` / `is no longer shared with you` / `has not granted chat.public` / `bot_not_shared` / `bot_not_found`; a bare `capability_not_granted` is not one, #988), true otherwise. The host states the same answer in two spellings (`noRetry` on an approval, `retry: false` on a held reply), so both cards and Bot Social's own dock ask this one function instead of keeping a copy each; a hold written before the flag existed is read from its stored sentence at render time, with no state migration. Tests: `core/test/held-retry.test.js`. | | `core/src/client/schedules.js` | Settings → Scheduled tasks, a Bot's card and the details line. Each row has **Edit** (pencil; the row turns into the same form that creates a task, filled in, with **Save changes**; a new task opens with no target chosen — **Send to** reads "Pick a Bot or group chat" and **Create task** stays disabled until one is picked, so one click cannot create the task for the first Bot (#1334); an edited task keeps its time zone, a cron rule it cannot show is kept), **Copy** (`CopyRow`, #1287: the row turns into the create form's target picker, minus the chat the task already runs in, and **Copy** posts `schedule-create` with `copyFrom: { sessionId, id }` — the host writes the row, so the copy keeps the source's next run) and **Pause** / **Resume**; a paused row reads **Paused** (#354). A row with a recorded run adds one line under it: "Running since …", "Last run …", "Last run … · Stopped" or, in red, "Last run … · Failed: "; a failed run of a task that is not paused gets **Retry**, and any other task the host will run on demand — active, with no run in flight — gets **Run now**; both call the same `actions.scheduleRetry` and run the task once now without moving DSH's own next time (#473, #2507). With Automation tasks off, the hint reads "Switch on Automation tasks on the Plugins page." and the button **Open Plugins** closes the Settings dialog and opens DSH's Plugins panel (`actions.selectPanel('plugins')`); the Harness settings dialog has no such switch (#378). The page opens with the team's **Quiet hours** card (#1166): **Off** with a **Turn on** button, or **On, 22:00–07:00** with the window's two time fields and **Save window** (the settings write refuses an invalid window, and the host's named refusal appears on the form's error line, as a `role="alert"` so it is announced and not only printed (#2232)). While quiet hours are on and an `every`/`cron` task exists, the view's `quietUnchecked` flag draws one line saying such a task can still run inside the window: that flag is the only place the rule is computed — the page does not scan the task list itself (#1199) — and it asks for the view again right after the settings write, so the line does not wait for the 30-second poll. Never a silent claim of silence, and no suppressed count. A repeat whose interval is not a whole number of minutes is stated as seconds (`Every 90 seconds`, #1651): the edit form opens holding that exact value in the **seconds** unit, and saving a task whose repeat was not touched sends the stored interval back unchanged instead of a rounded number of minutes. The details panel's line (`ScheduleSummary`) lists every task of that Bot, paused and finished ones included, because a Bot whose tasks are all paused has tasks while a Bot with none is the only empty state; each row answers when — `Next: