Skip to content

Let a person see every task by what it needs from them - #918

Draft
jerelvelarde wants to merge 70 commits into
CopilotKit:mainfrom
jerelvelarde:jerel/openbot-tasks
Draft

jerelvelarde wants to merge 70 commits into
CopilotKit:mainfrom
jerelvelarde:jerel/openbot-tasks

Conversation

@jerelvelarde

Copy link
Copy Markdown
Contributor

Let a person see every task by what it needs from them

Depends on #916 and #917. Its own change is the last five commits; the seventeen before them are #917's: compare.

The problem

With #917, started work runs as a task in a conversation of its own, and the app has nowhere to show one. Start on a suggestion opens a task that shows only as one more conversation in the sidebar, where a reply runs outside the task's order. A Bot's start_task draws as a generic tool call. Nothing says which tasks wait on the person, which are working, and which couldn't finish.

The OpenDuet skin's Tasks tab only rearranges OpenBot's lists of approvals, questions, responsibilities, routines and suggestions, and counts every approval and question waiting anywhere.

The approach

Tasks lists every task by what it needs. Needs you, Working, Failed and Ready for review, then Done, folded. Each row is the task's Bot, its title and one line of what comes next, such as the question it stopped on. Tasks sits above Bots in the sidebar, and its count is the Needs you group itself, so the number and the list can't disagree. The list sits beside the open task when there is room, and before it on a narrow screen. ↓ and ↑, or j and k, move through it; Enter opens a task and Escape closes it.

A task's page is its report above its conversation, drawn with the same transcript and approval cards, with Mark done, Reopen, and Try again when it couldn't finish. The turns run on the server, so the conversation has a task mode: a reply goes to the server and is drawn at once under the id the turn records it with, held replies show as Queued, and the Bot reads as working while its turn runs there. A reply on a failed task tries it again with the person's words.

A task's conversation is the task's. It is left out of every list a person picks a conversation from, the skins' menus and pickers included. /channel/$channelId sends a task's conversation to the task's page, replacing its own entry so Back still goes where the link was, and a Bot's activity links what was raised in a task to the task.

Starting one. When a Bot hands work on with start_task, the conversation draws the task as a card under its "On it", live, opening the task; a refused call draws nothing. The + opens New task: which Bot, the default one first, and what to do. Start on a suggestion opens a task, from the Bot's page or from Up next under the list, which shows the Bots' open suggestions only.

Reads stay cheap. The sidebar keeps the list mounted on every screen and a task can wait on the person all day, so the list is read every three seconds only while a task is working, and every thirty otherwise. An answer given here reads them again at once. A failed refresh keeps the page and the list on screen under a line saying so, so a reply being typed is never lost.

The OpenDuet skin's Tasks tab now opens Tasks, and its "waiting" became Needs you: its count is the tasks that need the person. Its own screen is gone.

The cost: every page now polls the task list.

A Bot hands work on The task, by what it needs
The task's card under the Bot's On it Tasks grouped by what each needs, beside the open task

What is not covered

  • A task's approvals still count on the Bots badge as well as on Tasks. That needs /api/bots/attention to leave task threads out.
  • Try again losing a race is recognised by the server's English sentence.
  • The list shows the newest 200 tasks, with no way to see more.
  • A task marked done after failing shows the Try again hint without the button.
  • The sidebar leaves task conversations out after a page arrives, so a page holding only tasks can read "No conversations yet" while more wait.
  • An answer given in another tab reaches a quiet list within thirty seconds.
  • A reply watched live while its turn runs on the server can drop a few characters on screen; the stored reply is whole. Upstream main shows the same on a reply resumed after Allow once.
  • A long reply in a task's conversation can show a small empty bubble between sections.

Verification

typecheck, lint, format:check and the assertion-shape guard are clean. The full app suite is 2453 pass, 0 fail, against 2416 on #916 and 2129 on upstream main. The server suite is 6333 pass, 20 fail, the same 20 that fail on #917 and upstream.

  • app/tests/task-groups.test.ts: the groups' order; a task's own approval or question makes it Needs you; the sidebar's count is that group; fast reads only while one is working.
  • app/tests/tasks-screen.test.tsx: the grouped list; a row opens beside it; the keys; Up next; New task; task conversations out of the sidebar.
  • app/tests/task-thread.test.tsx: a reply goes through the server; Queued replies; Try again with words; a failed refresh keeps what was typed; a task's conversation opens as the task; start_task's card.
  • app/tests/skins-openduet-frame.test.tsx and skins-openduet-model.test.tsx, reworked: the tab opens Tasks and counts Needs you.

