176 Commits
Author SHA1 Message Date
tbenandClaude Opus 5.5 89c4e26d06 Release v0.2.2
CI / check (push) Canceled after 0s
Release image / image (release) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
v0.2.2
2026-10-25 09:58:21 +01:00
tbenandClaude Opus 5.5 d67cd6a27a Close the proposal modal once a proposal is decided
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:57:28 +01:00
tbenandClaude Opus 5.5 2cbf6a6e6c Compress AGENTS.md to its rules, invariants and pointers
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:56:18 +01:00
tbenandClaude Opus 5.5 373210c2a5 Make the skill runbook limits a general setting
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]>
2026-10-25 09:43:00 +01:00
tbenandClaude Opus 5.5 2408185a65 Release v0.2.1
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:25:43 +01:00
tbenandClaude Opus 5.5 577523de3c Only ignore the root logs folder
The bare "logs" pattern ignored every folder named logs, so
api/admin/logs/clear-logs.ts never reached the repository or the
production build. Anchor it to the root and add the route.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:24:55 +01:00
tbenandClaude Opus 5.5 b9a49ee09c Fix data archives and restores failing on their own lock
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]>
2026-10-25 09:24:55 +01:00
tbenandClaude Opus 5.5 410d05e0b1 Release v0.2.0
CI / check (push) Canceled after 0s
Release image / image (release) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
v0.2.0
2026-10-25 09:06:34 +01:00
tbenandClaude Opus 5.5 19fac97d18 Default lucide icons to 16px
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 09:04:14 +01:00
tbenandClaude Opus 5.5 ea62dd11f0 Add bot types with a permission matrix, and workspace admin bots
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]>
2026-10-25 09:04:14 +01:00
tbenandClaude Opus 5.5 4161863587 Run whole-data archives and restores in the background
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]>
2026-10-25 08:34:29 +01:00
tbenandClaude Opus 5.5 86becd50bc Keep every bot's computer work in its own workspace folder
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]>
2026-10-25 08:24:49 +01:00
tbenandClaude Opus 5.5 fefc295d15 Let the coordinator manage skills, MCP servers and tools, behind a confirm card
- 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]>
2026-10-25 06:08:09 +01:00
tbenandClaude Opus 5.5 e6c278198b Quiet the logs and capture the errors that were missing
- 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]>
2026-10-25 05:11:31 +01:00
tbenandClaude Opus 5.5 3a9e57f682 Fix the usage page's tables, range tabs and day bars
- 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]>
2026-10-25 05:01:24 +01:00
tbenandClaude Opus 5.5 b0fe619ae8 Let bots take in messages sent while they reply
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]>
2026-10-25 04:49:10 +01:00
tbenandClaude Opus 5.5 c13b9ea826 Show how each bot message was generated
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]>
2026-10-25 04:33:47 +01:00
tbenandClaude Opus 5.5 feec69adca Share every bot and workspace picture with everyone signed in
A bot's or workspace's picture was only shared when its media row carried
the bot-image or workspace-image paradigm. A picture set before pictures
were copied into their own row points at the chat upload itself, so only
its uploader could load it and everyone else saw the default icon. A
picture is now shared because a bot or workspace points at it, whatever
its paradigm; profile photos still have to be the user's primary one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 04:08:16 +01:00
tbenandClaude Opus 5.5 d542b4e9ad Load a conversation's history on the client
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]>
2026-10-25 03:55:38 +01:00
tbenandClaude Opus 5.5 cdb93ac1bc Fix file access, XSS and access-control bugs from a security scan
- 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]>
2026-10-25 03:38:07 +01:00
tben 2e8e8de976 Release v0.1.8
CI / check (push) Canceled after 0s
Release image / image (release) Canceled after 0s
v0.1.8
2026-10-23 18:00:06 +01:00
tben e75ab28041 Open a computer chip in the viewer instead of switching to it
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.
2026-10-23 17:58:08 +01:00
tben 4261535741 Tell every bot its own id, in every run
The bot's id never reached its prompt: `composeBotPrompt` only took its
instructions, the appendix, its memory and its workspace, and
`grabBotPrompt` never passed `bot.id` or `bot.name` on. A bot could only
learn its id by accident, from a tool description that named it, so
"which bot am I" was unanswerable in most contexts.

