quoroom 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9914,7 +9914,7 @@ var require_package = __commonJS({
9914
9914
  "package.json"(exports2, module2) {
9915
9915
  module2.exports = {
9916
9916
  name: "quoroom",
9917
- version: "0.1.21",
9917
+ version: "0.1.23",
9918
9918
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
9919
9919
  main: "./out/mcp/server.js",
9920
9920
  bin: {
@@ -9939,7 +9939,7 @@ var require_package = __commonJS({
9939
9939
  "build:mcp": "node scripts/build-mcp.js",
9940
9940
  "build:ui": "vite build --config src/ui/vite.config.ts",
9941
9941
  "kill:ports": "node scripts/kill-ports.js",
9942
- "kill:dev-ports": "npm run kill:ports -- 4700 3710 5173",
9942
+ "kill:dev-ports": "npm run kill:ports -- 4700 3715 5173",
9943
9943
  "dev:links": "node scripts/dev-links.js",
9944
9944
  dev: `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room & npm run dev:cloud & wait'`,
9945
9945
  "dev:room": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
@@ -9947,7 +9947,7 @@ var require_package = __commonJS({
9947
9947
  "dev:room:shared": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
9948
9948
  "doctor:split": "node scripts/doctor-split.js",
9949
9949
  "dev:isolated": `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room:isolated & npm run dev:cloud & VITE_API_PORT=4700 npm run dev:ui & wait'`,
9950
- "dev:cloud": `sh -c 'npm run kill:ports -- 3710 && cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='"'"'http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'"'"' npm start'`,
9950
+ "dev:cloud": `sh -c 'npm run kill:ports -- 3715 && cd ../cloud && PORT=3715 CLOUD_PUBLIC_URL=http://127.0.0.1:3715 CLOUD_ALLOWED_ORIGINS='"'"'http://127.0.0.1:3715,http://localhost:3715,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'"'"' npm start'`,
9951
9951
  "dev:ui": "vite --config src/ui/vite.config.ts",
9952
9952
  "seed:style-demo": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev; node scripts/seed-style-demo.js'",
9953
9953
  typecheck: "tsc --noEmit",
@@ -12069,6 +12069,7 @@ function mapRoomRow(row) {
12069
12069
  queenNickname: row.queen_nickname ?? null,
12070
12070
  chatSessionId: row.chat_session_id ?? null,
12071
12071
  referredByCode: row.referred_by_code ?? null,
12072
+ allowedTools: row.allowed_tools ?? null,
12072
12073
  webhookToken: row.webhook_token ?? null,
12073
12074
  createdAt: row.created_at,
12074
12075
  updatedAt: row.updated_at
@@ -12161,6 +12162,7 @@ function updateRoom(db2, id, updates) {
12161
12162
  config: "config",
12162
12163
  referredByCode: "referred_by_code",
12163
12164
  queenNickname: "queen_nickname",
12165
+ allowedTools: "allowed_tools",
12164
12166
  webhookToken: "webhook_token"
12165
12167
  };
12166
12168
  const fields = [];
@@ -12799,6 +12801,10 @@ function listRoomMessages(db2, roomId, status2) {
12799
12801
  function markRoomMessageRead(db2, id) {
12800
12802
  db2.prepare("UPDATE room_messages SET status = 'read' WHERE id = ?").run(id);
12801
12803
  }
12804
+ function markAllRoomMessagesRead(db2, roomId) {
12805
+ const result = db2.prepare("UPDATE room_messages SET status = 'read' WHERE room_id = ? AND status = 'unread'").run(roomId);
12806
+ return result.changes;
12807
+ }
12802
12808
  function replyToRoomMessage(db2, id) {
12803
12809
  db2.prepare("UPDATE room_messages SET status = 'replied' WHERE id = ?").run(id);
12804
12810
  }
@@ -12856,6 +12862,22 @@ function listRoomCycles(db2, roomId, limit = 20) {
12856
12862
  ).all(roomId, safeLimit);
12857
12863
  return rows.map(mapWorkerCycleRow);
12858
12864
  }
12865
+ function countProductiveToolCalls(db2, workerId, lastNCycles = 2) {
12866
+ const row = db2.prepare(`
12867
+ SELECT COUNT(*) as cnt FROM cycle_logs
12868
+ WHERE cycle_id IN (
12869
+ SELECT id FROM worker_cycles
12870
+ WHERE worker_id = ? AND status = 'completed'
12871
+ ORDER BY started_at DESC LIMIT ?
12872
+ )
12873
+ AND entry_type = 'tool_call'
12874
+ AND (content LIKE '%web_search%' OR content LIKE '%web_fetch%' OR content LIKE '%remember%'
12875
+ OR content LIKE '%send_message%' OR content LIKE '%inbox_send%'
12876
+ OR content LIKE '%update_progress%' OR content LIKE '%complete_goal%'
12877
+ OR content LIKE '%set_goal%' OR content LIKE '%delegate_task%' OR content LIKE '%propose%' OR content LIKE '%vote%')
12878
+ `).get(workerId, lastNCycles);
12879
+ return row.cnt;
12880
+ }
12859
12881
  function cleanupStaleCycles(db2) {
12860
12882
  const result = db2.prepare(
12861
12883
  "UPDATE worker_cycles SET status = 'failed', error_message = 'Server restarted', finished_at = datetime('now','localtime') WHERE status = 'running'"
@@ -22503,11 +22525,13 @@ async function sendToken(db2, roomId, to, amount, encryptionKey, network = "base
22503
22525
  var DEFAULT_QUEEN_SYSTEM_PROMPT = `You are the Queen agent of this Room \u2014 the strategic coordinator.
22504
22526
  Your role is to pursue the room's objectives by:
22505
22527
  - Decomposing goals into actionable sub-goals
22506
- - Creating and delegating to worker agents
22528
+ - **Delegating tasks to workers** using quoroom_delegate_task \u2014 this is your primary way to get work done
22507
22529
  - Proposing decisions to the quorum
22508
22530
  - Self-improving your strategies and skills based on results
22509
22531
  - Managing resources efficiently
22510
22532
 
22533
+ **Delegation is key.** You have workers \u2014 use them. Break work into concrete tasks and assign each to a worker with quoroom_delegate_task. Workers see their assigned tasks each cycle and will prioritize them. Coordinate via quoroom_send_message. Don't do everything yourself \u2014 delegate, then focus on strategy, coordination, and quality control.
22534
+
22511
22535
  You have access to all room MCP tools. Use them to manage goals, workers, skills, and decisions.
22512
22536
 
22513
22537
  Quorum: Use quoroom_propose to create proposals (types: strategy, resource, personnel, rule_change, low_impact). Low-impact proposals may be auto-approved. Use quoroom_vote to cast your vote (yes/no/abstain) with reasoning. Use quoroom_list_decisions and quoroom_decision_detail to review pending and past decisions.
@@ -22518,7 +22542,7 @@ Revenue: Your room has a USDC wallet. Beyond pursuing the room's core objectives
22518
22542
 
22519
22543
  Resource Management: Workers using API models (openai:*, anthropic:*) run on stations (cloud compute) \u2014 they cannot execute locally. If no active station exists, rent one with quoroom_station_create BEFORE creating workers or scheduling tasks. Minimum tier: small ($25/mo, 2GB RAM). Check the Stations section in your context each cycle for available compute. Maximize use of your room's stations \u2014 they are your compute power for running workers. If workers are failing with "no active station" errors, rent a station immediately.
22520
22544
 
22521
- Network Growth: A larger network of rooms makes everyone stronger. Rooms created through your invite links become part of your network \u2014 you can exchange knowledge, discover money-making opportunities, and propose paid collaborations with them via inter-room messages. After meaningful milestones (goal completed, revenue earned, successful collaboration), consider generating an invite link with quoroom_invite_create and sharing it with the keeper via quoroom_inbox_send_keeper. Frame it around the value: more rooms in the network means more potential deals, shared insights, and collective intelligence. Don't be pushy \u2014 one mention per milestone is enough. Use quoroom_invite_network to see your network's growth.`;
22545
+ Network Growth: A larger network of rooms makes everyone stronger. Rooms created through your invite links become part of your network \u2014 you can exchange knowledge, discover money-making opportunities, and propose paid collaborations with them via inter-room messages. After meaningful milestones (goal completed, revenue earned, successful collaboration), consider generating an invite link with quoroom_invite_create and sharing it with the keeper via quoroom_send_message. Frame it around the value: more rooms in the network means more potential deals, shared insights, and collective intelligence. Don't be pushy \u2014 one mention per milestone is enough. Use quoroom_invite_network to see your network's growth.`;
22522
22546
  function createRoom2(db2, input) {
22523
22547
  const config = { ...DEFAULT_ROOM_CONFIG, ...input.config };
22524
22548
  const room = createRoom(db2, input.name, input.goal, config, input.referredByCode);
@@ -23981,33 +24005,131 @@ async function closeBrowser() {
23981
24005
  }
23982
24006
  }
23983
24007
  async function webFetch(url) {
23984
- const jinaUrl = `https://r.jina.ai/${url}`;
23985
- const response = await fetch(jinaUrl, {
23986
- headers: {
23987
- "Accept": "text/plain",
23988
- "X-No-Cache": "true"
23989
- },
23990
- signal: AbortSignal.timeout(3e4)
24008
+ try {
24009
+ const jinaUrl = `https://r.jina.ai/${url}`;
24010
+ const response = await fetch(jinaUrl, {
24011
+ headers: { "Accept": "text/plain", "X-No-Cache": "true" },
24012
+ signal: AbortSignal.timeout(2e4)
24013
+ });
24014
+ if (response.ok) {
24015
+ const text = await response.text();
24016
+ if (text.length > 200 && !text.includes("Warning: Target URL returned error")) {
24017
+ return text.slice(0, MAX_CONTENT_CHARS);
24018
+ }
24019
+ }
24020
+ } catch {
24021
+ }
24022
+ return fetchWithBrowser(url);
24023
+ }
24024
+ async function fetchWithBrowser(url) {
24025
+ const browser = await getBrowser();
24026
+ const context = await browser.newContext({
24027
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
23991
24028
  });
23992
- if (!response.ok) {
23993
- throw new Error(`Jina fetch failed: ${response.status} ${response.statusText}`);
24029
+ const page = await context.newPage();
24030
+ try {
24031
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
24032
+ const text = await page.innerText("body").catch(() => "");
24033
+ if (!text) throw new Error(`Could not read content from ${url}`);
24034
+ return text.slice(0, MAX_CONTENT_CHARS);
24035
+ } finally {
24036
+ await context.close();
23994
24037
  }
23995
- const text = await response.text();
23996
- return text.slice(0, MAX_CONTENT_CHARS);
23997
24038
  }
23998
24039
  async function webSearch(query) {
23999
- const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
24000
- const response = await fetch(searchUrl, {
24001
- headers: {
24002
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
24003
- },
24004
- signal: AbortSignal.timeout(15e3)
24040
+ const browserResults = await searchWithBrowser(query);
24041
+ if (browserResults.length > 0) return browserResults;
24042
+ const ddgResults = await searchDdg(query);
24043
+ if (ddgResults.length > 0) return ddgResults;
24044
+ try {
24045
+ const response = await fetch(`https://s.jina.ai/${encodeURIComponent(query)}`, {
24046
+ headers: { "Accept": "application/json", "X-No-Cache": "true" },
24047
+ signal: AbortSignal.timeout(15e3)
24048
+ });
24049
+ if (response.ok) {
24050
+ const data = await response.json();
24051
+ if (data.data && Array.isArray(data.data)) {
24052
+ return data.data.slice(0, 5).map((r) => ({
24053
+ title: r.title ?? "",
24054
+ url: r.url ?? "",
24055
+ snippet: (r.description ?? r.content ?? "").slice(0, 300)
24056
+ })).filter((r) => r.url);
24057
+ }
24058
+ }
24059
+ } catch {
24060
+ }
24061
+ return [];
24062
+ }
24063
+ async function searchWithBrowser(query) {
24064
+ let browser;
24065
+ try {
24066
+ browser = await getBrowser();
24067
+ } catch {
24068
+ return [];
24069
+ }
24070
+ const context = await browser.newContext({
24071
+ userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
24072
+ locale: "en-US"
24005
24073
  });
24006
- if (!response.ok) {
24007
- throw new Error(`DuckDuckGo search failed: ${response.status}`);
24074
+ const page = await context.newPage();
24075
+ try {
24076
+ await page.goto(`https://search.yahoo.com/search?p=${encodeURIComponent(query)}`, {
24077
+ waitUntil: "domcontentloaded",
24078
+ timeout: 15e3
24079
+ });
24080
+ await page.waitForTimeout(1e3);
24081
+ const results = await page.evaluate(() => {
24082
+ const items = [];
24083
+ const blocks = document.querySelectorAll("#web .algo, .dd.algo, .algo");
24084
+ for (const block of blocks) {
24085
+ const link = block.querySelector("a");
24086
+ const h3 = block.querySelector("h3");
24087
+ const snippetEl = block.querySelector(".compText p, .compText, p");
24088
+ if (!link) continue;
24089
+ const url = link.getAttribute("href") || "";
24090
+ if (!url.startsWith("http")) continue;
24091
+ items.push({
24092
+ title: h3 ? (h3.textContent || "").trim() : (link.textContent || "").trim(),
24093
+ url,
24094
+ snippet: snippetEl ? (snippetEl.textContent || "").trim().slice(0, 300) : ""
24095
+ });
24096
+ if (items.length >= 5) break;
24097
+ }
24098
+ return items;
24099
+ });
24100
+ return results.filter((r) => r.url);
24101
+ } catch {
24102
+ return [];
24103
+ } finally {
24104
+ await context.close();
24008
24105
  }
24009
- const html = await response.text();
24010
- return parseDdgResults(html).slice(0, 5);
24106
+ }
24107
+ async function searchDdg(query) {
24108
+ for (let attempt = 0; attempt < 3; attempt++) {
24109
+ if (attempt > 0) await new Promise((r) => setTimeout(r, 1e3 * attempt));
24110
+ try {
24111
+ const response = await fetch("https://html.duckduckgo.com/html/", {
24112
+ method: "POST",
24113
+ headers: {
24114
+ "Content-Type": "application/x-www-form-urlencoded",
24115
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
24116
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
24117
+ "Accept-Language": "en-US,en;q=0.9",
24118
+ "Referer": "https://html.duckduckgo.com/"
24119
+ },
24120
+ body: `q=${encodeURIComponent(query)}&b=`,
24121
+ signal: AbortSignal.timeout(15e3),
24122
+ redirect: "follow"
24123
+ });
24124
+ if (response.status === 202) continue;
24125
+ if (!response.ok) continue;
24126
+ const html = await response.text();
24127
+ const results = parseDdgResults(html).slice(0, 5);
24128
+ if (results.length > 0) return results;
24129
+ } catch {
24130
+ }
24131
+ }
24132
+ return [];
24011
24133
  }
24012
24134
  function parseDdgResults(html) {
24013
24135
  const results = [];
@@ -24159,6 +24281,22 @@ var QUEEN_TOOL_DEFINITIONS = [
24159
24281
  }
24160
24282
  }
24161
24283
  },
24284
+ {
24285
+ type: "function",
24286
+ function: {
24287
+ name: "quoroom_delegate_task",
24288
+ description: 'Delegate a task to a specific worker. Creates a goal assigned to that worker. The worker will see it in their "Your Assigned Tasks" context. Use this to divide work among your team.',
24289
+ parameters: {
24290
+ type: "object",
24291
+ properties: {
24292
+ workerName: { type: "string", description: "The worker name to assign to (from Room Workers list)" },
24293
+ task: { type: "string", description: "Description of the task to delegate" },
24294
+ parentGoalId: { type: "number", description: "Optional parent goal ID to attach as sub-goal" }
24295
+ },
24296
+ required: ["workerName", "task"]
24297
+ }
24298
+ }
24299
+ },
24162
24300
  {
24163
24301
  type: "function",
24164
24302
  function: {
@@ -24319,18 +24457,19 @@ var QUEEN_TOOL_DEFINITIONS = [
24319
24457
  }
24320
24458
  }
24321
24459
  },
24322
- // ── Keeper messaging ───────────────────────────────────────────────────
24460
+ // ── Messaging ──────────────────────────────────────────────────────────
24323
24461
  {
24324
24462
  type: "function",
24325
24463
  function: {
24326
- name: "quoroom_ask_keeper",
24327
- description: "Send a question or request to the keeper (human operator). Use for decisions that require human input.",
24464
+ name: "quoroom_send_message",
24465
+ description: "Send a message to the keeper or another worker. The keeper sees all messages. Use to coordinate with teammates, report progress, ask for help, or escalate to the keeper.",
24328
24466
  parameters: {
24329
24467
  type: "object",
24330
24468
  properties: {
24331
- question: { type: "string", description: "The question or request for the keeper" }
24469
+ to: { type: "string", description: 'Recipient: "keeper" or a worker name from Room Workers list' },
24470
+ message: { type: "string", description: "The message content" }
24332
24471
  },
24333
- required: ["question"]
24472
+ required: ["to", "message"]
24334
24473
  }
24335
24474
  }
24336
24475
  },
@@ -24402,6 +24541,47 @@ var QUEEN_TOOL_DEFINITIONS = [
24402
24541
  required: ["url", "actions"]
24403
24542
  }
24404
24543
  }
24544
+ },
24545
+ // ── Wallet ──────────────────────────────────────────────────────────────
24546
+ {
24547
+ type: "function",
24548
+ function: {
24549
+ name: "quoroom_wallet_balance",
24550
+ description: "Get the room's wallet balance (USDC). Returns address and transaction summary.",
24551
+ parameters: {
24552
+ type: "object",
24553
+ properties: {},
24554
+ required: []
24555
+ }
24556
+ }
24557
+ },
24558
+ {
24559
+ type: "function",
24560
+ function: {
24561
+ name: "quoroom_wallet_send",
24562
+ description: "Send USDC from the room's wallet to an address.",
24563
+ parameters: {
24564
+ type: "object",
24565
+ properties: {
24566
+ to: { type: "string", description: "Recipient address (0x...)" },
24567
+ amount: { type: "string", description: 'Amount (e.g., "10.50")' }
24568
+ },
24569
+ required: ["to", "amount"]
24570
+ }
24571
+ }
24572
+ },
24573
+ {
24574
+ type: "function",
24575
+ function: {
24576
+ name: "quoroom_wallet_history",
24577
+ description: "Get recent wallet transaction history.",
24578
+ parameters: {
24579
+ type: "object",
24580
+ properties: {
24581
+ limit: { type: "string", description: "Max transactions to return (default: 10)" }
24582
+ }
24583
+ }
24584
+ }
24405
24585
  }
24406
24586
  ];
24407
24587
  async function executeQueenTool(db2, roomId, workerId, toolName, args) {
@@ -24422,10 +24602,12 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24422
24602
  if (goalCheck.roomId !== roomId) return { content: `Error: goal #${goalId} belongs to another room. Your room's goals are shown in the Active Goals section \u2014 use those goal IDs.`, isError: true };
24423
24603
  const observation = String(args.observation ?? args.progress ?? args.message ?? args.text ?? "");
24424
24604
  const metricValue = args.metricValue != null ? Number(args.metricValue) : args.metric_value != null ? Number(args.metric_value) : void 0;
24605
+ const subGoals = getSubGoals(db2, goalId);
24425
24606
  updateGoalProgress(db2, goalId, observation, metricValue, workerId);
24426
24607
  const goal = getGoal(db2, goalId);
24427
24608
  const pct = Math.round((goal?.progress ?? 0) * 100);
24428
- return { content: `Progress logged on goal #${goalId}. Now at ${pct}%.` };
24609
+ const note = subGoals.length > 0 && metricValue != null ? ` (metricValue ignored \u2014 goal has ${subGoals.length} sub-goals, progress is calculated from them. Update sub-goals directly.)` : "";
24610
+ return { content: `Progress logged on goal #${goalId}. Now at ${pct}%.${note}` };
24429
24611
  }
24430
24612
  case "quoroom_create_subgoal": {
24431
24613
  const goalId = Number(args.goalId);
@@ -24437,6 +24619,32 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24437
24619
  const subGoals = decomposeGoal(db2, goalId, descriptions);
24438
24620
  return { content: `Created ${subGoals.length} sub-goal(s) under goal #${goalId}.` };
24439
24621
  }
24622
+ case "quoroom_delegate_task": {
24623
+ const workerName = String(args.workerName ?? args.worker ?? args.to ?? "").trim();
24624
+ const task = String(args.task ?? args.description ?? args.goal ?? "").trim();
24625
+ if (!workerName) return { content: 'Error: "workerName" is required (a worker name from Room Workers list).', isError: true };
24626
+ if (!task) return { content: 'Error: "task" is required (description of the task to delegate).', isError: true };
24627
+ const roomWorkers = listRoomWorkers(db2, roomId);
24628
+ const target = roomWorkers.find((w) => w.name.toLowerCase() === workerName.toLowerCase());
24629
+ if (!target) {
24630
+ const available = roomWorkers.filter((w) => w.id !== workerId).map((w) => w.name).join(", ");
24631
+ return { content: `Worker "${workerName}" not found. Available: ${available || "none"}`, isError: true };
24632
+ }
24633
+ const parentGoalId = args.parentGoalId != null ? Number(args.parentGoalId) : void 0;
24634
+ if (parentGoalId != null) {
24635
+ const parentCheck = getGoal(db2, parentGoalId);
24636
+ if (!parentCheck) return { content: `Error: parent goal #${parentGoalId} not found.`, isError: true };
24637
+ if (parentCheck.roomId !== roomId) return { content: `Error: parent goal #${parentGoalId} belongs to another room.`, isError: true };
24638
+ }
24639
+ const goal = createGoal(db2, roomId, task, parentGoalId, target.id);
24640
+ if (parentGoalId) {
24641
+ const parentGoal = getGoal(db2, parentGoalId);
24642
+ if (parentGoal && parentGoal.status === "active") {
24643
+ updateGoal(db2, parentGoalId, { status: "in_progress" });
24644
+ }
24645
+ }
24646
+ return { content: `Task delegated to ${target.name}: "${task}" (goal #${goal.id})` };
24647
+ }
24440
24648
  case "quoroom_complete_goal": {
24441
24649
  const goalId = Number(args.goalId);
24442
24650
  const goalCheck = getGoal(db2, goalId);
@@ -24458,6 +24666,13 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24458
24666
  case "quoroom_propose": {
24459
24667
  const proposalText = String(args.proposal ?? args.text ?? args.description ?? args.content ?? args.idea ?? "").trim();
24460
24668
  if (!proposalText) return { content: 'Error: proposal text is required. Provide a "proposal" string.', isError: true };
24669
+ const recentDecisions = listDecisions(db2, roomId);
24670
+ const isDuplicate = recentDecisions.slice(0, 10).some(
24671
+ (d) => (d.status === "voting" || d.status === "approved") && d.proposal.toLowerCase() === proposalText.toLowerCase()
24672
+ );
24673
+ if (isDuplicate) {
24674
+ return { content: `A similar proposal already exists: "${proposalText}". No need to propose again.`, isError: true };
24675
+ }
24461
24676
  const decisionType = String(args.decisionType ?? args.type ?? args.impact ?? args.category ?? "low_impact");
24462
24677
  const decision = propose(db2, {
24463
24678
  roomId,
@@ -24498,6 +24713,10 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24498
24713
  const systemPrompt = String(args.systemPrompt ?? args.system_prompt ?? args.instructions ?? args.prompt ?? "").trim();
24499
24714
  if (!name) return { content: 'Error: name is required for quoroom_create_worker. Provide a "name" string.', isError: true };
24500
24715
  if (!systemPrompt) return { content: `Error: systemPrompt is required for quoroom_create_worker. Provide a "systemPrompt" string describing this worker's role and instructions.`, isError: true };
24716
+ const existingWorkers = listRoomWorkers(db2, roomId);
24717
+ if (existingWorkers.some((w) => w.name.toLowerCase() === name.toLowerCase())) {
24718
+ return { content: `Worker "${name}" already exists in this room. Use quoroom_update_worker to modify it, or choose a different name.`, isError: true };
24719
+ }
24501
24720
  const role = args.role && args.role !== args.name ? String(args.role) : void 0;
24502
24721
  const description = args.description ? String(args.description) : void 0;
24503
24722
  const preset = role ? WORKER_ROLE_PRESETS[role] : void 0;
@@ -24530,6 +24749,10 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24530
24749
  const taskWorkerId = args.workerId ? Number(args.workerId) : void 0;
24531
24750
  const maxTurns = args.maxTurns ? Number(args.maxTurns) : void 0;
24532
24751
  const triggerType = cronExpression ? "cron" : scheduledAt ? "once" : "manual";
24752
+ const existingTasks = listTasks(db2, roomId, "active");
24753
+ if (existingTasks.some((t) => t.name.toLowerCase() === name.toLowerCase())) {
24754
+ return { content: `Task "${name}" already exists. Choose a different name or manage the existing task.`, isError: true };
24755
+ }
24533
24756
  if (taskWorkerId) {
24534
24757
  const taskWorker = getWorker(db2, taskWorkerId);
24535
24758
  if (!taskWorker || taskWorker.roomId !== roomId) {
@@ -24555,6 +24778,11 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24555
24778
  const name = String(args.name ?? "");
24556
24779
  const content = String(args.content ?? "");
24557
24780
  const type = String(args.type ?? "fact");
24781
+ const existing = listEntities(db2, roomId).find((e) => e.name.toLowerCase() === name.toLowerCase());
24782
+ if (existing) {
24783
+ addObservation(db2, existing.id, content, "queen");
24784
+ return { content: `Updated memory "${name}" (added new observation to existing entry).` };
24785
+ }
24558
24786
  const entity = createEntity(db2, name, type, void 0, roomId);
24559
24787
  addObservation(db2, entity.id, content, "queen");
24560
24788
  return { content: `Remembered "${name}".` };
@@ -24569,13 +24797,27 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24569
24797
  }).join("\n");
24570
24798
  return { content: summary };
24571
24799
  }
24572
- // ── Keeper messaging ─────────────────────────────────────────────
24573
- case "quoroom_ask_keeper": {
24574
- const question = String(args.question ?? "");
24575
- const escalation = createEscalation(db2, roomId, workerId, question);
24576
- const deliveryStatus = await deliverQueenMessage(db2, roomId, question);
24577
- const deliveryNote = deliveryStatus ? ` ${deliveryStatus}` : "";
24578
- return { content: `Question sent to keeper (escalation #${escalation.id}).${deliveryNote}` };
24800
+ // ── Messaging ────────────────────────────────────────────────────
24801
+ case "quoroom_send_message": {
24802
+ const to = String(args.to ?? "").trim();
24803
+ const message = String(args.message ?? args.question ?? "").trim();
24804
+ if (!to) return { content: 'Error: "to" is required ("keeper" or a worker name).', isError: true };
24805
+ if (!message) return { content: 'Error: "message" is required.', isError: true };
24806
+ if (to.toLowerCase() === "keeper") {
24807
+ const escalation2 = createEscalation(db2, roomId, workerId, message);
24808
+ const deliveryStatus = await deliverQueenMessage(db2, roomId, message);
24809
+ const deliveryNote = deliveryStatus ? ` ${deliveryStatus}` : "";
24810
+ return { content: `Message sent to keeper (#${escalation2.id}).${deliveryNote}` };
24811
+ }
24812
+ const roomWorkers = listRoomWorkers(db2, roomId);
24813
+ const target = roomWorkers.find((w) => w.name.toLowerCase() === to.toLowerCase());
24814
+ if (!target) {
24815
+ const available = roomWorkers.filter((w) => w.id !== workerId).map((w) => w.name).join(", ");
24816
+ return { content: `Worker "${to}" not found. Available: ${available || "none"}`, isError: true };
24817
+ }
24818
+ if (target.id === workerId) return { content: "Cannot send a message to yourself.", isError: true };
24819
+ const escalation = createEscalation(db2, roomId, workerId, message, target.id);
24820
+ return { content: `Message sent to ${target.name} (#${escalation.id}).` };
24579
24821
  }
24580
24822
  // ── Room config ──────────────────────────────────────────────────
24581
24823
  case "quoroom_configure_room": {
@@ -24609,6 +24851,26 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
24609
24851
  if (!url) return { content: "Error: url is required", isError: true };
24610
24852
  return { content: await browserAction(url, actions) };
24611
24853
  }
24854
+ // ── Wallet ────────────────────────────────────────────────────
24855
+ case "quoroom_wallet_balance": {
24856
+ const wallet = getWalletByRoom(db2, roomId);
24857
+ if (!wallet) return { content: "No wallet found for this room.", isError: true };
24858
+ const summary = getWalletTransactionSummary(db2, wallet.id);
24859
+ const net = (parseFloat(summary.received) - parseFloat(summary.sent)).toFixed(2);
24860
+ return { content: `Wallet ${wallet.address}: ${net} USDC (received: ${summary.received}, sent: ${summary.sent})` };
24861
+ }
24862
+ case "quoroom_wallet_send": {
24863
+ return { content: "Wallet send requires on-chain transaction \u2014 use the MCP tool quoroom_wallet_send with encryptionKey, or ask the keeper to send funds.", isError: true };
24864
+ }
24865
+ case "quoroom_wallet_history": {
24866
+ const wallet = getWalletByRoom(db2, roomId);
24867
+ if (!wallet) return { content: "No wallet found for this room.", isError: true };
24868
+ const limit = Math.min(Number(args.limit) || 10, 50);
24869
+ const txs = listWalletTransactions(db2, wallet.id, limit);
24870
+ if (txs.length === 0) return { content: "No transactions yet." };
24871
+ const lines = txs.map((tx) => `[${tx.type}] ${tx.amount} USDC \u2014 ${tx.description ?? ""} (${tx.status})`).join("\n");
24872
+ return { content: lines };
24873
+ }
24612
24874
  default:
24613
24875
  return { content: `Unknown tool: ${toolName}`, isError: true };
24614
24876
  }
@@ -24873,7 +25135,8 @@ async function runCycle(db2, roomId, worker, maxTurns, options) {
24873
25135
  id: g.id,
24874
25136
  goal: g.description,
24875
25137
  progress: g.progress,
24876
- status: g.status
25138
+ status: g.status,
25139
+ assignedWorkerId: g.assignedWorkerId
24877
25140
  }));
24878
25141
  const roomWorkers = listRoomWorkers(db2, roomId);
24879
25142
  const roomTasks = listTasks(db2, roomId, "active").slice(0, 10);
@@ -24979,10 +25242,21 @@ ${skillContent}` : ""
24979
25242
  ${status2.room.goal}`);
24980
25243
  }
24981
25244
  if (goalUpdates.length > 0) {
25245
+ const workerMap = new Map(roomWorkers.map((w) => [w.id, w.name]));
24982
25246
  contextParts.push(`## Active Goals
24983
- ${goalUpdates.map(
24984
- (g) => `- [#${g.id}] [${Math.round(g.progress * 100)}%] ${g.goal} (${g.status})`
24985
- ).join("\n")}`);
25247
+ ${goalUpdates.map((g) => {
25248
+ const assignee = g.assignedWorkerId ? ` \u2192 ${workerMap.get(g.assignedWorkerId) ?? `Worker #${g.assignedWorkerId}`}` : "";
25249
+ return `- [#${g.id}] [${Math.round(g.progress * 100)}%] ${g.goal} (${g.status})${assignee}`;
25250
+ }).join("\n")}`);
25251
+ const myTasks = status2.activeGoals.filter((g) => g.assignedWorkerId === worker.id);
25252
+ if (myTasks.length > 0) {
25253
+ contextParts.push(`## Your Assigned Tasks
25254
+ ${myTasks.map(
25255
+ (g) => `- [#${g.id}] [${Math.round(g.progress * 100)}%] ${g.description}`
25256
+ ).join("\n")}
25257
+
25258
+ These tasks were delegated to you. Prioritize completing them and report progress.`);
25259
+ }
24986
25260
  }
24987
25261
  const memoryEntities = listEntities(db2, roomId).slice(0, 20);
24988
25262
  if (memoryEntities.length > 0) {
@@ -25011,19 +25285,21 @@ ${recentResolved.map((d) => {
25011
25285
  return `- ${icon} ${d.status}: "${d.proposal.slice(0, 120)}"`;
25012
25286
  }).join("\n")}`);
25013
25287
  }
25014
- const pendingToKeeper = pendingEscalations.filter((e) => e.fromAgentId === worker.id && !e.toAgentId);
25015
- const pendingFromOthers = pendingEscalations.filter((e) => e.fromAgentId !== worker.id);
25016
- if (pendingToKeeper.length > 0) {
25017
- contextParts.push(`## Pending Questions to Keeper (awaiting keeper reply)
25018
- ${pendingToKeeper.map(
25288
+ const myKeeperMessages = pendingEscalations.filter((e) => e.fromAgentId === worker.id && !e.toAgentId);
25289
+ const incomingWorkerMessages = pendingEscalations.filter((e) => e.toAgentId === worker.id && e.fromAgentId !== worker.id);
25290
+ if (myKeeperMessages.length > 0) {
25291
+ contextParts.push(`## Pending Messages to Keeper (awaiting reply)
25292
+ ${myKeeperMessages.map(
25019
25293
  (e) => `- #${e.id}: ${e.question}`
25020
25294
  ).join("\n")}`);
25021
25295
  }
25022
- if (pendingFromOthers.length > 0) {
25023
- contextParts.push(`## Escalations Awaiting Your Response
25024
- ${pendingFromOthers.map(
25025
- (e) => `- #${e.id}: ${e.question}`
25026
- ).join("\n")}`);
25296
+ if (incomingWorkerMessages.length > 0) {
25297
+ const senderNames = new Map(roomWorkers.map((w) => [w.id, w.name]));
25298
+ contextParts.push(`## Messages from Other Workers
25299
+ ${incomingWorkerMessages.map((e) => {
25300
+ const sender = senderNames.get(e.fromAgentId ?? 0) ?? `Worker #${e.fromAgentId}`;
25301
+ return `- #${e.id} from ${sender}: ${e.question}`;
25302
+ }).join("\n")}`);
25027
25303
  }
25028
25304
  if (recentKeeperAnswers.length > 0) {
25029
25305
  contextParts.push(`## Keeper Answers (recent)
@@ -25051,6 +25327,14 @@ ${roomTasks.map(
25051
25327
  (t) => `- #${t.id} "${t.name}" [${t.triggerType}] \u2014 ${t.status}`
25052
25328
  ).join("\n")}`);
25053
25329
  }
25330
+ const wallet = getWalletByRoom(db2, roomId);
25331
+ if (wallet) {
25332
+ const summary = getWalletTransactionSummary(db2, wallet.id);
25333
+ const net = (parseFloat(summary.received) - parseFloat(summary.sent)).toFixed(2);
25334
+ contextParts.push(`## Wallet
25335
+ Address: ${wallet.address}
25336
+ Balance: ${net} USDC (received: ${summary.received}, spent: ${summary.sent})`);
25337
+ }
25054
25338
  if (unreadMessages.length > 0) {
25055
25339
  contextParts.push(`## Unread Messages
25056
25340
  ${unreadMessages.map(
@@ -25088,26 +25372,62 @@ ${top3.map(
25088
25372
  }
25089
25373
  contextParts.push(`## Execution Settings
25090
25374
  ${settingsParts.join("\n")}`);
25375
+ const STUCK_THRESHOLD_CYCLES = 2;
25376
+ const productiveCallCount = countProductiveToolCalls(db2, worker.id, STUCK_THRESHOLD_CYCLES);
25377
+ const recentCompletedCycles = listRoomCycles(db2, roomId, 5).filter((c) => c.workerId === worker.id && c.status === "completed");
25378
+ const isStuck = recentCompletedCycles.length >= STUCK_THRESHOLD_CYCLES && productiveCallCount === 0;
25379
+ if (isStuck) {
25380
+ contextParts.push(`## \u26A0 STUCK DETECTED
25381
+ Your last ${STUCK_THRESHOLD_CYCLES} cycles produced no external results (no web searches, no memories stored, no goal progress, no keeper messages). You MUST change strategy NOW:
25382
+ - Try a different web search query
25383
+ - Store what you know in memory even if incomplete
25384
+ - Update goal progress with what you've learned
25385
+ - Message the keeper if you're blocked
25386
+ Do NOT repeat the same approach. Pivot immediately.`);
25387
+ logBuffer.addSynthetic("system", `Stuck detector: 0 productive tool calls in last ${STUCK_THRESHOLD_CYCLES} cycles \u2014 injecting pivot directive`);
25388
+ }
25091
25389
  const selfRegulateHint = rateLimitEvents.length > 0 ? "\n- **Self-regulate**: You are hitting rate limits. Use quoroom_configure_room to increase your cycle gap or reduce max turns to stay within API limits." : "";
25092
25390
  const isClaude = model === "claude" || model.startsWith("claude-");
25093
25391
  const toolCallInstruction = isClaude ? "Always call tools to take action \u2014 do not just describe what you would do." : "IMPORTANT: You MUST call at least one tool in your response. Respond ONLY with a tool call \u2014 do not write explanatory text without a tool call.";
25094
- const commsTools = isCli ? "quoroom_inbox_send_keeper (message keeper), quoroom_inbox_list (inter-room), quoroom_inbox_send_room, quoroom_inbox_reply" : "quoroom_ask_keeper";
25095
- const webTools = isCli ? "(use your built-in web search and fetch tools)" : "quoroom_web_search, quoroom_web_fetch, quoroom_browser";
25096
- const toolList = `**Goals:** quoroom_set_goal, quoroom_update_progress, quoroom_create_subgoal, quoroom_complete_goal, quoroom_abandon_goal
25097
- **Governance:** quoroom_propose, quoroom_vote
25098
- **Workers:** quoroom_create_worker, quoroom_update_worker
25099
- **Tasks:** quoroom_schedule
25100
- **Memory:** quoroom_remember, quoroom_recall
25101
- **Web:** ${webTools}
25102
- **Comms:** ${commsTools}
25103
- **Settings:** quoroom_configure_room${selfRegulateHint}`;
25104
- const sendKeeperTool = isCli ? "quoroom_inbox_send_keeper" : "quoroom_ask_keeper";
25392
+ const allowListRaw = status2.room.allowedTools?.trim() || null;
25393
+ const allowSet = allowListRaw ? new Set(allowListRaw.split(",").map((s) => s.trim())) : null;
25394
+ const has = (name) => !allowSet || allowSet.has(name);
25395
+ const toolLines = [];
25396
+ const goalTools = ["quoroom_set_goal", "quoroom_update_progress", "quoroom_create_subgoal", "quoroom_delegate_task", "quoroom_complete_goal", "quoroom_abandon_goal"].filter(has);
25397
+ if (goalTools.length) toolLines.push(`**Goals:** ${goalTools.join(", ")}`);
25398
+ const govTools = ["quoroom_propose", "quoroom_vote"].filter(has);
25399
+ if (govTools.length) toolLines.push(`**Governance:** ${govTools.join(", ")}`);
25400
+ const workerTools = ["quoroom_create_worker", "quoroom_update_worker"].filter(has);
25401
+ if (workerTools.length) toolLines.push(`**Workers:** ${workerTools.join(", ")}`);
25402
+ if (has("quoroom_schedule")) toolLines.push("**Tasks:** quoroom_schedule");
25403
+ const memTools = ["quoroom_remember", "quoroom_recall"].filter(has);
25404
+ if (memTools.length) toolLines.push(`**Memory:** ${memTools.join(", ")}`);
25405
+ const walletToolNames = isCli ? ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history", "quoroom_wallet_topup"] : ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history"];
25406
+ const filteredWallet = walletToolNames.filter(has);
25407
+ if (filteredWallet.length) toolLines.push(`**Wallet:** ${filteredWallet.join(", ")}`);
25408
+ const webToolNames = isCli ? null : ["quoroom_web_search", "quoroom_web_fetch", "quoroom_browser"];
25409
+ if (isCli) {
25410
+ if (has("quoroom_web_search") || has("quoroom_web_fetch")) toolLines.push("**Web:** (use your built-in web search and fetch tools)");
25411
+ } else {
25412
+ const filteredWeb = (webToolNames || []).filter(has);
25413
+ if (filteredWeb.length) toolLines.push(`**Web:** ${filteredWeb.join(", ")}`);
25414
+ }
25415
+ const commsToolNames = isCli ? ["quoroom_send_message", "quoroom_inbox_list", "quoroom_inbox_send_room", "quoroom_inbox_reply"] : ["quoroom_send_message"];
25416
+ const filteredComms = commsToolNames.filter(has);
25417
+ if (filteredComms.length) {
25418
+ if (isCli) toolLines.push(`**Comms:** ${filteredComms.map((t) => t === "quoroom_send_message" ? `${t} (message keeper or worker)` : t === "quoroom_inbox_list" ? `${t} (inter-room)` : t).join(", ")}`);
25419
+ else toolLines.push(`**Comms:** ${filteredComms.join(", ")}`);
25420
+ }
25421
+ if (has("quoroom_configure_room")) toolLines.push(`**Settings:** quoroom_configure_room${selfRegulateHint}`);
25422
+ const toolList = toolLines.join("\n");
25105
25423
  contextParts.push(`## Instructions
25106
25424
  Based on the current state, decide what to do next and call the appropriate tools. Available tools:
25107
25425
 
25108
25426
  ${toolList}
25109
25427
 
25110
- Do NOT "stand by" or wait for anyone \u2014 every cycle must make progress. Act autonomously: make decisions and execute. Inform the keeper of progress or important updates using ${sendKeeperTool}, but never block on a response. If the keeper hasn't replied, proceed with your best judgment.
25428
+ Do NOT "stand by" or wait for anyone \u2014 every cycle must make progress. Act autonomously: make decisions and execute. Inform the keeper of progress or important updates using quoroom_send_message (to="keeper"), but never block on a response. If the keeper hasn't replied, proceed with your best judgment.
25429
+
25430
+ Revenue is always a priority. Every room must sustain itself financially. Actively seek ways to earn: offer services to the keeper, propose paid work to other rooms, or find monetizable opportunities in your domain. Check your wallet balance and report financial status to the keeper.
25111
25431
 
25112
25432
  ${toolCallInstruction}`);
25113
25433
  const prompt = contextParts.join("\n\n");
@@ -25117,8 +25437,9 @@ ${toolCallInstruction}`);
25117
25437
  logBuffer.flush();
25118
25438
  const apiKey = apiKeyEarly;
25119
25439
  const needsQueenTools = model === "openai" || model.startsWith("openai:") || model === "anthropic" || model.startsWith("anthropic:") || model.startsWith("claude-api:");
25440
+ const filteredToolDefs = allowSet ? QUEEN_TOOL_DEFINITIONS.filter((t) => allowSet.has(t.function.name)) : QUEEN_TOOL_DEFINITIONS;
25120
25441
  const apiToolOpts = needsQueenTools ? {
25121
- toolDefs: QUEEN_TOOL_DEFINITIONS,
25442
+ toolDefs: filteredToolDefs,
25122
25443
  onToolCall: async (toolName, args) => {
25123
25444
  logBuffer.addSynthetic("tool_call", `\u2192 ${toolName}(${JSON.stringify(args)})`);
25124
25445
  const result2 = await executeQueenTool(db2, roomId, worker.id, toolName, args);
@@ -25134,6 +25455,8 @@ ${toolCallInstruction}`);
25134
25455
  timeoutMs: 5 * 60 * 1e3,
25135
25456
  maxTurns: maxTurns ?? 10,
25136
25457
  onConsoleLog: logBuffer.onConsoleLog,
25458
+ // CLI models: block non-quoroom MCP tools (daymon, etc.)
25459
+ disallowedTools: isCli ? "mcp__daymon*" : void 0,
25137
25460
  // CLI models: pass resumeSessionId for native --resume
25138
25461
  resumeSessionId,
25139
25462
  // API models: pass conversation history + persistence callback
@@ -25148,6 +25471,23 @@ ${toolCallInstruction}`);
25148
25471
  if (rateLimitInfo) {
25149
25472
  throw new RateLimitError(rateLimitInfo);
25150
25473
  }
25474
+ if (result.exitCode !== 0) {
25475
+ const errorDetail = result.output?.trim() || `exit code ${result.exitCode}`;
25476
+ logBuffer.addSynthetic("error", `Agent execution failed: ${errorDetail.slice(0, 500)}`);
25477
+ logBuffer.flush();
25478
+ completeWorkerCycle(db2, cycle.id, errorDetail.slice(0, 500), result.usage);
25479
+ options?.onCycleLifecycle?.("failed", cycle.id, roomId);
25480
+ logRoomActivity(
25481
+ db2,
25482
+ roomId,
25483
+ "error",
25484
+ `Agent cycle failed (${worker.name}): ${errorDetail.slice(0, 200)}`,
25485
+ errorDetail,
25486
+ worker.id
25487
+ );
25488
+ updateAgentState(db2, worker.id, "idle");
25489
+ return result.output;
25490
+ }
25151
25491
  if (isCli && result.sessionId) {
25152
25492
  saveAgentSession(db2, worker.id, { sessionId: result.sessionId, model });
25153
25493
  }
@@ -25556,6 +25896,7 @@ function registerRoomRoutes(router) {
25556
25896
  const trimmed = body.queenNickname.trim().replace(/\s+/g, "");
25557
25897
  if (trimmed.length > 0 && trimmed.length <= 40) updates.queenNickname = trimmed;
25558
25898
  }
25899
+ if (body.allowedTools !== void 0) updates.allowedTools = body.allowedTools || null;
25559
25900
  if (body.config !== void 0 && typeof body.config === "object" && body.config !== null) {
25560
25901
  updates.config = { ...room.config, ...body.config };
25561
25902
  }
@@ -25621,7 +25962,7 @@ function registerRoomRoutes(router) {
25621
25962
  if (!room) return { status: 404, error: "Room not found" };
25622
25963
  if (!room.queenWorkerId) return { status: 404, error: "No queen worker" };
25623
25964
  const worker = getWorker(ctx.db, room.queenWorkerId);
25624
- const model = worker?.model ?? null;
25965
+ const model = worker?.model ?? room.workerModel ?? null;
25625
25966
  const auth = await getModelAuthStatus(ctx.db, roomId, model);
25626
25967
  return {
25627
25968
  data: {
@@ -25745,6 +26086,29 @@ function registerWorkerRoutes(router) {
25745
26086
  eventBus.emit("workers", "worker:deleted", { id });
25746
26087
  return { data: { ok: true } };
25747
26088
  });
26089
+ router.post("/api/workers/:id/start", (ctx) => {
26090
+ const id = Number(ctx.params.id);
26091
+ const worker = getWorker(ctx.db, id);
26092
+ if (!worker) return { status: 404, error: "Worker not found" };
26093
+ if (!worker.roomId) return { status: 400, error: "Worker has no room" };
26094
+ const room = getRoom(ctx.db, worker.roomId);
26095
+ if (!room) return { status: 404, error: "Room not found" };
26096
+ if (room.status !== "active") return { status: 400, error: "Room is not active" };
26097
+ triggerAgent(ctx.db, worker.roomId, id, {
26098
+ onCycleLogEntry: (entry) => eventBus.emit(`cycle:${entry.cycleId}`, "cycle:log", entry),
26099
+ onCycleLifecycle: (event, cycleId) => eventBus.emit(`room:${worker.roomId}`, `cycle:${event}`, { cycleId, roomId: worker.roomId })
26100
+ });
26101
+ eventBus.emit("workers", "worker:started", { id, roomId: worker.roomId });
26102
+ return { data: { ok: true, running: true } };
26103
+ });
26104
+ router.post("/api/workers/:id/stop", (ctx) => {
26105
+ const id = Number(ctx.params.id);
26106
+ const worker = getWorker(ctx.db, id);
26107
+ if (!worker) return { status: 404, error: "Worker not found" };
26108
+ pauseAgent(ctx.db, id);
26109
+ eventBus.emit("workers", "worker:stopped", { id });
26110
+ return { data: { ok: true, running: false } };
26111
+ });
25748
26112
  router.get("/api/rooms/:roomId/workers", (ctx) => {
25749
26113
  const workers = listRoomWorkers(ctx.db, Number(ctx.params.roomId));
25750
26114
  return { data: workers };
@@ -28047,7 +28411,15 @@ function registerChatRoutes(router) {
28047
28411
  // 3 minutes
28048
28412
  });
28049
28413
  if (result.exitCode !== 0 || result.timedOut) {
28050
- const reason = result.output?.trim() || (result.timedOut ? "Chat request timed out" : "Chat execution failed");
28414
+ const rawOutput = result.output?.trim();
28415
+ let reason;
28416
+ if (result.timedOut) {
28417
+ reason = "Chat request timed out";
28418
+ } else if (rawOutput) {
28419
+ reason = rawOutput;
28420
+ } else {
28421
+ reason = `Chat execution failed (model: ${model}, exit code: ${result.exitCode})`;
28422
+ }
28051
28423
  return { status: result.timedOut ? 504 : 502, error: reason.slice(0, 500) };
28052
28424
  }
28053
28425
  const response = result.output || "No response";
@@ -28134,6 +28506,7 @@ CREATE TABLE IF NOT EXISTS rooms (
28134
28506
  queen_nickname TEXT,
28135
28507
  chat_session_id TEXT,
28136
28508
  referred_by_code TEXT,
28509
+ allowed_tools TEXT,
28137
28510
  created_at DATETIME DEFAULT (datetime('now','localtime')),
28138
28511
  updated_at DATETIME DEFAULT (datetime('now','localtime'))
28139
28512
  );
@@ -28618,6 +28991,13 @@ function runMigrations(database, log = console.log) {
28618
28991
  database.exec(`ALTER TABLE workers ADD COLUMN max_turns INTEGER`);
28619
28992
  log("Migrated: added cycle_gap_ms and max_turns columns to workers");
28620
28993
  }
28994
+ const hasRoomAllowedTools = database.prepare(
28995
+ `SELECT name FROM pragma_table_info('rooms') WHERE name='allowed_tools'`
28996
+ ).get()?.name;
28997
+ if (!hasRoomAllowedTools) {
28998
+ database.exec(`ALTER TABLE rooms ADD COLUMN allowed_tools TEXT`);
28999
+ log("Migrated: added allowed_tools column to rooms");
29000
+ }
28621
29001
  const ollamaWorkers = database.prepare(`SELECT id FROM workers WHERE model LIKE 'ollama:%'`).all();
28622
29002
  if (ollamaWorkers.length > 0) {
28623
29003
  database.prepare(`UPDATE workers SET model = 'claude' WHERE model LIKE 'ollama:%'`).run();
@@ -28835,7 +29215,7 @@ function semverGt(a, b) {
28835
29215
  }
28836
29216
  function getCurrentVersion() {
28837
29217
  try {
28838
- return true ? "0.1.21" : null.version;
29218
+ return true ? "0.1.23" : null.version;
28839
29219
  } catch {
28840
29220
  return "0.0.0";
28841
29221
  }
@@ -28992,7 +29372,7 @@ var cachedVersion = null;
28992
29372
  function getVersion3() {
28993
29373
  if (cachedVersion) return cachedVersion;
28994
29374
  try {
28995
- cachedVersion = true ? "0.1.21" : null.version;
29375
+ cachedVersion = true ? "0.1.23" : null.version;
28996
29376
  } catch {
28997
29377
  cachedVersion = "unknown";
28998
29378
  }
@@ -29659,6 +30039,12 @@ function registerRoomMessageRoutes(router) {
29659
30039
  if (!msg) return { status: 404, error: "Message not found" };
29660
30040
  return { data: msg };
29661
30041
  });
30042
+ router.post("/api/rooms/:roomId/messages/read-all", (ctx) => {
30043
+ const roomId = Number(ctx.params.roomId);
30044
+ const count = markAllRoomMessagesRead(ctx.db, roomId);
30045
+ if (count > 0) eventBus.emit(`room:${roomId}`, "room_message:updated", { roomId, allRead: true });
30046
+ return { data: { ok: true, count } };
30047
+ });
29662
30048
  router.post("/api/rooms/:roomId/messages/:id/read", (ctx) => {
29663
30049
  const roomId = Number(ctx.params.roomId);
29664
30050
  const id = Number(ctx.params.id);