Merge notes

The seventeen commits under this branch have the same server, toolkit and shared files as #917, including migration 0070_tasks, so a renumbering there has to be repeated here. app/src/routeTree.gen.ts gains the /tasks routes.

The last two places still drawing boring-avatars' gradient circles now draw the
orb in the colour the Bot's id picks, so a Bot looks the same in the sidebar,
the conversation header and its profile. Nothing imports boring-avatars any
more, so it leaves the app's dependencies.
…ll when asked

A Bot's orb is a block, so a group conversation's 12px faces no longer sit on
the header's text baseline and drop half out of their circles. It resizes its
canvas when the screen's pixel density changes, so dragging a window to another
screen or zooming no longer leaves it soft, and it follows the reduced-motion
setting live rather than only when it first draws. The changelog and the
stylesheet now describe what this step draws, not what later steps will.
The skins import the orb's state type from agent-orb now that ai-core is gone,
OpenBot's mark keeps its agent-orb class for them, and their tests check the
orb's colour rather than boring-avatars' SVG. The Appearance test's preferences
carry the new skin field.
AgentOrb read "100%" as 100px, so with no Bot yet the header drew a 100px orb
in a 44 or 52px slot. With a Bot, the classes that used to rescale
boring-avatars' SVG squeezed the avatar's circle to 44px below 900px wide while
the orb inside stayed 52px, clipped and off centre. The frame now picks 44 or
52px from the window's width and draws the face at that size, the selectors
that matched only the old SVG are gone here and from OpenDots' Bot rows, and
AgentOrb takes its size only in px and refuses anything else.
Every orb on screen redrew on every animation frame, uncapped, so a sidebar of
conversations kept a few dozen canvases repainting at the screen's full rate
while the app sat idle: a small orb's colour still drifted, so no two frames
were ever the same. A small orb's colour now holds still, an orb skips any
frame that would draw what its canvas already shows in the same theme and at
the same density, and a small orb draws at most about thirty frames a second.
A face at rest with the pointer still now draws only to blink.
…violet

A Bot's face was one of nine colours and nothing else, so a team of four had
about an even chance of two identical faces. In the Bots OpenBot ships with,
three were the same sky blue, and release notes wore violet, OpenBot's own
mark. A Bot is now drawn from its seed: one of the eight colours that aren't
violet, with its colour masses turned, its hue nudged and its eyes spaced by
the same hash, so two Bots in one colour still look apart. BotOrb takes the
seed rather than a colour and draws OpenBot's violet when it has none. The
OpenDots avatar test checked one colour in nine, so it passed or failed on the
hash; it now checks that the seed reaches the orb.
GET /api/setup says, row by row, whether Intelligence, the model, the Bot
computer, more apps and Slack and Teams work here, from their own answers rather
than from a variable being set. POST /api/setup/check asks them again. An admin,
or somebody running OpenBot alone, is told the page or the variable that closes a
gap; everybody else is told only that it works or that their admin sets it up,
and their response carries no variable name.
Onboarding is the only page somebody who has not finished it may open, so a
consent started on its Connect your apps step returns to that step, worked or
not, for OAuth and brokered apps alike. A consent cancelled at the vendor now
returns to the screen it started on too, instead of the Connections list. The
returnTo name is still narrowed to a closed set before it is sealed or used.
A step-by-step onboarding frame that works on a desktop screen too. One DOM,
laid out by CSS: from lg up, the steps as a list (the ones reached can be
picked) with the step's heading, sentence and Continue/Back on the left, and its
picture filling the right; below lg, a dash for each step, a round Back and the
buttons pinned to the bottom. Focus moves to each new heading. A demo's clock
runs only while it is on screen in a shown tab, and holds still with reduced
motion.
Welcome types a request under OpenBot's listening orb, a computer of its own
shows the Bot's desktop with its pointer on an errand, the team shows three
Bots' orbs taking turns at work, and an approval card waits for Allow once
before it sends. Somebody running OpenBot alone also sees it on a laptop, with
the agents they have, and for their organization. Each is a picture, read by
its label, with nothing inside to click.
Connect your apps lists the apps the person's default Bot uses as them and
connects the ones with a sign-in page right there, coming back to the step; a
brokered consent is asked of the vendor on return, so the row turns Connected
only when it is. Get notified previews the notification OpenBot really sends
and asks the browser, sharing its answer with a Bot's page. Check your setup
draws the server's checklist with a status pill per row, and Check again.
First run is now welcome, a browser and files of its own, the team of Bots,
asking before it acts, running it yourself (only for somebody trying OpenBot
alone), connect apps, get notified, check your setup, and the name step. Where
somebody is lives in the address and is saved on every move, so a reload,
browser Back and a consent's return keep their place. The name step stays last,
under the orb: Get started saves the name, completes onboarding and plays the
orb's finish, and a checklist link carries its page through it, since nothing
else opens until onboarding is done.
…ws now