- Open the prompt with who the bot is (`composeIdentity`): its name,
  whether it's the coordinator or a worker, its role, and its id, said
  to be how tools and refs point back at it. `grabBotPrompt` now passes
  the bot, which covers chat replies, tasks, routines and delegated
  tasks, since both runners build their prompt there.
- Turn the notes' example refs into functions bound to the running bot:
  `chatReplyNote`, `botFilesNote` and `coordinatorReplyNote` take its id,
  so the folder-ref example no longer points at a hardcoded `owner_id: 5`
  (another bot's folder) nor `[bot:3]`. ComputerFilesNote keeps the
  computer it's given.
- Stop `create_task` from hiding the worker's own id in its own chat.

Tests: four new `composeBotPrompt` cases pin the identity block (worker,
coordinator, unnamed, no bot at all), and a new `harness-prompts.test.ts`
checks each note's own-folder and self refs point at exactly the running
bot, for three ids.
2026-10-23 17:40:28 +01:00
tben 0166f31cfc Release v0.1.7
CI / check (push) Canceled after 0s
Release image / image (release) Canceled after 0s
v0.1.7
2026-10-23 16:50:51 +01:00
tben caa9ad87e4 Remove sticky breadcrumbs 2026-10-23 16:49:52 +01:00
tben 30a4df183c Give every admin page a breadcrumb trail
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.
2026-10-23 16:49:01 +01:00
tben ee687e1d31 Chunk Tailwind classes with tailwind-merge
Say it once in AGENTS.md: a className is twMerge(...) with one chunk per claim, so a later chunk beats an earlier one instead of depending on CSS source order.
2026-10-23 16:42:01 +01:00
tben 024d847b9a Let the install set how many steps a run may take
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.
2026-10-23 16:20:38 +01:00
tben 06586a6dc8 Release v0.1.6
CI / check (push) Canceled after 0s
Release image / image (release) Canceled after 0s
v0.1.6
2026-10-23 16:05:01 +01:00
tben 55fc5ee97b Follow the thread's bottom, and offer a jump back to it
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.
2026-10-23 16:03:37 +01:00
tben d9aea8e9c8 Record what every model call spends, and show it
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.
2026-10-23 15:41:41 +01:00
tben 0048324d8a Let a delegated task choose the workspace it runs in
delegate_task had no workspace argument: a task handed over from the
coordinator's own chat always ran in the worker's own chat, on that
worker's memory, files and directly assigned computers alone, even when
the work belonged to a workspace. The coordinator could not say where the
work goes, and the prompt never said the two differ.

- delegate_task takes an optional workspace_id: the worker runs the task
  in that workspace's context, with its own instructions and memory
  there, the project's files and the workspace's computers, and the
  result lands in its conversation in that workspace.
- A worker missing from that workspace is added first (pushBotCreated)
  and the reply says so.
- Handed over from inside a workspace the task always runs there; a
  different workspace_id is refused instead of ignored.
- The coordinator prompt, CoordinatorReplyNote and the tool description
  now spell the distinction out.
2026-10-23 14:25:21 +01:00
tben f7ef218507 Take the Gitea registry out of CI and mirror it separately
CI / check (push) Canceled after 0s
The Gitea registry is on a VPN, which a GitHub runner can never reach, so
the mirror step in the release workflow could only ever fail: its last four
releases reported a step that did nothing. The workflow now builds and
pushes to GHCR alone, which is the registry the installer defaults to, and
nothing in CI mentions Gitea.

The image reaches the Gitea registry through `scripts/mirror-gitea.sh`
(`bun run mirror:gitea`) instead, run in parallel with a release and from the
VPN side: it copies from the Gitea host over ssh, where skopeo can see both
registries, so nothing has to be installed there. `skopeo copy --all` keeps
the manifest list and its signatures, and every destination tag is read back
and compared with GHCR's digest, so a partial copy fails loudly instead of
being assumed to have worked. `latest` and `major.minor` are only moved while
that release still owns them in GHCR, so mirroring an older release never
rolls them back.

Neither direction can fail the other now, which is the point: a registry that
is down, slow or full no longer takes a release with it.
2026-10-23 12:03:23 +01:00
tben aac5d41474 Release v0.1.5
CI / check (push) Canceled after 0s
Release image / image (release) Canceled after 0s
v0.1.5
2026-10-23 10:21:06 +01:00
tben fcad4b49d6 Let a user stop a bot, and say the app is still alpha
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.
2026-10-23 10:19:41 +01:00
tben 5beb5f3de4 Let a conversation's menu show the routines that run in it
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.
2026-10-23 07:23:15 +01:00
tben 796c53716f Show a mirror failure, and link the release's own range
CI / check (push) Canceled after 0s
2026-10-23 06:54:06 +01:00
tben 5af2f0362a Fetch the tags in CI so the publish tests can read them 2026-10-23 06:13:14 +01:00
tben b3afda9ed8 Stop the tamper test from tampering with its own prefix
CI / check (push) Canceled after 0s
Release image / image (release) Canceled after 0s
v0.1.4
2026-10-23 06:10:41 +01:00
tben eb29e2bca9 Bump the version to 0.1.4 2026-10-23 06:10:05 +01:00
tben 41bb4aaac2 Let a super admin read the database from the app
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.
2026-10-23 06:03:42 +01:00
tben 9b26c169d6 Let a text run take 200 steps
A task that works through a folder or a long computer session was
running out of steps before it could report.
2026-10-23 06:01:38 +01:00
tben b5c3b7d126 Share every picture with everyone signed in
A profile photo, a bot's picture and a workspace's picture all come from
one condition now (sharedPictureMediaSql), so a face is never blank just
because its owner isn't in the viewer's rail. Anything that isn't a
picture stays behind its own check.
2026-10-23 06:01:34 +01:00
tben 93e3a4e3b0 Say what media.text_content is for in the schema
The column holds extracted text only; raw binary never goes in it.
2026-10-23 06:01:24 +01:00
tben f2802c1bf3 Let bots set a picture from a file they can see
A coordinator asked to set an uploaded logo as a workspace picture could
not: the picture tools take a media id, and nothing ever gave the bot one.
Attachments reached the model as "[Attached image \"logo.png\"]", every
file tool answers with a [file:ID] ref, and an SVG is deliberately stored
as a document rather than an image.

- copyMediaAsPicture copies an approved image into a new picture row
  (bot-image/workspace-image). The source keeps its row and bytes, so a
  message that attached it still loads and changing a picture can never
  take a file away from a conversation.
- grabPictureSource resolves a picture by [file:ID] (the same scope gate
  every file tool uses) or by the user's own upload; parseFileRef reads
  the [file:12] form.
- The new set_picture tool ships on every run: a bot sets its own face,
  the coordinator can also dress a worker. update_workspace gained
  picture_file_id, and setBotPicture/setProfileImage share the copy and
  the delete.
- removeReplacedPicture deletes a replaced picture only when it really is
  one, never a chat upload.
- uploadAndRecordMedia writes images in their own format: PNG stays PNG
  with its transparency, WebP stays WebP, GIF becomes PNG, everything
  else JPEG, and the row records the real mime type (it used to say jpeg
  for every image).
- Pictures change live: BotUpdatedData carries image_media_id and a new
  server:workspace-updated pushes workspace pictures, relayed between
  processes.
- Attachments are labelled with their media id, and BotFilesNote tells a
  bot that a [file:ID] can become a picture.
- grabRunScopes is now the single source of a run's reachable folders,
  shared by the file tools, imports and the picture tools.
2026-10-23 05:24:55 +01:00
tben d3357cb7df Stop storing raw file bytes in media.text_content
`text_content` is an extracted-text cache, but nothing stopped it from
holding a dump of bytes:

- `fileToText` ended its switch with `default: return buffer.toString("utf-8")`
  and defaulted a missing extension to "txt", so any binary was decoded.
- Both extraction callers gated on `media_type == "document"`, and
  `attachmentFileKind` classes everything non-image/audio/video as a
  document: an SVG, a zip, a spreadsheet, a .bin.
- Nothing capped the length, so a 20 MB attachment could leave 20 MB of
  text in the database.
- `storeChatAttachments` copied the text onto a second `media` row, and
  `SELECT m.*` pulled the column into page props and API responses.

On a real install an SVG held 6852 characters of markup and a
`.designcraft` file held 142 KB of raw bytes.

Changes:

- `file-to-text.ts` lists the text extensions explicitly and returns
  undefined for anything else; no extension is no text.
- `can-extract-text.ts` is the one allowlist (reusing
  `BotTextFileExtensions`), used by both upload routes and by
  `uploadAndRecordMedia`; `extension-of.ts` is shared with the storage
  catalog instead of duplicated.
- `BotAttachmentTextStoreMaxChars` (200k) caps the column via
  `cap-text-content.ts`.
- `store-chat-attachments.ts` no longer copies the text; ingestion
  re-reads the bytes once.
- `media-columns.ts` selects every media column but `text_content`, which
  now has to be asked for (`grab-messages-media`'s reply path and
  ingestion do); the media CRUD routes sanitize rows they write.
- `ingest-bot-file.ts` decides on size alone, since a cached slice made
  an oversized file look already read.
- `bun run media:prune-text` cleans rows that were already stored
  (rerunnable, with a VACUUM hint).

Verified on the dev database: both bad rows cleared (146 KB), rerun
reports nothing to do.
2026-10-23 05:22:04 +01:00
tben 5475241371 Move the notification settings onto the general settings page 2026-10-23 04:11:21 +01:00
tben a5eb2b6a95 Let the coordinator set a workspace's picture
CI / check (push) Canceled after 0s
update_workspace now takes an image media id (or null to remove the
picture). updateWorkspace writes image_media_id through
resolveWorkspacePicture, which only accepts the caller's own image and
deletes the picture it replaced, so a bot can never show someone else's
file or drop a picture it is still using.
v0.1.3
2026-10-22 21:10:06 +01:00
tben 34667581b6 Tighten the coordinator prompt around how a run actually works
The prompt had grown to a policy document: 13 sections, the same rules
repeated across them, and a state vocabulary no tool accepts. It also
never mentioned the reply/task split, so the model would burn a reply's
8 tool steps on work that belongs in a 50-step task.

- Lead with work placement: answer directly, start_task for its own long
  work, delegate_task for a worker, and say work is under way, not done.
- Replace the invented Requested/Queued/Verified list with the real
  TaskStatuses and update_task.
- Name the 19 tools it tells the coordinator to use, and which tool
  writes which instruction layer, instead of "the available tools".
- Give routines their own section; create_schedule was uncovered.
- Say each safety rule once, and keep the interrupt list short so the
  prompt reads as proactive rather than timid.
- Drop the commented-out previous prompt: that is git's job.

The test asserted a heading and a section that no longer exist, so it was
already failing. It now checks the heading, the SiteData name, the
absence of em dashes, the real statuses, and every tool named.
2026-10-22 20:14:08 +01:00
tben 2398314703 Give the coordinator a fuller prompt, and show bot instructions in find_bot
The coordinator's default prompt was a page of job descriptions. It now
states the working rules it kept getting wrong: a queued task is not a
running one, a tool result is the only proof an action happened, an
approved text is written whole, and an explicit constraint such as "no
computer" is binding. The prompt is attached at creation only, so this
reaches new installs and a coordinator whose prompt is still empty.

find_bot now returns each match's instructions, so the coordinator can
judge a worker by what it was told rather than by its name.

(The previous prompt is kept as a commented-out block at the bottom of the
file.)
2026-10-22 20:11:03 +01:00