Let the coordinator create workspaces and write findings in tasks

create_workspace gives the coordinator a way to start a workspace with its shared context, and the tool list is shared, so a new id can be used by create_bot or add_bot_to_workspace in the same reply.

A coordinator's tasks now get create_workspace and update_workspace too: the research task that had the material could only report it, never write it into the workspace, so the coordinator promised an update it had no turn to make.

A workspace created from chat is pushed as server:workspace-created, from the tool and from the live relay, so the rail shows it without a reload.
This commit is contained in:
2026-10-19 14:32:47 +01:00
parent 1cafb40eef
commit 550aaeabac
12 changed files with 268 additions and 16 deletions
+5 -3
View File
@@ -4,15 +4,16 @@ import { logError } from "@/src/functions/backend/logger";
import grabConnectedUserIds from "@/src/websocket/(functions)/grab-connected-user-ids";
import relayChangedBots from "./relay-changed-bots";
import relayCreatedBots from "./relay-created-bots";
import relayCreatedWorkspaces from "./relay-created-workspaces";
import relayNewBotMessages from "./relay-new-bot-messages";
import relayNewJobUpdates from "./relay-new-job-updates";
import type { LiveRelayCursor } from "./types";
/**
* Keeps this process's sockets current with work done anywhere: every
* `AppData.LiveRelayPollMs` it reads new bot messages, new job steps and
* changed bots from the database and pushes them to the sockets here
* of the users they're for. The process that did the work pushes at once
* `AppData.LiveRelayPollMs` it reads new bot messages, new job steps,
* new workspaces and changed bots from the database and pushes them to
* the sockets here of the users they're for. The process that did the work pushes at once
* (`pushBotNotice`); this catches the rest, e.g. a job run by another
* server on the same database. Clients ignore what they already have.
* Runs in every server process, ungated.
@@ -54,6 +55,7 @@ export default async function liveRelay() {
const bots_since = cursor.bots_at - AppData.LiveRelayPollMs;
await relayCreatedBots({ since: bots_since, users });
await relayCreatedWorkspaces({ since: bots_since, users });
await relayChangedBots({ since: bots_since, users });
cursor.bots_at = now;
} catch (error) {
@@ -0,0 +1,35 @@
import BunSQLite from "@moduletrace/bun-sqlite";
import type { WorkspaceWithActivity } from "@/src/types";
import pushWorkspaceCreated from "@/src/bots-queue/push-workspace-created";
type Params = {
/**
* Unix ms: workspaces created since then are pushed.
*/
since: number;
users: Set<string>;
};
/**
* Pushes `server:workspace-created` for every workspace created since
* `since` to the connected users who see it (`pushWorkspaceCreated`), so a
* workspace another process created still shows in the rail. Pushes are
* idempotent, so overlapping windows only repeat them.
*/
export default async function relayCreatedWorkspaces({ since, users }: Params) {
if (!users.size) return;
const workspaces =
(
await BunSQLite.sql<WorkspaceWithActivity>({
sql: `SELECT w.*, 0 AS unread, NULL AS last_activity_at
FROM workspaces w
WHERE w.created_at > ? AND w.archived = 0`,
values: [since],
})
).payload || [];
for (const workspace of workspaces) {
await pushWorkspaceCreated({ workspace, users });
}
}