The root's drop guard named the onboarding poster's composer, which is now a
drawing inside onboarding's picture frame. The changelog says the computer check
also runs once at start-up.
Found running the steps against a real install. Connect apps told an admin to wait for an
admin; it now offers Plugins, carried through the name step like the checklist's links.
The checklist's "Required" tag ran into each title, and the notification preview cut
"General Assistant needs you" off at the Bot's name.
…s written out

Setting OPENAI_BASE_URL to https://api.openai.com/v1 counted as a custom endpoint, so the
check was skipped and the setup checklist said the model was not checked. The provider's own
host is now checked as if no address were set.
…the box

The step promised that a Bot asks before it sends, buys or deletes anything.
Ask before making changes starts off, so out of the box a Bot asks only where
a rule says to, and paying or changing account security is handed to the
person. The step now says that, and where to turn asking on.
With AGENT_COMPUTER_URL set and no computer running, the usual state and
always true on a first run, the row told an admin Bots can't browse the web
and to set the variables and restart, for something already set. Not checked
now says it is set up and none is running to check, and opens Computers.
Intelligence, model and computer answers were asked at boot and then served
for as long as the server ran. An Intelligence slower to start than OpenBot
read as a red required gap until somebody pressed Check again, and a key
refused days after boot still read Ready. A read now asks again, in the
background and under the shared 10-second limit, when an answer failed or is
five minutes old, so the next read has it. Reads are answered from the last
answers that are all in, so none waits on a slow fleet.
The changelog said the intro was replaced by another product's steps, and two
comments cited it, which reads as a skin being added. They now say what
OpenBot does. The changelog also said a cancelled connect returns to where it
started from anywhere; only Connect your apps changed, so it says that.
At 1280 by 800 the self-hosting step and the name step ran past the bottom
of the window, cutting Continue and Back in half; at 700 tall, four steps
did. The left pane stacked the nine-step list, the step's words and its
buttons, and the page was left to scroll.

On a desktop the frame is now one screen tall and the step list's row is
the only one that can shrink, so the step's words and its buttons always
keep their full size. When the screen is too short for all of it, the list
scrolls within itself, fades at the edge it scrolls under, and keeps the
step on screen in view. Its rows and the space under it are a little
tighter, so every step still fits whole at 1280 by 800. Only a window too
short for the words and buttons themselves scrolls the page.

The picture pane now stretches to the screen instead of being set to its
height: a set height spanning every row gave that height to the empty last
row before the list got any. It drops the phone's margin on a desktop, which
would stop it short of the bottom on the name step.
Rows, destinations and the header's controls lift to the card's fill and
shadow under the pointer and while open, with a visible focus ring. Row
text is on type-sub and type-caption, and the unread dot is the brand
colour. OpenBot's orb sits beside the product name, and the + button's
tooltip names its shortcut.

A busy conversation's orb works, and a still dot in the working colour
says so as well: an orb under 40px only blinks, and reduced motion
stills it. ChannelAvatar takes an optional state; its props are
otherwise unchanged.

Each conversation row has a "…" button (SidebarMenuAction showOnHover)
with the Pin and Delete that were on right-click alone. It shows under
the pointer and with focus in the row, and comes straight after the row
in tab order.

NavigationItem gains an optional badge, a count the sidebar draws beside
the entry, set by useNavigationItems, which the sidebar now reads.
Primary entries draw in list order, so a new destination and its count
are added in lib/navigation.ts alone.

