Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1566-inbox-hidden-failed-turn.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A failed turn in a hidden Bot's own chat no longer leaves a card in the Inbox that cannot be opened or dismissed; a failure in a group is still listed, because the group's row opens there, and *In progress* is unchanged. ([#1566](https://github.com/ThinkFlowLab/FlyBot/issues/1566))
1 change: 1 addition & 0 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -7778,6 +7778,7 @@ function inboxModel({ roster, work = {}, tasks = [], sessions, now = Date.now()
for (const [key, entry] of lastEnded) {
const bot = ownBot(roster, entry.botId);
if (bot === void 0 || entry.failed === void 0 || !fresh(entry.at) || seen.has(key) || waitingAt.has(key)) continue;
if (bot.hidden === true && entry.roomId === void 0) continue;
const chat = entry.roomId ?? bot.id;
const asked = sessions?.byId ? previewOf(sessions, partOf(roster, chat), [], [], botNames(roster)) : "";
needs.push({
Expand Down
3 changes: 3 additions & 0 deletions core/src/client/inbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ export function inboxModel({ roster, work = {}, tasks = [], sessions, now = Date
for (const [key, entry] of lastEnded) {
const bot = ownBot(roster, entry.botId)
if (bot === undefined || entry.failed === undefined || !fresh(entry.at) || seen.has(key) || waitingAt.has(key)) continue
// A hidden Bot has no sidebar row, so this card's only action (Open) could not show the chat
// it names (#1566). Its failure in a group still lists: the group's own row opens there.
if (bot.hidden === true && entry.roomId === undefined) continue
const chat = entry.roomId ?? bot.id
const asked = sessions?.byId ? previewOf(sessions, partOf(roster, chat), [], [], botNames(roster)) : ''
needs.push({
Expand Down
25 changes: 25 additions & 0 deletions core/test/inbox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,31 @@ test('a failure no longer needs me once the Bot works or waits there again', ()
assert.equal(freed.needs.find(card => card.id === 'turn:e3').place, 'in Ops')
})

// #1566: a hidden Bot has no sidebar row, so the failed-turn card's only action (Open) could
// never reveal the chat it names. Its failure in its own chat is left out; one in a group still
// lists, because the group's row opens it. Progress keeps counting a hidden Bot's running turn.
const hiddenRoster = { ...roster, bots: roster.bots.map(bot => (bot.id === 'b2' ? { ...bot, hidden: true } : bot)) }
hiddenRoster.byId = Object.fromEntries(hiddenRoster.bots.map(bot => [bot.id, bot]))

test("a failed turn in a hidden Bot's own chat is not a Needs you card (#1566)", () => {
const hidden = inboxModel({ roster: hiddenRoster, work: endedWork, tasks, sessions: endedSessions, now: NOW })
assert.ok(!ids(hidden.needs).includes('turn:e2'))
// The count badge drops it with the card, and it is not reclassified into Done.
assert.equal(needsCount(hiddenRoster, endedWork, tasks, NOW), model.needs.length)
assert.ok(!ids(hidden.done).includes('chat:b2'))
// The same snapshot with a visible Bot still lists it: the card itself is not the defect.
assert.ok(ids(endedModel.needs).includes('turn:e2'))
// A hidden Bot failing in a group still needs me: that chat is opened from the group's row.
const inGroup = { ...endedWork, ended: [...endedWork.ended, { id: 'e9', botId: 'b2', roomId: 'r1', at: NOW - 30_000, failed: 'quota' }] }
const groupModel = inboxModel({ roster: hiddenRoster, work: inGroup, tasks, sessions: endedSessions, now: NOW })
const groupCard = groupModel.needs.find(card => card.id === 'turn:e9')
assert.ok(groupCard, 'a hidden Bot failing in a group is still listed')
assert.equal(groupCard.chat, 'r1')
// In progress keeps counting a hidden Bot's running turn; this fix only reads Needs you.
const running = inboxModel({ roster: hiddenRoster, work: { ...endedWork, bots: { ...endedWork.bots, b2: { since: NOW - 1000, tool: 'pwsh' } } }, tasks, sessions: endedSessions, now: NOW })
assert.ok(ids(running.progress).includes('run:b2|'))
})

test('each card has a one-line latest activity that never repeats its title or reason', () => {
const byId = Object.fromEntries([...endedModel.needs, ...endedModel.progress, ...endedModel.done].map(card => [card.id, card]))
// An approval and a running turn in Ops show that chat's last line.
Expand Down
2 changes: 1 addition & 1 deletion docs/subsystems/core-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second
- One **Inbox** row sits at the top of the sidebar, above the Bot list (wide and rail), with a count badge of the *Needs you* cards; it opens the inbox dialog over the conversation (owner decision 2026-10-20, option a). No new + menu entry, no new sidebar section.
- Groups, in this order and with these words (the decision's "Need you" is written **Needs you**, as in the triage): **Needs you**, **In progress**, **Done**; each has a one-line empty message. Only data the client already has, no new host state: `work.approvals` (tool approvals), `pendingCount` on chats (held Bot messages), `work.bots` / `work.rooms` (running turns), the task records of `src/host/tasks.js` (`actions.tasks` → the existing `tasks` endpoint, polled every 30 s) and the Session list's `updatedAt` and last line for finished Bot chats.
- Only my own Bots (not a friend's Bot, not another member's): a friend's chat appears only as a held message of mine waiting in it. Every card carries `assigner`, `owner` and `executingBot` (the first two are me in step 1) so team views can filter later.
- *Needs you*: tool approvals, held messages, tasks that are blocked, have a result ready or failed (with the reason), and failed turns (#471, `kind: 'turn'`): from `work.ended`, the same ended-turn entries the system notifications read (#353), a place's (a Bot's own chat or one group) latest ended turn when it failed, in the last 7 days, while that Bot is not running or waiting for an approval there; its title is the chat's last line (*The last turn did not finish* without the Session list), its reason the failure. A later finished turn there clears it, and that Bot chat is then not also in *Done*. **Open** only. *In progress*: running turns (with elapsed time; a turn waiting for an approval shows only in *Needs you*) and open/claimed/working tasks. *Done*: accepted or cancelled tasks and Bot chats whose last turn finished. Done and failed tasks age out after 7 days; Done shows the newest 20.
- *Needs you*: tool approvals, held messages, tasks that are blocked, have a result ready or failed (with the reason), and failed turns (#471, `kind: 'turn'`): from `work.ended`, the same ended-turn entries the system notifications read (#353), a place's (a Bot's own chat or one group) latest ended turn when it failed, in the last 7 days, while that Bot is not running or waiting for an approval there; its title is the chat's last line (*The last turn did not finish* without the Session list), its reason the failure. A later finished turn there clears it, and that Bot chat is then not also in *Done*. **Open** only. A hidden Bot's failure in its own chat is left out (#1566): it has no sidebar row, so **Open** could not show the chat the card names; one in a group still lists, because that group's row opens there. *In progress*: running turns (with elapsed time; a turn waiting for an approval shows only in *Needs you*) and open/claimed/working tasks. *Done*: accepted or cancelled tasks and Bot chats whose last turn finished. Done and failed tasks age out after 7 days; Done shows the newest 20.
- A group's open question card is a *Needs you* card too (#552): `roster.questions[<room id>]` — a card a Bot asked about work handed out from that group (#550) — has the status **Needs your answer** (`input`, as a blocked task's question does), the question as its title, its detail as the reason, and *in <group>* as its place. An ask_user card in the Bot's own chat (`roster.questions[<session id>]`, #1140) is the same kind of row and names that Bot; **Open** goes to the session that holds the card (the Bot's chat, not the group the task was handed out from). Both rows show the card's options and a short answer. One option sends at once; a multi-select toggles and Submit sends the set, with the short answer. Either call is `actions.answerQuestion`, the same answer the chat card sends, and the row leaves. What was typed or toggled stays when the Inbox re-renders. A task that is waiting on input shows the same short answer and calls `actions.answerTaskInput` (`task-answer-input`), so the owner is not sent to a group that has no card as the only way to answer. A card for a removed group, and one from another member's Bot, add none.
- Each card has exactly one status, as a colored pill with an icon and a word (never color alone), the Bot, where and when, and **Open** (closes the inbox, opens the chat). *In progress* and *Done* keep the title on one line, with the full text on hover. A *Needs you* card (`data-status` `approval`, `held`, `input`, `result` or `failed`) shows its title and reason in full — they wrap, including line breaks, and the phone layout's two-line clamp does not apply — so the ask can be read in the list (#1177). The dialog body scrolls. Hover still carries the full title. An approval the inbox lists and the group chat's card can answer (`answerable`) also has **Reject** / **Allow once**, which call `actions.answerApproval`, the same answer as that card (no second approval path); the card leaves and a status line says what was done; an error stays on the card. No native confirm dialog. A held Bot message and an approval DSH's card answers in the Bot's own chat have **Open** only: Bot-sent messages are approved in their chat. Ask_user rows and a task waiting on input are answered in the inbox, as the previous bullet says. Every action is a button, so the dialog is keyboard reachable; the close button takes focus and Escape closes.
- **Done is the acceptance surface (#401).** A `result-ready` task is a *Needs you* card with the result summary as its reason, its evidence as up to five links (`task.result.links`, rendered under the title) and the producing Bot plus its model in the meta line. The card answers the result in place: **Accept** calls `actions.acceptTaskResult` (`task-accept-result`) and the card leaves; **Send back** opens a one-line box (`What should change`) and **Send it back** calls `actions.sendBackTask` (`task-send-back`) with that note, so the same task returns to the Bot in *In progress* and shows *Sent back: …* (`task.followUp.note`). A note is required — an empty box sends nothing — and the box survives a roster or clock poll, like a short answer (#1140). Typing an approval in the chat is not an acceptance: this button is.
Expand Down
Loading
Loading