Let workers ask each other through the coordinator in chatrooms
A worker asks for another bot's help with ask_crew. The coordinator reads the request, asks the right bot (in a workspace, only its members), and sends the answer back into the asking bot's conversation, waking it. Each exchange is kept as a read-only chatroom: listed in the main rail after the workspaces, or in the workspace's rail for requests made there, with live status rows for each bot while it's open and a Stop button. Room sessions are hidden from every conversation list, other bots' words reach the model fenced as data, and turns, nesting and open rooms are capped. Closed rooms are pruned after a retention period and past a cap per place (Settings > General). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -68,7 +68,7 @@ Enables SQLite foreign keys (`enableForeignKeys`), `bunextInit()`, `ensureDocker
|
||||
|
||||
### Cron
|
||||
|
||||
`src/cron/index.ts` registers everything via `registerCronJob` (`src/cron/register-cron-job.ts`); one folder per task. In-process `Bun.cron`, UTC, never overlapping. Notable: `heartbeat`, `sync-harness-models` (hourly), `run-cron-jobs` (every minute, fires schedules), `sync-computers`, `sync-mcp-servers`, `prune-bots-queue`.
|
||||
`src/cron/index.ts` registers everything via `registerCronJob` (`src/cron/register-cron-job.ts`); one folder per task. In-process `Bun.cron`, UTC, never overlapping. Notable: `heartbeat`, `sync-harness-models` (hourly), `run-cron-jobs` (every minute, fires schedules), `sync-computers`, `sync-mcp-servers`, `prune-bots-queue`, `prune-chatrooms`.
|
||||
|
||||
### Admin CRUD API
|
||||
|
||||
@@ -77,7 +77,7 @@ Enables SQLite foreign keys (`enableForeignKeys`), `bunextInit()`, `ensureDocker
|
||||
### Crew access (bots, workspaces, computers)
|
||||
|
||||
- Bots, workspaces and computers belong to the install; their `user_id` is only the creator. Users reach them only through grants: `bot_users` (a worker's own chat) and `workspace_users` (every bot in that workspace).
|
||||
- `crewRole` (`src/utils/rbac/crew-role.ts`): `super_admin` reaches everything and is the **only** one who sees or chats with the coordinator; `admin` reaches every worker and workspace; `member` only their grants. `canManageCrew` (super admin + admin) creates/edits/deletes crew and grants access; DB helpers check it themselves (`assertCrewManager`).
|
||||
- `crewRole` (`src/utils/rbac/crew-role.ts`): `super_admin` reaches everything and is the **only** one who chats with the coordinator (a member sees it only inside their own chatrooms, read-only); `admin` reaches every worker and workspace; `member` only their grants. `canManageCrew` (super admin + admin) creates/edits/deletes crew and grants access; DB helpers check it themselves (`assertCrewManager`).
|
||||
- Access SQL lives in `src/utils/crew-access/` (tested against SQLite) and backs `grabChatBot`, `grabUserBot`, `grabUserWorkspace(s)`, `grabWorkspaceBot`, `grabBots`, `grabUserComputer(s)`.
|
||||
- Conversations, faces and status lines are **per user** (`bot_sessions.user_id`, `bot_user_faces`). A job runs for its session's user, with that user's memory and reach. Page reads overlay the viewer's face (`botFaceSql`). Status lines never name a task.
|
||||
|
||||
@@ -90,11 +90,11 @@ Enables SQLite foreign keys (`enableForeignKeys`), `bunextInit()`, `ensureDocker
|
||||
- **Tables:** `bots` (`kind`, `color` `#rrggbb`, `stone`, `role`, `system_prompt`; `hue` is a legacy fallback), `bot_user_faces`, `bot_sessions`, `bot_messages`, `bot_tasks`, `bot_task_steps` (unused yet). Enums in `src/dict/bot-dict.ts`.
|
||||
- **Sessions:** one thread per user per bot in its own chat plus one per workspace; split into `bot_sessions` (rollover at the app setting `bot_session_max_messages`, default `AppData.BotSessionMaxMessages`, via `grabBotSessionMaxMessages`; titled and summarised by `name-bot-session.ts` from the session's opening (`grabSessionOpening`; `grabBotMessages` reads the latest), summary carried into the next session only when they're linked by `previous_session_id`, which only a carry-over sets; older implicit links are cleared at boot by `clearImplicitSessionLinks`). New session (`startBotSession`) and a conversation with no active session always start fresh. The rollover carries over like the bot does (`rollOverBotSession` -> `continueBotSession`, falling back to the stored summary; kept for the next message while a job is open in the full session or there's nothing to carry), inside send-message, so the user's message lands under the summary. Helpers in `src/functions/backend/db/bot-sessions/`. Job results land in the job's own session, even after it closes, except a reply that called `continue_in_new_session` (`carryOverBotSession`; the menu's and sessions modal's Continue in new session does the same through `continueBotSession`, the model writing title and summary, refused while a job is open in the session): its summary is kept on the closed session and opens the new one as a bot message (`carried_from_session_id`), the job's `session_id` moves so the answer follows, the new session is titled by the bot then (it has no first exchange to be named from), and the old one can never be reopened (`reopenBotSession` refuses; a thread showing it swaps its composer for a link to the new one). Session lists (rail, sessions modal) reopen a session on click, as before; a carried-over one can't be, so it opens to read (`?session_id=`, `conversationHref`). The page tells them which session it shows (`announceShownSession` -> `useShownSession`): they reload, mark it Current (greyed out; without `?session_id=` the active one is), list it even when it's older than the rail's latest, and the active row then leads back. From half the session length the reply prompt suggests moving on (`longSessionNote`). Earlier sessions never enter the prompt (only the last one's summary): the thread can open them above a session (`useOlderMessages.loadPrevious`, a divider per session, quote and reply as usual), and replies get `search_past_sessions`/`read_past_session` (scoped to that user, bot and place, never the current session) when the conversation has any.
|
||||
- **Coordinator:** exactly one, the super admin's alone, created idempotently by `ensureCoordinatorBot`. Default prompt `src/data/coordinator-system-prompt.ts`, backfilled only when empty.
|
||||
- **Routes:** `/admin/bots` reopens the last visited bot (cookie `cobble_last_visited_bot_id`, never a workspace conversation) if still reachable, else coordinator (super admin) / latest bot / first workspace / empty. `/admin/bots/[bot_id]` conversation (header, thread and composer at 760px, no right panel; `?session_id=` opens another of the user's sessions there: a closed one is reopened, except a carried-over one, which shows with `SessionMovedBar` instead of the composer, pointing where the conversation went on); `/admin/bots/[bot_id]/models`; `/admin/workspaces/[id]` dashboard; `/admin/workspaces/[id]/bots/[bot_id]` same conversation scoped to the workspace.
|
||||
- **Routes:** `/admin/bots` reopens the last visited bot (cookie `cobble_last_visited_bot_id`, never a workspace conversation) if still reachable, else coordinator (super admin) / latest bot / first workspace / empty. `/admin/bots/[bot_id]` conversation (header, thread and composer at 760px, no right panel; `?session_id=` opens another of the user's sessions there: a closed one is reopened, except a carried-over one, which shows with `SessionMovedBar` instead of the composer, pointing where the conversation went on); `/admin/bots/[bot_id]/models`; `/admin/workspaces/[id]` dashboard; `/admin/workspaces/[id]/chatrooms` that workspace's chatrooms; `/admin/workspaces/[id]/bots/[bot_id]` same conversation scoped to the workspace.
|
||||
- **Conversation loading:** pages render everything server-side (`grabConversationProps`) **except the history**, which the client loads (`api/admin/bots/conversation-history`, merged with what's already shown, refetched on bfcache restore) and pages older messages on scroll. Rail lists go through `railBots`/`railWorkspaces` (no prompts/memory, previews cut). While a reply is due the page polls `session-messages` (`use-reply-catch-up`) in case a push was missed.
|
||||
- **Sending:** `api/admin/bots/send-message.ts` -> `enqueueBotReply` -> `wakeBotsQueue`. **Never import `src/bots-queue/index.ts` from a page or API route** (pulls the bunext runtime into page bundles). Optimistic bubble in `pending_messages` until the saved message replaces it.
|
||||
- **Composer:** Tiptap editor reading/writing markdown, drafts in `localStorage`, `@` bots / `#` workspaces written as `[bot:ID]`/`[workspace:ID]`. Enter sends (Mod+Enter in lists/code). `@tiptap/react` needs `use-sync-external-store`; the app ships an ESM stand-in in `packages/use-sync-external-store-esm` (file dependency + override; the Dockerfile copies `packages/` before `bun install`).
|
||||
- **Rendering:** markdown via `react-markdown` + `remark-gfm`, raw HTML never rendered, component map in `[bot_id]/(data)/message-markdown-components.tsx`. Inline refs, all linked by `link-computer-refs.ts` and rendered by chips in `(partials)/`: `[bot:ID]`, `[workspace:ID]`, `[computer:ID]`, `[file:ID]` (a saved file), `[folder:<volume>:<owner>:<path>]` (opens `/admin/files`), `[take-over:ID:mode]`, `[proposal:ID]`, `[hook-proposal:ID]`, `[secret:ID]` (masked env, Reveal/Copy), `[secret-request:ID]` (masked input card), `[graphic:ID]` (a bot graphic, framed inline). Parsers in `src/utils/*-ref.ts`.
|
||||
- **Rendering:** markdown via `react-markdown` + `remark-gfm`, raw HTML never rendered, component map in `[bot_id]/(data)/message-markdown-components.tsx`. Inline refs, all linked by `link-computer-refs.ts` and rendered by chips in `(partials)/`: `[bot:ID]`, `[workspace:ID]`, `[computer:ID]`, `[file:ID]` (a saved file), `[folder:<volume>:<owner>:<path>]` (opens `/admin/files`), `[take-over:ID:mode]`, `[proposal:ID]`, `[hook-proposal:ID]`, `[secret:ID]` (masked env, Reveal/Copy), `[secret-request:ID]` (masked input card), `[graphic:ID]` (a bot graphic, framed inline), `[room:ID]` (a chatroom). Parsers in `src/utils/*-ref.ts`.
|
||||
- **Copy chat** (header menu): the active session (the one on screen) as one markdown document (`api/admin/bots/conversation-export` -> `conversationMarkdown`, refs as names, message headings demoted). The clipboard write starts on the click with a pending `ClipboardItem` (`copyPendingText`), since Safari drops the gesture after a fetch.
|
||||
- **Replies/reactions:** `reply_to_message_id` (not an FK); reactions from the fixed `BotMessageReactions`, toggled by `react-message.ts` and the `react_to_message` tool, pushed as `server:bot-reaction`.
|
||||
- **Attachments:** uploaded on pick (`upload-attachment.ts`, private `media`, text extracted when possible), linked via `bot_message_media`. Replies read files of the last `BotReplyAttachmentMessages` messages. Tasks don't get files yet.
|
||||
@@ -121,6 +121,10 @@ Enables SQLite foreign keys (`enableForeignKeys`), `bunextInit()`, `ensureDocker
|
||||
|
||||
Server push only. `pushBotNotice` (`server:bot-message`, `server:bot-updated`), `pushBotUpdated` (to the bot's whole audience), `server:bot-created|removed`, `server:workspace-created|updated|removed`, `server:computer-updated`. Deletions push to the audience read **before** the delete. `liveRelay` (`src/live-relay/`) polls the DB every `AppData.LiveRelayPollMs` so work from other processes shows; clients dedupe by id. `useLiveBots` keeps rail and header current.
|
||||
|
||||
### Chatrooms (`chatrooms`)
|
||||
|
||||
A worker reaches another worker **only through the coordinator**: `ask_crew` (every worker run except hook calls; never the coordinator) opens a room (`createChatroom`) for the run's user and place. Who can be asked is the app's call (`chatroomAskableBots`): in a workspace its members, else every worker the user reaches. Every bot in a room has a hidden session there (`bot_sessions.chatroom_id`, status `chatroom`) holding its messages and jobs; **every session query that lists or picks a conversation must exclude them** (`chatroom_id IS NULL`). The coordinator's turns are queued straight onto the queue (`enqueueChatroomTurn`, it works for a member there) and run by `runChatroomTurn` with only `ask_member`/`answer_requester`/`refuse_request`, the room fenced as data (`chatroomTurns`). A room job ending (`onChatroomJobEnded`, from complete/fail) wakes the coordinator; closing (`closeChatroom`, once) cancels what's open and posts the outcome into the asking bot's session as a user-side message with `from_chatroom_id` (fenced for the model, shown as from the coordinator) and wakes it, except when stopped. Caps: `AppData.ChatroomMaxCoordinatorTurns`, `ChatroomMaxDepth` (nested rooms, `parent_chatroom_id`; a bot waiting up the chain can't be asked), `ChatroomMaxOpenPerBot`. Room jobs never change faces or name sessions; their messages push as `server:chatroom-message`/`server:chatroom-updated`, never as notifications. Rooms are read-only records kept per place, like conversations: a workspace's open under it (`/admin/workspaces/<id>/chatrooms[/<room>]`, listed in the workspace rail after its bots), the rest under `/admin/chatrooms` (after the workspaces in the main rail); links go through `chatroomHref` and a room opened at the wrong place redirects. The rail loads its section itself (`api/admin/chatrooms/list?workspace_id=`); `[room:ID]` chip. The user can only Stop one. Under the thread an open room shows where each bot stands (`chatroomBotStatuses`, from the summary's open `jobs` plus live `server:bot-job-update` steps); bots' faces stay out of it. **Retention:** closed rooms go after the app setting `chatroom_keep_days` (daily `prune-chatrooms`) and past `chatroom_max_per_place` rooms per place (also pruned as each room closes), oldest first (`pruneChatroomsSql`; open rooms and rooms an open one was asked from are never pruned; deletion through `deleteChatroomRows`).
|
||||
|
||||
### Workspaces
|
||||
|
||||
`workspaces` (`description`, shared `system_prompt`, default `computer_id` (not an FK), `archived`), `workspace_bots` (per-bot instructions and memory there; coordinator is an implicit member), `workspace_computers`. First computer linked becomes the default. Workspace models win over a bot's own, then the defaults. A bot's instructions and memory there (`WorkspaceBotModal`/`WorkspaceBotForm`, crew managers) open from the workspace's settings, the bot's settings and the conversation menu's "Instructions here" (loaded on open). Helpers in `src/functions/backend/db/workspaces/` are access-checked; structural ones are crew-manager only.
|
||||
@@ -214,7 +218,7 @@ Read-only browser of every volume (`src/utils/storage-volumes.ts`: bots, workspa
|
||||
|
||||
## Admin routes
|
||||
|
||||
`AdminSections`: `/admin/bots`, `/admin/workspaces`, `/admin/routines`, `/admin/logs`, `/admin/help`, `/admin/settings`. `/admin` -> `/admin/bots`. Every `/admin/*` page renders in the bots layout (`src/layouts/bots/`); there is no separate admin layout.
|
||||
`AdminSections`: `/admin/bots`, `/admin/workspaces`, `/admin/chatrooms`, `/admin/routines`, `/admin/logs`, `/admin/help`, `/admin/settings`. `/admin` -> `/admin/bots`. Every `/admin/*` page renders in the bots layout (`src/layouts/bots/`); there is no separate admin layout.
|
||||
|
||||
- **Rail pages** (`src/dict/rail-pages-dict.ts`): Routines, Files, Usage (crew managers), Activity (`/admin/logs`, super admin), Help (`(data)/help-topics.ts`). Visibility via `canSeeRailItem` for both the popover and the aside.
|
||||
- **Settings** (`src/dict/settings-pages-dict.ts`; add a sub page = folder + dict entry): `general` (theme per device, plus super-admin app settings: prompt append, run limits, skill limits; crew-manager storage quota; running version), `profile`, `notifications`, `change-password`, `secrets`, `models`, `environment`, `backups` and `database` (super admin), `bots`, `workspaces`, `computers`, `mcps`, `tools`, `skills`, `knowledgebases` (crew managers), `users` (`canListUsers`).
|
||||
|
||||
Reference in New Issue
Block a user