⌘K or Ctrl+K opens the sidebar, or its sheet on a narrow window, and
focuses its search box. Settings lists it from the hotkey registry.
A Bot's reply is drawn one bubble per thought (split-bubbles.ts), each
bubble memoised so a streamed reply parses only the part still arriving.
A bubble holding a code block or a table takes the column's width from md
up; the rest keep an 82% cap. The person's messages sit in a tinted bubble,
and Thinking is three dots in the Bot's bubble, still with reduced motion.
New --bubble, --bubble-user and --radius-bubble tokens carry the colours
and the shape, and their words clear 4.5:1 in both themes.

Tool lines draw a hook, an outcome mark and a Tabler icon, and only a line
that worked carries the done mark. A call that was refused, failed or
declined is named for what it would have done, a declined one reads "You
declined" (toolResultDeclined) and is not counted as an issue, a group
where nothing worked never claims it did, and a step held on its approval
card reads "Waiting for you". Mail and calendar calls through any app are
named for what happened to the person's own ("Searched your email"), by
their words rather than a table of one Gmail plugin's tools.

The Bot's bubble stays the ghost variant, which the skins find a Bot's
words by, and OpenDots takes the bubble off again to keep its plain prose.
A Bot's page names its tools by what they do ("Send your email").
A conversation with one Bot shows that Bot's orb in its header, at 40px,
with a line under the conversation's name. The orb works while the Bot has
the turn, listens while the person types, and lands an answer (not a
failure or a Stop). The line says "Working · Searching the web" while a
step runs, "Needs you" while an approval waits in that conversation, and
nothing at rest. The conversation list's busy flag counts as working, so a
run the server resumes after Allow once shows as "Working"; the flag of
the tab's own turn does not outlast the turn. Groups keep their own header.

The state lives in components/channels/conversation-presence.tsx, a store
the conversation route provides and ConversationView writes. Each reader
subscribes to the fields it uses, so a keystroke reaches the orb and not
the line, and a new step reaches the line and not the orb. Reports carry
the conversation they came from, so the route, which stays mounted from
one conversation to the next, never shows the last one's turn.

The step comes from lib/channels/step.ts (stepFor, stepPhrase) in the
running words the step's own tool line uses. An app's call it cannot name
by what it does reads "Using <app>"; a question for the person and a
drawn interface are passed over.

The compact composer is a pill: 62px on a soft shadow around 44px
controls, the editor at the bubble's 17px, and the focus ring kept.
Its border stays, clear, so the skins still colour it. It takes a
placeholder ("Message Ana" in a conversation with one Bot, "Ask anything"
elsewhere) and an onTextChange. Every data-slot hook is where it was.
… tool calls it has

TanStack Query polls once for every component reading a query, and fetches only merge
while one is in flight. Every tool call row read the inbox twice, mounted at different
moments as a reply streamed, so thirty calls asked for it sixty times every ten seconds.

The rows and cards now read the cache without a poll of their own. The conversation
polls it once, and so does the Bot page, whose cards used to keep themselves fresh.
A turn that stopped for an approval counted as answered. The browser sends a gated
click to its card and stops the run with no failure and no Stop, and a call the server
holds ends its turn the same way, so the orb played its landing while the line said
"Needs you".

A turn that ends on a call still waiting for the person no longer counts as answered,
and an approval waiting in the conversation holds off any landing.
…itself

The check read declined[1]?.output under toBeUndefined, which also passes when
the step is missing; the assertion-shape guard refuses that shape.
The changelog still said the box names the site the value goes to. Since the
password box stopped naming a host, which it could not vouch for, it says the
value goes straight to the page and the Bot never sees it; the changelog now
says the same.
What a task's conversation needs from the queue, with nothing in OpenBot
using it yet:

- `claim({ serialBy })` runs one item at a time per group. An item waits
  while another of its group holds a live lease, or while an older one is
  unfinished with attempts left. A payload field's name groups one kind by
  that field; a `SerialOrder` lets several kinds share one order, each
  with its own attempt cap and its own way of naming its group, so the
  queue never needs to know what a group is.
- `release({ unstarted })` gives back the attempt a claim spent when the
  work never began.
- `renew`, `finish` and `release` act only on a live lease. An expired
  lease on its last attempt keeps its owner, and a late heartbeat, finish
  or refund from that owner could otherwise revive it beside the sibling
  now running in its place.

