Knowledgebases give bots RAG over documents, scoped like extensions: to
particular bots, to a workspace, or to every bot, with their own button
beside Tools in the composer. Documents are chunked and embedded by the
file-ingest worker with the install's embedding model, which is now an
install-wide setting rather than a per-bot one.
- Word search runs on an FTS5 index ranked by BM25, with a scored LIKE
fallback for scripts written without spaces.
- Changing the embedding model re-indexes documents automatically.
- Ingest jobs wait without spending attempts while every embedding model
rests, fail at once on errors retrying can't fix, and keep passages
searchable by words when the model refuses.
- Settings > Knowledgebases shows each knowledgebase's and document's
index state: spinner, warning, grey check (words only) or green check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MCP, skill and tool rows in Settings say "Updated 5m ago" beside how
they're reached and who gets them. updated_at now means a change to an
extension's settings (an edit, or any Access save): runs, connections and
MCP syncs keep their own last_checked_at / last_synced_at and no longer
touch it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Coordinator and worker come from bots.kind; bot_types now only holds
workspace_admin (was admin), each row naming its workspace, with no scope
field. Rows from v0.2.8 and earlier are fixed at boot by migrateBotTypes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The conversation page tells the rail and the sessions modal which
session it shows (announceShownSession, useShownSession); they reload,
so a session reopened by ?session_id= shows at once.
- Only that session is Current and greyed out (the active one without
?session_id=); the active row then leads back to it.
- The rail lists the session on screen even when it's older than its
latest sessions.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Bots get continue_in_new_session: the bot writes a title and summary,
the session closes for good (read-only) and the new one opens with the
summary as a "Carried over from the last session" message that links
back to the old session.
- Continue in new session (conversation menu, sessions modal) and the
automatic rollover at the session length do the same; New session now
starts a fresh, unlinked session.
- Session length is an app setting in Settings > General.
- ?session_id= opens a session; a carried-over one shows a bar linking to
where the conversation went on instead of the composer.
- The thread can open the previous session above the current one (shown
only, never in the prompt), and replies get search_past_sessions and
read_past_session.
- Sessions are named from their opening messages, not their latest, and
a carried summary is labelled as the earlier session's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A hook call used to record only how it was taken; its run's answer went
to the chat alone and was lost to the call once the job was pruned. Now
`bot_hook_events` keeps `response`, `response_message_id` and
`responded_at`, written on every call a run took in when that run ends
(completed, failed, lost, stopped or cancelled) by
`recordHookCallResponse` in the job helpers, and calls turn done or
failed. The Calls view searches what calls sent and what runs answered,
filters by status and pages; Show all loads one call whole. Bots get
`search_hook_calls` and `read_hook_call` for their own hooks (never in a
hook call's own run), with payloads always marked as outside data.
Calls are kept for the app setting `hook_events_keep_days` (Settings >
General, default now 30 days). The caller still only gets 202 Queued.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A hook's calls used to run in whatever session was active at the time.
Now `bot_hooks.session_id` pins them to one session, open or closed
(null still follows the active one). The hook form picks where it runs
(the bot's own chat or one of its workspaces) and a session there, or a
new one of its own started beside the active one so the chat on screen
stays. A bot's proposed hook gets the session it was proposed in, and a
missing session is replaced on the next call. Changing place or session
is refused while a run is open; the computer stays only if reachable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new personal env scope (paradigm user: one user's secret for one bot)
beats every other scope, reaches only that user's runs of that bot, and
is redacted like any secret. Bots take secrets in with request_secret,
which puts a [secret-request:ID] card in the chat: the user types the
value into a masked input there, never into a message, and the bot is
told only the key. Bots show secrets with capture_secret (a task's
computer tool that saves a file or a command's output straight to a
personal env, then redacts it from the rest of the run) and show_secret
(an env by key): both give a [secret:ID] chip whose Reveal and Copy fetch
the value on click, for its own user (personal) or a super admin
(anything else), logged by key. Values never go into messages, page
props, history, Copy chat or logs. Users manage their own secrets on
Settings > Secrets; the Environment page lists everyone's, remove only.
Deleting a bot or user removes its personal secrets, and SecretsNote
tells bots never to ask for or print secrets in chat.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each computer's Chrome profile now lives on the data volume
(DATA_DIR/computer-browsers/<id>, mounted at ~/.cobble-browser) and
Chrome is pointed at it with --user-data-dir. The sandbox image resets
its own profile's preferences to session-only cookies on every boot,
which signed every site out whenever Chrome exited. New profiles are
seeded with the image's preferences minus that rule, stale Singleton
links are removed before each provision, and existing computers are
recreated once (skipped while a job runs on them).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bots get list_hooks, pause_hook and delete_hook, which act directly since
they only take away. A new hook, or a new name, brief or filter, or
switching a paused one back on, goes through propose_hook: it stores a
hook_proposals row and puts a [hook-proposal:ID] card in the reply, which
the user it was proposed to confirms or rejects. The card is read from
what Confirm saves, with the bot's reason labelled as its own; confirming
checks everything again and saves through the Hooks modal's helpers. A
new hook's URL is shown on the card once, to the user only, never in the
outcome the bot is told. What calls a hook, its computers and its signing
are never proposed. Only chat replies propose, and a hook call's own run
gets no hook tools, so a caller's payload can't reach them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A bot hook (bot_hooks) is a secret URL, POST /api/hooks/<token>, whose
calls queue the hook's brief plus the raw body as a task, per user and
bot like a routine. Only the token's hash is kept (shown once, rotatable);
an optional HMAC-SHA256 signing secret is stored encrypted. Calls are
rate limited per address and per hook, recorded in bot_hook_events
(pruned daily), deduplicated by delivery id, and a call while the last
run still waits joins its brief instead of queueing another. The payload
is fenced data and HookRunNote tells the bot never to take instructions
from it; failed runs retry like routines.
A hook can instead watch a computer's browser notifications: the app
installs a small watcher that owns org.freedesktop.Notifications on the
sandbox's session bus, so Chrome hands it every notification (it only
looks for that service at start-up, so Chrome is restarted once, never
mid-task), and it posts each one to its hooks, optionally filtered.
Computers reach the app at COMPUTER_APP_URL or the app container's name.
Users make hooks in the conversation's Hooks modal and manage them all
on /admin/routines; bots never create or change hooks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A bot's instructions and memory for one workspace were only reachable
from the workspace's settings. The editor is now shared and also opens
from the conversation menu in that workspace ("Instructions here",
loaded on open through api/admin/workspaces/workspace-bot) and from the
workspace rows on the bot's settings page.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The conversation header menu gets a Copy chat row that puts the active
session on the clipboard as one markdown document: a summary, then each
message under its author and time, with quotes, attachments and
reactions. Bot and workspace refs read as names, message headings drop
below the message headings and code blocks stay untouched. The clipboard
write starts on the click with a pending ClipboardItem so Safari allows
it after the fetch.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
delegate_task takes a task_id to hand over a task already on a list: it
moves to the worker (a workspace's task stays on its list), goes to
doing, and the job is linked back so its end is noted on the task.
hand_over_schedule moves a bot's own routine to another bot, keeping its
workspace. Users get Hand over in the Tasks UI and both routine lists.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Settings > General gets an App icon (super admin): one of the seven
stones, or an uploaded image with the picked stone as a top-left corner
mark (inside a round launcher's crop, clear of the browser's badge),
previewed in both launcher crops before saving.
Uploads are decoded and cropped to a stored square (no SVG); the set is
drawn into DATA_DIR/install-icon/<version>/ and served publicly and
immutable. The manifest, head icon links and push notifications point at
it; the ember stone keeps the stock icons. The generator shares the icon
renderer.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Settings > General: an App name (super admin) for the installed app.
The manifest's name/short_name, the iOS head tags and the loading
page's title use it, so two installs on one phone can be told apart.
- Notifications get a real badge (white stone silhouette): Android masks
the badge by alpha, so the opaque app icon showed as a square.
- The loading and offline pages load the app's font.
- The font is now Archivo (variable, one request for every weight),
matching the marketing site.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The longest runbook a skill may save and how much of it load_skill hands
back are now set on Settings > General (skill_body_max_chars,
skill_body_shown_chars), defaulting to AppData.SkillBodyMaxChars and
SkillBodyShownChars. Saving, confirming and proposing a skill check the
install's limit; a skill saved before it was lowered can still be toggled.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
startDataBackupRun already holds the backup run lock, but the archive
(createDataBackup) and restoreDataBackup took it again, so every run
failed a few seconds in with "Another backup or restore is already
running". The writer and the restore now take no lock of their own.
The run's notice (archiving, restoring, or why it failed) moves to its
own line under the actions row so the buttons don't shift, and archiving
reads in gray rather than the warning colour.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A bot's powers came from bots.kind alone, so only the coordinator could
hand work to other bots. bot_types now works like user_types: a row with
no workspace holds everywhere (the coordinator), one with a workspace
only there (a workspace admin), and no row is a worker. bots.kind stays
the legacy marker of the one coordinator for who sees it.
The matrices in src/dict/bot-types-dict.ts say what each type may do,
whom it may direct and who grants it, read by one generic interpreter
(rbac/type-assignment.ts) that userTypeAssignment now wraps too. Every
run reads its powers once (grabBotRunPowers) and the chat tools, task
runs and task tools gate on them instead of kind; a run's powers are its
type's and its user's together.
In the workspace it leads, an admin hands tasks to the workers there
(results relayed back to it), sets their instructions, adds tasks to
their lists and keeps the workspace's context when its user may. Super
admins and admins grant it from the workspace's settings or the chat
header's menu, and the coordinator with set_bot_type. Admins are marked
with a filled green shield beside their name across the workspace, kept
live by pushBotUpdated's admin_workspace_ids.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A data archive or restore ran inside its request, which sent nothing
until tar finished: Bun closes a silent socket after 10s (its default
idleTimeout in production) and a proxy cuts it at about 100s, so the
page showed a network error while the archive kept going, and a retry
said another backup was already running.
The routes now start the run (startDataBackupRun, refused while the
backup lock is held) and answer at once. The page follows it by polling
api/admin/backups/data-status: an archive reloads once written, a
restore once the status comes from a new process after its restart,
and a failure shows its error. A run in progress survives a reload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Computers now mount only /home/gem/workspace from DATA_DIR/computers/<id>,
so only the workspace persists and goes into the data archive. An older
computer's whole home is moved aside to <id>-home-<ts> (kept, never
archived) and its container recreated once by sync-computers.
Each run works in bots/<bot>/users/<user> (also its cwd and HOME), with
bots/<bot> for work every user shares and workspaces/<id> shared by a
workspace's bots; the coordinator reaches the whole workspace. Computer
tools are gated by withComputerDirs: bash and code start there, file tool
paths and file: URLs outside the run's folders are refused, and so is sudo.
search_computers, import_file and script tools start there too.
The files browser shows members only the bots' folders they reach, their
own user folder in each and their workspaces', with names on the id folders.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Coordinator tools in its own chat (super admin only): list_extensions,
set_extension_access, delete_extension, and update_skill /
update_mcp_server / update_custom_tool for what can't change where
anything is sent (on/off, names, arguments, left-out tools)
- Adding a skill, MCP server or tool, or changing a skill's words, a
server's URL or headers or a tool's kind or config, goes through
propose_skill / propose_mcp_server / propose_custom_tool: stored in
extension_proposals with the bot's background check, shown as a
[proposal:ID] chip that opens a card with Confirm and Reject
- The card's facts are worked out by the app from what Confirm saves: the
host, every {{env.KEY}} and whether it's saved, the config; for a skill
its links, hosts, tool names, mentions of secrets and a diff against
the saved runbook
- Confirming decides once and checks again; a server or tool missing a key
is saved switched off, and the outcome is posted to the bot in chat
- Never from a bot: stdio servers, a URL whose host a template decides,
envs in a prompt tool, a key's value
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Activity page: "Delete all" empties the open tab (api/admin/logs/clear-logs,
super admin only, logged under system)
- Stop logging every WebSocket connect
- Log unexpected API errors (safeErrorMessage -> logUnexpectedError), every
failed bot job attempt and every job the queue lost
- Drop the cron jobs' console output; a failed model sync goes to the logs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Both tables lay their heading and rows on one shared grid, so every
figure sits under its own heading (twui's Row wrapped the cells).
- The selected range reads as pressed: ink-filled in a hairline track.
- Each day's bar opens a small summary card on hover or tap.
- twui 0.1.7 places popovers before paint, so opening one no longer
makes the layout flicker.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A reply read the session once at the start, so a message sent while it
ran was never seen, and the follow-up reply queued for it then finished
silently: the message was saved before the reply, so the thread ended
with the bot. Those messages went unanswered.
The text loop now reads new user messages after every round of tools and
before an answer goes out, and the bot decides from what each one means
whether it changes the work (folded in, or the unsent answer redrafted)
or is a separate request, which it acknowledges at once with the new
send_message tool and does after the current work. Messages sent that
way are marked interim, so the thinking row stays up while the bot
works. A reply job records the newest user message it read, and a queued
reply only stays silent when a finished reply already read everything.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every bot message gets an info button that opens its details: the
provider and model behind it, tokens and cost, the job that ran it, each
model used and every call in order. Each model call now records how it
was made in token_usages.settings (generation settings sent, whose
settings they were, pinned or automatic model, fallbacks), shown per
model in the modal.
Live replies now carry their token usage, from the job's push and the
live relay alike, so it shows without a reload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The conversation pages rendered their messages on the server, so a page
the back button restored from the back-forward cache showed an old
thread. The server now renders everything else (rail, header, composer)
and the history (active session, latest messages, active task) loads
from api/admin/bots/conversation-history once the page is on screen,
with a loading line under the header, and again on a pageshow restore.
Messages sent or pushed while it loads are merged with it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Media rows can no longer be created through the CRUD API, updates only
take description fields on the caller's own row, and every media read
or delete goes through mediaFilePath, which refuses paths outside
DATA_DIR or inside the database folder.
- Uploads strip the file name and extension to safe characters and keep
only title, description and summary from the request's media object.
- Folder imports no longer follow symlinks (walkRegularFiles), and the
source path is quoted in the computer's shell.
- User and bot files are served with nosniff, and HTML, SVG, XML and
scripts are sandboxed by CSP.
- Sandbox cookies are kept to the computer's prefix, and websocket
upgrades on /ws and the computer proxy must come from the app's own
address (README and Help note nginx's Host header).
- user_types CRUD writes go through the assignment matrix; members only
read their own user record; read-only users can read CRUD again;
grabUsers builds valid SQL for array search terms and bad pages; an
admin media DELETE needs an id; read-only users can't delete media.
- Rate limits and the audit log use the connection's address, trusting
forwarded headers only from a proxy, and login limits are split per
address, per account from that address and per account overall.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A computer a bot names in a reply is now a button that opens it in the
shared computer viewer modal, the bot's own computer included (sunken and
checked, as before). Switching stays in the header's computers modal, so
the chip has one job. A computer the bot can no longer use stays a plain
name.
The trail is twui's Breadcrumbs reading Home, the sections and the page
off the address, rendered as the first thing in SettingsShell's content
column, so every page that is not a conversation gets one without a page
touching it.
The last crumb is named by the page's own title rather than the address:
a record reads Scout instead of its id, /admin/logs reads Activity, and
/admin/settings/mcps reads MCPs. That replaces the breadcrumb_title prop
the four settings record pages had to pass, so settings-breadcrumbs.tsx
goes.
hasBreadcrumbTrail drops the trail on the crew of bots, home, and on
anything outside /admin; the trailing slash and query string are ignored,
so a folder in the files browser keeps the trail of the page it is on.
The two limits were compiled in: a task run stopped at
AppData.HarnessMaxToolSteps (200) and a chat reply at
AppData.BotReplyMaxToolSteps (8), so an install that needed a longer task
had to rebuild.
Both are now `app_settings` rows (`harness_max_tool_steps` and
`bot_reply_max_tool_steps`), read at the two loop call sites through
grabHarnessMaxToolSteps and grabReplyMaxToolSteps, and edited by the super
admin on Settings > General beside the prompt appendix. Zero clears a
limit back to the code default, like the bot file quota.
readStepLimit keeps the saved number only when it is a whole number of at
least 1 and no more than AppData.BotStepLimitMax (1000), so a blank, a
zero or a typo falls back instead of stopping every run before its first
turn, and a huge number still ends in the wrap-up report.
runTextLoop keeps AppData.HarnessMaxToolSteps as its default, so it stays
free of database reads and its tests run as they did.
Sending a message while scrolled up left it out of sight: the thread only
ever anchored its bottom, so nothing brought the view down. The column now
follows new messages while it sits at the bottom, stays put while it's
scrolled up, and always brings a message the user just sent into view.
While the thread sits above its bottom, "Jump to latest" floats over it.
Whether the thread is at its bottom is read from where the content ends
rather than from `scrollTop`, whose sign a reversed column reports
differently across engines. Content that grows after it lands (an image,
a wider window) is followed too, so the newest message never slips under
the composer.
Nothing anywhere recorded what a model call cost: the harness used a
provider's usage numbers only to decide whether a turn had finished, then
dropped them. So a reply could not say what it took to make, one model
could not be compared with another on price, and there was no way to ask
what a day, a week or a month of the crew's work came to.
- `token_usages` records one row per call: user, bot, workspace, session,
job and message, the harness, the provider and model that actually
answered, the prompt and completion counts, and the cost when the
provider reports one.
- `harness-usage-scope.ts` carries those ids in an AsyncLocalStorage
scope that `run-bot-job.ts` enters once, so a run's turns, its session
summary, its transcriptions, images and embeddings are all attributed
without threading ids through every harness function. Nested runs merge
rather than replace, and a null genuinely clears a field (a bot's own
chat has no workspace).
- Work with no run behind it is charged to its owner instead:
`run-with-fallback-scope.ts` reaches the workspace's creator else the
bot's, and deliberately does not override a real run, so an ingest a
bot asked for is not misattributed to the bot's owner.
- All six harnesses record through `withCandidates`'s `on_success`.
Recording never throws: a model that answered must not lose its answer
to a failed insert.
- A job's calls land before its message exists, so `complete-bot-job.ts`
back-fills `message_id` once the message is inserted.
- The message's actions row shows what it cost, beside the timestamp and
the play button, with the breakdown in its title.
- `/admin/usage` totals the install's spending for 7, 30 or 90 days: the
total against the previous period, a day-by-day chart, a split per kind
of call, and a table per model with the price per thousand tokens
worked out from cost and tokens, which is what makes two models
comparable. Crew managers only, since it is the whole install's work
rather than one user's conversation. It sits with Routines and Activity
in the user popover rather than under Settings, because it is a record
of what happened, not something to configure.
Days are grouped in local time (`date(created_at / 1000, 'unixepoch',
'localtime')`), matching the local midnights the window builds and the
`YYYY-MM-DD` buckets it looks up. Grouping by the bare UTC day put a late
evening's calls under a name no bucket carried, so they vanished and the
chart's total silently disagreed with the totals card; a test pins it.
`canSeeRailItem` now decides who sees a popover or rail entry. The two
each filtered with their own inline rule and only one understood
`can_view`, so a gated page could be offered in the popover to everyone.
The thinking row and the header menu both carry Stop while a bot has work
open for the user anywhere: every pending or processing job of that bot in
their conversations is failed as stopped, its task closed, a Stopped step
recorded and one closing message posted per session, so the thread stops
waiting and the bot goes idle. The handler running the job is aborted from
this process through a registry, so a job stuck waiting on a model stops at
once instead of on its next heartbeat, and a late face change after the
abort is dropped.
The release stage is now said in the app too: Settings > General reports
"Cobble v0.1.5 (alpha)" next to development, and the README and RELEASING.md
say what alpha means for installs.
The header menu had Tasks but nothing for the schedules whose results
land in that conversation. It now has Schedules, right under Tasks,
opened in a modal that lists only what runs there: a bot's own chat holds
the routines that run nowhere, a workspace only that bot's routines in
it (`routineScopeSql`, tested against SQLite).
The list loads when the modal opens, through
`api/admin/bots/list-routines`, which checks the user chats in that
conversation first, so the page props stay small. `grabCronJobs` gained
an optional `workspace_id`: left out it still means everywhere.
The routines page's row, form and edit modal moved to
`src/components/general/`, and its subtitle to `src/utils/`, since both
the page and the modal use them.
A new super admin settings page, /admin/settings/database, lists every
table with its record count and size, and one page of the open table's
records newest key first. Values are cut inside SQL (400 characters, with
a flag saying so) and a record opens whole in a modal, so a table of long
texts reads without shipping it all. A search matches every column's
text, word by word or as a phrase, which is the one thing that also works
on a virtual table. Nothing in it writes: the catalog, the records and
the single record are all reads, GET only.
The General page ends with a Version block naming the release this
server runs, so an update can be confirmed from the app.
The version is COBBLE_VERSION, the release tag the image was built with
(Dockerfile ARG, passed by the release workflow, which falls back to
package.json's version on a workflow-dispatch build), else package.json's
version, which a development server reports too, with (development)
after it. runningVersion (src/utils/running-version.ts) drops an
unusable version string rather than showing it.
Also includes the working tree's pending signup guard in
src/pages/__root.server.ts: a user who still holds a session while the
install has no users is sent to /auth/logout (and may load it).
Give each env row its own horizontal padding so the key lines up with the
group's other content, and reflow the props and description literals in
env-row and envs-group.
The copy button sat on its own row above the group's time, costing a
line of vertical space for nothing. It now shares that row, left of the
timestamp, the way a bot message's actions already do. A file-only
message leaves no gap: only messages with text get a button, picked by
copyableMessages.
A skill is a runbook: a name, a unique slug, a one-line description of when
it applies, and the markdown body. A run only ever sees the slug and
description (one load_skill tool carries them), and reads the whole body
only when it calls the tool, so a long runbook costs nothing until needed.
Skills are the third extension kind beside MCP servers and custom tools, on
the same botExtensionsSql path, so per-bot, per-workspace and all-bots
grants work the same way, and attachDefaultExtensions hands an every-bot
skill to new bots. load_skill is a reserved tool name, so a custom tool
cannot shadow it, and a built-in wins a clash.
Managed on Settings > Skills; ExtensionRow links each kind to its own page
and a skill never reports a failure, since nothing connects or runs.
A bot's identity was one of seven hues, and the hue picked both its colour
and its stone shape. Now its colour is free and its stone is its own:
- `bots.color` holds any `#rrggbb`, `bots.stone` the flat-cut shape it's
drawn on. `bots.hue` stays as the palette column they replaced, read only
as a fallback; `src/scripts/move-bot-colour-to-owners.ts` backfills an
install from it (safe to rerun).
- `src/utils/bot-color.ts` is the only place a colour is validated or
resolved (`resolveBotColor`/`resolveBotStone` carry the old hue forward,
`botColorNeedsPaperFace` flips the eyes to paper on a very dark body), and
`src/utils/bot-stone-props.ts` gives every row its props, so a row saved
before this still renders as it did.
- `BotStone` takes the colour as the body's SVG `fill`, the one documented
exception to the token rule: an arbitrary hex has no token to point at.
`UnreadDot` follows it.
- The picker keeps the palette tiles (still shape and colour together) and
adds a native swatch and a hex field, so a custom colour keeps its stone.
Same place as before: the header avatar's modal and Settings > Bots, for
crew managers. The coordinator stays hue-less.
- A recolour reaches other open tabs through `pushBotUpdated`.
Bot and user messages get a quiet copy button under them: the copy icon becomes a check for a moment after it copies the message's text as stored, markdown and refs included. A message still on its way to the server can be copied too, though reply and reactions wait for its id. The async clipboard API is used where it exists and a hidden textarea where a plain HTTP install has none.
The install-wide appendix now uses the shared CodeEditor (AceEditor,
markdown mode) instead of a plain textarea, discarding by remounting the
editor with the saved text. The storage quota field moves to the twui
0.1.6 number input, so the dependency is bumped with it.
`/admin/bots` sent the super admin to the coordinator and everyone else to
their latest bot, which lost the thread each time the tab was reopened. The
conversation pages now record where the browser is in two HttpOnly cookies
(`cobble_last_visited_bot_id`, `..._workspace_id`, a year, `Secure` only over
HTTPS), and the landing page opens that conversation again, in its workspace
when it was one.
A stale cookie can't loop: `grabReachableConversation` re-checks it with
`grabUserConversation`, the gate the conversation pages use, so a revoked
grant, a deleted bot or an archived workspace falls back to the coordinator
(super admin) or the latest bot, first workspace, empty page. Signing out
clears both cookies so the next user on that browser doesn't land in the
previous one's conversation.
Settings > General gains a prompt appendix the super admin sets once:
markdown every bot runs with, right after its own core instructions.
- The text lives in app_settings (bot_system_prompt_append, read through
grabSystemPromptAppend, cached like every app setting), so it changes
without a deploy and applies to every bot on the next run.
- composeBotPrompt takes an appendix and pushes it as
# Install-wide instructions directly after system_prompt, before the
workspace block and memory, so the bot's own more specific rules still
win. grabBotPrompt feeds it in, so chat replies, tasks, scheduled runs
and the coordinator all get it with no call site touched.
- api/admin/settings/system-prompt-append saves it (super admin only,
capped at AppData.PromptMaxLength, empty clears it) and is audit
logged under system.
- The section and its form are hidden from everyone but the super admin,
save in place with a character count and no reload, and say what an
empty text does.
Settings > Backups gains a whole-data archive beside the database
snapshots, and both lists now fully work from the page.
- Data archives: `write-data-backup-archive.ts` tars all of DATA_DIR into
BACKUP_ARCHIVE_DIR as cobble-data-<epoch-ms>.tar.gz, every entry under one
root folder named after the archive, writing a .part name and renaming on
success so a cut-off run is never listed. The newest
AppData.DataBackupsKept are kept; db/.backups is left as it is.
- Restore: `restore-data-backup.ts` extracts to a temp dir, requires one
root folder holding a db folder, takes a safety archive first, then
replaces DATA_DIR (the database snapshots included) and `restart-app.ts`
restarts the process through the docker socket, since the open SQLite
handle points at the replaced file.
- Upload and restore: a modal over each list (database file or data
archive), replacing the old /admin/settings/backups/restore page.
- Delete for both kinds, icon-only row actions shared through
RowIconButton, and restore on every row.
- One backup or restore of either kind at a time (with-backup-run-lock).
- BACKUP_ARCHIVE_DIR is a start-up path (ProcessOnlyEnvKeys); the installer
mounts $COBBLE_HOME/backups-archives at /backups-archives and recreates a
container that lacks it. The image installs tar and gzip.
A bot's folder chip went to bare /admin/files instead of the folder it
named, and even once there the computer volume pointed at the wrong
directory, so the folder a bot linked to looked empty.
The chip lost its folder because folderRefUrl only accepted a ref's
volume name (computer) while the chip hands it the browser's
(computers), so storageVolumeFromRef found nothing and the whole query
was dropped. Both spellings resolve now, through parseStorageSpec, so a
bad owner falls back cleanly instead of emitting owner=0.
The deeper bug: a computer's home was assumed to be
DATA_DIR/computers/<id>, but on a bind mount it can be anywhere on the
docker host, and a computer the app didn't provision (the main one on a
server reached at MAIN_COMPUTER_URL, i.e. a dev server outside docker)
records nothing at all. resolveComputerHomeDir holds the precedence:
the directory provisionComputer actually bound in
(containers.metadata.data_dir), else the new MAIN_COMPUTER_DATA_DIR app
setting for the main computer, else the conventional folder.
A path that isn't on disk also no longer reads as an empty folder:
listStorageEntries reports it as missing and the page says so, so a
stale folder link can't look like a folder nothing has written yet.