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]>
This commit is contained in:
@@ -203,7 +203,7 @@ Every folder on the data volume, browsable read-only: a bot's own, a workspace's
|
||||
- **Custom tools** (`CustomToolKinds` in `src/dict/custom-tool-dict.ts`): a name (`checkCustomToolName`: snake_case, never a built-in's, `ReservedToolNames`), a description, a JSON-schema `parameters` and a `config` per kind, built by `buildCustomTool` (`src/harness/tools/custom/`): `http` (`runHttpTool`: the app fetches, arguments URL-encoded in the URL, no body template sends the arguments as JSON), `script` (`runScriptTool`: bash/python/javascript on the run's computer, the task's or in chat the bot's current one, over the sandbox REST API in a subshell, computer envs set, arguments as JSON in `COBBLE_ARGS` and on stdin; left out without a computer), `prompt` (`runPromptTool`: one `generateText` turn with the bot's models). Super admins try one from the page (`testCustomTool`, the main computer for scripts): a saved one from its row (Try), or the form as it is, saved or not (`draft`, nothing recorded), from the Test panel in its modal (`custom-tool-test-panel.tsx`). Every run (a bot's call or a try; not bad arguments or a cancel) is saved as the tool's `status`, `last_error` and `last_checked_at` (`recordCustomToolRun`); editing resets it to `untested`.
|
||||
- **MCP servers** (`McpTransports` in `src/dict/mcp-dict.ts`): `remote` (URL plus templated headers, `connectMcpServer`) or `stdio` (a command run with the host's shell, `hostShell`, in `DATA_DIR/mcp/<id>` (`grabMcpDir`), after its `setup_command`, which `runMcpSetup` reruns whenever it changes (marker file) or on Reinstall; its env is only its own `env` map plus the shell basics, `grabMcpStdioEnv`, never the app's). Their tools are listed into `tools_cache` (`syncMcpServer`: on save, Refresh, and hourly for remote ones by the `sync-mcp-servers` cron) and named `<slug>__<tool>` (`mcpToolName`); the modal's Test connection checks the form as it is, saved or not, outside the pool (`testMcpDraft`, `testMcpServer`, `api/admin/mcps/test-mcp`, a new stdio server's setup runs in `mcp/0`); a run that can't connect, or connects again after failing, updates its `status` too (`recordMcpServerConnection`); runs build tools from the cache (`mcpHarnessTool`) and connect on the first call (`openMcpServer`). stdio servers live in a per-process pool (`src/harness/mcp/mcp-stdio-pool.ts`), stopped after `AppData.McpStdioIdleMs` idle, on save or delete.
|
||||
- **Check before and after acting:** every reply and task gets `ActionCheckNote` (`src/data/harness-prompts.ts`): look for an existing record before creating one (update it instead), and verify every action took effect before saying it's done. The tools back it: `update_memory` and `update_instructions` read the save back (`checkSavedText`, `grabBotSavedText`) and fail if it didn't take; `create_bot` refuses a name already on the crew, `create_schedule` a twin of one of the bot's schedules (`findDuplicateCronJob`: same place and same title, or same timing and brief), and `start_task`/`delegate_task` a task with the same title still open in that session (`grabOpenBotTask`); `create_workspace` already refuses a name in use.
|
||||
- **Replies and tasks:** `run-bot-reply.ts` answers from the last `BotReplyHistoryMessages` messages (`botMessagesToHarness`) with `ChatReplyNote`, the bot's computers (`compose-computers-note.ts`: what they are and that they're reached through `start_task`) and the chat tools, at most `grabReplyMaxToolSteps()` steps (the `bot_reply_max_tool_steps` app setting, else `AppData.BotReplyMaxToolSteps`), and finishes silently if the bot already had the last word (`enqueueBotReply` reuses a pending reply, so a burst of messages gets one). `run-bot-task.ts` runs the brief (`compose-job-brief.ts`) with `TaskRunNote`, the bot tools, the file tools and the job's computer tools, closing the MCP connection after. Both add `BotFilesNote` (where a bot's files live, that they persist, and to search before saying it doesn't know) and collect `attachments` for files the bot wrote with `attach`, which `completeBotJob` links to the result message. Retryable harness errors go back to the queue's backoff, the rest are `BotJobFatalError`.
|
||||
- **Replies and tasks:** `run-bot-reply.ts` answers from the last `BotReplyHistoryMessages` messages (`botMessagesToHarness`) with `ChatReplyNote`, the bot's computers (`compose-computers-note.ts`: what they are and that they're reached through `start_task`) and the chat tools, at most `grabReplyMaxToolSteps()` steps (the `bot_reply_max_tool_steps` app setting, else `AppData.BotReplyMaxToolSteps`), and takes in what the user sends while it works: the text loop's `incoming` reads the session's new user messages after every round of tools and before an answer goes out, joining them to the transcript (`IncomingMessagesNote`), and an answer they arrive in time for is written again with them in mind (`RedraftNote`, the draft and tool turns kept so nothing is called twice; not on the wrap-up). The bot decides from what a message means, never from a keyword, whether it belongs to the work: one that changes or adds to it is folded in, a separate request is acknowledged at once with `send_message` (`src/harness/tools/bot/send-message-tool.ts`, chat replies only, at most `AppData.BotReplyInterimMessagesMax` per reply) and done after the current work in the same run. A message it sends that way is `bot_messages.interim`, and the conversation keeps the thinking row up while the thread ends with one and the bot's face is `working` (`(utils)/is-awaiting-reply.ts`). The job then records the newest user message it read as its `message_id` (`markReplyAnswered`), and a queued reply finishes silently only when a finished reply already read every user message (`hasUnansweredMessage`, `grabAnsweredMessageId`), never by who wrote last: a message sent mid-reply is saved before that reply, so the thread ends with the bot even when it never read it. `enqueueBotReply` reuses a pending reply, so a burst of messages gets one. `run-bot-task.ts` runs the brief (`compose-job-brief.ts`) with `TaskRunNote`, the bot tools, the file tools and the job's computer tools, closing the MCP connection after. Both add `BotFilesNote` (where a bot's files live, that they persist, and to search before saying it doesn't know) and collect `attachments` for files the bot wrote with `attach`, which `completeBotJob` links to the result message. Retryable harness errors go back to the queue's backoff, the rest are `BotJobFatalError`.
|
||||
- **API:** `api/admin/harness/` (`save-provider` also syncs, `sync-provider`, `delete-provider`, `save-settings`), super admin only, each returning the fresh `grabHarnessOverview()` page data.
|
||||
|
||||
### Token usage
|
||||
|
||||
Reference in New Issue
Block a user