`ThreadLockBusyError` names a lock refused on a turn's first acquisition,
the one conflict that is safe to defer. `pendingPersonQuestion` is the
person's waiting questions, less any a reply has already answered; the
inbox and `respond` use it, which changes nothing until replies carry
`questionIds`.
A task is background work a Bot does in a conversation of its own. This
adds where tasks are kept and how they are read; nothing opens one yet.

The `tasks` table holds what only the person or the task's start can
say: its Bot, its conversation, its title and why, where it came from,
and `resolved_at`. There is no status column. Every read works the
status out from the ledgers that already know it: the task's turns on
the work queue, approvals waiting in the inbox, questions its Bot asked,
and the last thing said in its conversation. A turn runs at most twice;
one out of runs reads as failed, with the reason in plain words.

`/api/tasks` serves the person's own tasks: `GET /`, `GET /:id`, and
`POST /:id/resolve` and `/reopen` for Mark done and Reopen. Another
person's task is the same as no task.

Migration 0070 creates the table.
The thread lock refused when a turn first asks for it now throws
`ThreadLockBusyError`, with the platform's 409 as its cause. That one
conflict means the turn never ran, so a queue can put it back without
spending an attempt. The same 409 later in a run, or on a renewal, may
follow a tool call or a model's spend, and is raised as it comes.

A turn also takes `systemMessages`: instructions the server resolved for
it, such as the skills picked for a reply to a task. They are kept as
history rows and given to the model as context, since a built-in Bot's
model leaves system rows out.

`keepLease` gives up exactly one interval before the lock lapses, not one
tick after: with renewals failing, the `>` let the lock expire on the
tick it should have stopped.
Started work used to run in the conversation the suggestion was delivered
to, so that conversation filled with the work. Start now opens a task: a
new conversation with the Bot and the record that names it, written in one
transaction so a lookup never sees one without the other. Its turn runs
there, queued and picked up straight away.

The proactive engine runs a task's turn at most twice: once, and once more
after a throw or a restart. A turn refused the conversation's lock on its
first ask is given back without spending a run. Out of runs, it stays on
the queue with its reason, which is what reads as failed, and the Bot says
so in the conversation. A turn queued before retries existed is never run
a second time. Try again (`again`) reruns the failed turn, keyed so two at
once run once. A reply (`reply`) is taken onto the queue before it is
answered, with the questions it answers and any files and skills checked
and kept, so it survives a restart. A task's turns, its replies and the
answers to question cards asked in it share one order (`TASK_ORDER`), so
none overtakes another.

A task's conversation stays out of the person's own: `direct()` never
picks one, the roster marks it with `taskId`, research never adopts it,
and its title comes from the ask inside the task frame. While a task's turn
holds its lease, the conversation's work list says so; the engine says it
is no longer busy once the turn is off the queue, so that list is read
after the turn has left it.

`POST /api/proactive/suggestions/:id/start` answers `{ suggestion, task }`,
`GET /api/proactive/suggestions/:id` reads one suggestion whatever its
status, and `/api/tasks/:id/reply` and `/again` steer a task.
A Bot OpenBot runs itself can now hand anything bigger than a quick
answer to a task of its own with `start_task`, and answer "On it" where it
was asked, so that conversation stays one thread and the work happens in
the task's. The tool opens the task through the engine's `startAsked`,
the path a started suggestion takes, so both run, wait and finish alike.

It is offered only to built-in Bots (`agents.type = 'built_in'`). A Bot
behind a remote agent framework composes its own turn at somebody else's
server, so it is not told to hand work on; its tasks start from its
suggestions and from the +.

`POST /api/tasks` is the + on Tasks: `{ botId, instruction, title? }`
opens a task with any of the person's Bots and starts it, titled from the
ask's first sentence when no title is given. A Bot the person cannot
reach is the same as no Bot.
`GET /api/tasks/:id/report` reads a task's conversation back as the
report the task page shows above it: the person's decisions on approvals
asked there, their replies, and the Bot's update as each turn ended, in
the order they happened. Nothing is stored. The conversation comes from
Intelligence, as the runtime holds it; its messages carry no times, so
each time comes from whichever ledger saw that moment, the approval
inbox or a turn's work item, or there is none.

A turn still running is left out until its own completion is on the
queue, and a rerun of an accepted reply adds an update without adding
the person's reply twice.

The report has no list of the emails and events a task read: that needs
connector-specific readers OpenBot does not have.
The settle test now checks the queue at the moment the conversation is
told it is no longer busy: the turn must already be finished there, which
is the reason the signal moved off the turn. Its assertions also keep
`?.` out of their subjects, as the assertion-shape guard requires.

The accepted-answer fixtures ended the earlier turn on the host's clock
just before a reply stamped on the database's. A host clock a few
milliseconds ahead put that end after the reply's failure, and the task
read as ready instead of failed. They now end it a minute earlier.
…n it must

The frame told the Bot to "end with what you did and anything still waiting on them", and
gpt-5.4 wrote that as literal "What I did:" and "Still waiting on you:" labels; the second
became the task's next-step line on Tasks. Every task turn now carries a system message
saying how to end. A question there reads as Needs you, so the Bot asks one only when it
can't go on without an answer. The frame itself stays word for word, so tasks opened
before this are still read as tasks.
… of what isn't waiting

The first wording came back as "Nothing is still waiting on them." The guidance now asks
for the ending written to the person as "you", and to leave out what waits on them when
nothing does.
A turn stopped for Allow once with no words of its own recorded the toolkit's "The turn is
waiting for human input." as the task's next step. It now reads "Waiting for you to allow
a step.", or "Waiting for your answer." for anything else, and a message of the gate's own
is kept as it was.
start_task was offered to every turn of a built-in Bot, and the task's turn
always runs as the person. A routine, responsibility, wake-up or hand-off that
read an injected email could start a task that ran past the rules written for
unattended runs, credited to the person on the trail.

It is now offered only to turns a person started, never to a task's own turn
(built by its own runner, which tells the tools so), and one turn starts at
most five tasks.
A task's status read whether its last reply asked from the line its
conversation keeps, which is cut to 200 characters with an ellipsis. Any
closing question longer than that lost its question mark there, and the
task read Ready for review instead.

Whether the reply asks is now read from the whole reply as the turn ends,
and kept on the turn's work item as it finishes (`finish` can now merge
what the work came to into its payload). Status uses it when the last line
is that reply; a reply from before this, or anything else said there, is
read from the line as before.
Nothing purges task turns or the answers to a task's questions, and the
reads of them had no index: a task's status, its report and the question
inbox's check for answers already taken each scanned every task turn ever
written, for every person, and the inbox runs on every page that counts
what is waiting.

`work_items_owner_task_idx` indexes work by kind, person and task, folded
into the unpublished 0070_tasks migration and its snapshot. The status
read now names the person too, as every task turn does, so each of those
reads is a lookup by person. A test plans them over a queue holding many
people's turns.

A task with no turns on the queue read as never started however old it
was, so clearing old turns would have failed every old task. One whose
conversation has had anything said in it did start, and now reads from
what was said.
A task's turn that stops for Allow once has finished, and its resume was
outside the order a task's turns take. So a reply sent before the person
allowed the step could run after the resume, or one sent after it before,
and the thread lock kept them apart but not in order: the resume carries
on from the conversation as it was when the turn stopped.

The approval resume sweep now claims under the task's order, which finds
an approval's task through the thread it was raised in, as it finds a
question card's. A task's work runs in the order the person gave it.
Each start launched its own drain, so task turns ran side by side without
limit: one reply starts at most five tasks, but many replies or
suggestions could still run dozens of headless model turns at once for
one person.

A claim can now leave an item queued while its value already has so many
running (`runningAtMost`), counted again under a lock so replicas racing
for one person cannot pass it. Task turns use it to run at most three per
person, across replicas, and the engine runs at most ten in one process.
A turn held back reads Working and starts as soon as one finishes, from
the turn that made room, not at the next sweep.
…turn

When Allow once, a question card's answer or a sign-in carried a turn on,
its Bot was rebuilt through the generic approval runner, even in a task's
own conversation. So the task's resumed turn was offered start_task again,
which a task's own turn never is.

Those three now carry a turn on through one runner that builds it as a
task's turn when its thread is a task's conversation, and as before
anywhere else.
The roster's summary carried a channel's task, but `GET /api/channels/:id`
did not, so a link that opened a task's conversation by its channel had no
way to know it was a task's. The browser drew it as an ordinary conversation,
where a reply runs outside the task's queue.

The channel read and the channel a create answers with now carry `taskId`,
null for an ordinary conversation.
A recording showed a task's report giving the Bot's own reason for making a
task as its why: "This is a multi-step planning request that benefits from a
dedicated task and a polished itinerary." The field was described only as
"why it matters or by when", which a model read as its own reasoning.

start_task now says why holds only what the person said about why it matters
or by when, in their words, is left out when they said neither, and is never
the Bot's reason for making a task. Source and link had the same gap: source
is never what kind of request it is, and link is the email or event it came
from as read, never a page to use for the work or one made up.

A model following a description can't be unit-tested; the test pins that the
JSON schema the model is given says these things.
The app has a data layer for the server's tasks now: queries for the
list, one task and its report, the writes for starting one, Mark done,
Reopen, Try again and replying, and the grouping Tasks draws (Needs you,
Working, Failed, Ready for review, Done) with each task's next-step
line. The sidebar's count is that grouping's Needs you, so the number
and the list can never disagree.

A task runs in a conversation of its own, and the conversation list
marks it with `taskId`. Those conversations are left out of every list
a person picks a conversation from: the sidebar, the skins' menus, and
the pickers for responsibilities, research and reaching you.

A task's turn running on the server reads as the Bot working on it
("Scout is working on it…"), where before it would have read as a
routine.

The list is read every few seconds only while a task is working, and
now and then otherwise: the sidebar keeps it mounted on every screen,
and a task can wait on the person all day. An answer given here, to an
approval or a question, reads the tasks again at once, so the task it
was asked in moves off Needs you without waiting for the next read. A
task's card reads the approval inbox its conversation already polls,
with no poll of its own.
Tasks lists every task by what it needs from the person: Needs you,
Working, Failed and Ready for review, then Done, folded. Each row is the
task's Bot, its title and one line of what comes next: the question it
stopped on, what an approval would do, why it couldn't finish, or the
Bot's own last line. The list sits in a column beside the open task once
the space Tasks is given is wide enough for both, and on a narrower one
it is the list, then the task with Back. ↓ and ↑, or j and k, move
through it, Enter opens a task and Escape closes it with the focus back
on its row. In the sidebar, Tasks comes above Bots with how many tasks
need the person.

A task's page is its report above its conversation, drawn with the same
transcript and approval cards as any conversation, with Mark done or
Reopen, and Try again when it couldn't finish. Its turns run on the
server, so the conversation has a task mode: a reply is sent to the
server and drawn at once under the id the turn records it with, the
replies the server holds show as Queued, and the Bot reads as working
while its turn runs there. A reply on a failed task tries it again with
the person's words; if the task was already being tried again, the reply
waits its turn instead. The ask the server frames for the model reads as
the person's own words, once however many times the turn runs.

A refresh that fails keeps the task's page and the list on screen,
under a line that says they couldn't refresh, so a failed poll never
loses a reply being typed; the error takes their place only when
nothing was read yet. `/channel/$channelId` sends a task's conversation
to the task's page, replacing its own entry so Back still goes where
the link was, and a Bot's activity links what was raised in a task to
the task. A reply to a task always goes through the task's queue, never
beside its own turn.
When a Bot hands work to a task with `start_task`, the conversation now
draws the task as a card under the Bot's "On it": its title and where it
stands, kept live, opening the task. The card goes to the end of the
Bot's turn, under its words, wherever in the turn the call was made. A
refused call draws nothing; the Bot is told what to fix.

The + on Tasks opens New task: which Bot, the person's default one until
they choose another, and what it should do. Start opens the task it
started. Under the list, Up next holds the Bots' open suggestions with
Start and Dismiss, and Start opens the task the server made of it. Start
on the Bot profile's suggestions opens the task too, since a suggestion
no longer runs in the person's own conversation.
The OpenDuet skin had a Tasks screen of its own that only rearranged
OpenBot's lists: approvals and questions waiting, responsibilities,
routines and suggestions. Its Tasks tab now opens OpenBot's Tasks, and
stays lit with a task open. Its count is the tasks that need the person,
counted the way Tasks lists them under Needs you, where before it was
every approval and question waiting anywhere. The skin's own screen and
its count are gone.

The test of a skin switched while on a screen only the old skin has now
uses the OpenMuse skin's screens, since the OpenDuet skin has none left.
The Unreleased entry for tasks said what the server does. It now says
what a person sees too: Tasks in the sidebar, the list beside the open
task and its keys, the task's page, New task and Up next, the card a Bot
leaves in a conversation, and the OpenDuet skin's Tasks tab.

This branch has not been deployed

No deployments
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