quoroom 0.1.38 → 0.1.39

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.38",
9917
+ version: "0.1.39",
9918
9918
  description: "Open-source local AI agent framework \u2014 Queen, Workers, Quorum. Experimental research tool.",
9919
9919
  main: "./out/mcp/server.js",
9920
9920
  bin: {
@@ -22623,13 +22623,14 @@ Every cycle:
22623
22623
  1. Check if workers reported results (messages, completed goals)
22624
22624
  2. If work is done \u2192 send results to keeper, take next step
22625
22625
  3. If work is stuck \u2192 help unblock (new instructions, different approach)
22626
- 4. If new work needed \u2192 delegate to a worker with clear instructions
22627
- 5. If a decision needs input \u2192 announce it (workers can object within 10 min)
22626
+ 4. If no workers exist yet \u2192 create an executor worker first
22627
+ 5. If new work is needed \u2192 delegate to a worker with clear instructions, then poke/follow up
22628
+ 6. If a decision needs input \u2192 announce it and process objections/votes (announce/object flow)
22628
22629
 
22629
22630
  Talk to the keeper regularly \u2014 they are your client.
22630
22631
 
22631
- Do NOT do execution work yourself (research, form filling, account creation).
22632
- Delegate it. That's what workers are for.`;
22632
+ Do NOT execute tasks directly (research, form filling, account creation, browser automation).
22633
+ Stay control-plane only: create workers, delegate, monitor, unblock, report.`;
22633
22634
  function createRoom2(db2, input) {
22634
22635
  const config = { ...DEFAULT_ROOM_CONFIG, ...input.config };
22635
22636
  const room = createRoom(db2, input.name, input.goal, config, input.referredByCode);
@@ -23227,7 +23228,10 @@ async function executeAgent(options) {
23227
23228
  }
23228
23229
  return executeAnthropicApi(options);
23229
23230
  }
23230
- return executeClaude(options);
23231
+ if (model === "claude" || model.startsWith("claude-")) {
23232
+ return executeClaude(options);
23233
+ }
23234
+ throw new Error(`Unsupported model "${model}". Configure an explicit supported model (claude, codex, openai:*, anthropic:*, gemini:*).`);
23231
23235
  }
23232
23236
  async function executeClaude(options) {
23233
23237
  const execOpts = {
@@ -24866,7 +24870,7 @@ var TOOL_WEB_SEARCH = {
24866
24870
  type: "function",
24867
24871
  function: {
24868
24872
  name: "quoroom_web_search",
24869
- description: "Search the web. Returns top 5 results.",
24873
+ description: "Search the web. Returns top 5 results. Queen should delegate this to workers first in control-plane mode.",
24870
24874
  parameters: {
24871
24875
  type: "object",
24872
24876
  properties: {
@@ -24880,7 +24884,7 @@ var TOOL_WEB_FETCH = {
24880
24884
  type: "function",
24881
24885
  function: {
24882
24886
  name: "quoroom_web_fetch",
24883
- description: "Fetch any URL and return its content as clean markdown.",
24887
+ description: "Fetch any URL and return its content as clean markdown. Queen should delegate this to workers first in control-plane mode.",
24884
24888
  parameters: {
24885
24889
  type: "object",
24886
24890
  properties: {
@@ -24894,7 +24898,7 @@ var TOOL_BROWSER = {
24894
24898
  type: "function",
24895
24899
  function: {
24896
24900
  name: "quoroom_browser",
24897
- description: "Control a headless browser: navigate, click, fill forms, buy services, register domains, create accounts.",
24901
+ description: "Control a headless browser: navigate, click, fill forms, buy services, register domains, create accounts. Queen should delegate this to workers first in control-plane mode.",
24898
24902
  parameters: {
24899
24903
  type: "object",
24900
24904
  properties: {
@@ -25342,6 +25346,12 @@ async function deliverQueenMessage(db2, roomId, question) {
25342
25346
  }
25343
25347
 
25344
25348
  // src/shared/agent-loop.ts
25349
+ var QUEEN_EXECUTION_TOOLS = /* @__PURE__ */ new Set([
25350
+ "quoroom_web_search",
25351
+ "quoroom_web_fetch",
25352
+ "quoroom_browser"
25353
+ ]);
25354
+ var QUEEN_POLICY_WIP_HINT = "[policy] Queen control-plane mode: delegate execution tasks to workers with quoroom_delegate_task, then monitor, unblock, and report outcomes. Avoid direct web/browser execution.";
25345
25355
  function isInQuietHours(from14, until) {
25346
25356
  const now = /* @__PURE__ */ new Date();
25347
25357
  const nowMins = now.getHours() * 60 + now.getMinutes();
@@ -25362,7 +25372,34 @@ function msUntilQuietEnd(until) {
25362
25372
  if (end <= now) end.setDate(end.getDate() + 1);
25363
25373
  return end.getTime() - now.getTime();
25364
25374
  }
25375
+ function nextAutoExecutorName(workers) {
25376
+ const names = new Set(workers.map((w) => w.name.toLowerCase()));
25377
+ let idx = 1;
25378
+ while (names.has(`executor-${idx}`)) idx++;
25379
+ return `executor-${idx}`;
25380
+ }
25381
+ function extractToolNameFromConsoleLog(content) {
25382
+ const usingMatch = content.match(/(?:Using|→)\s*([a-zA-Z0-9_]+)/);
25383
+ if (usingMatch?.[1]) return usingMatch[1];
25384
+ const callMatch = content.match(/^([a-zA-Z0-9_]+)\s*\(/);
25385
+ return callMatch?.[1] ?? null;
25386
+ }
25387
+ function resolveWorkerExecutionModel(db2, roomId, worker) {
25388
+ const explicit = worker.model?.trim();
25389
+ if (explicit) return explicit;
25390
+ const room = getRoom(db2, roomId);
25391
+ if (!room) return null;
25392
+ const roomModel = room.workerModel?.trim();
25393
+ if (!roomModel) return null;
25394
+ if (roomModel !== "queen") return roomModel;
25395
+ if (!room.queenWorkerId) return null;
25396
+ if (room.queenWorkerId === worker.id) return null;
25397
+ const queen = getWorker(db2, room.queenWorkerId);
25398
+ const queenModel = queen?.model?.trim();
25399
+ return queenModel || null;
25400
+ }
25365
25401
  var runningLoops = /* @__PURE__ */ new Map();
25402
+ var launchedRoomIds = /* @__PURE__ */ new Set();
25366
25403
  var RateLimitError = class extends Error {
25367
25404
  constructor(info) {
25368
25405
  super(`Rate limited: wait ${Math.round(info.waitMs / 1e3)}s`);
@@ -25485,12 +25522,29 @@ function pauseAgent(db2, workerId) {
25485
25522
  }
25486
25523
  updateAgentState(db2, workerId, "idle");
25487
25524
  }
25525
+ function setRoomLaunchEnabled(roomId, enabled) {
25526
+ if (enabled) {
25527
+ launchedRoomIds.add(roomId);
25528
+ return;
25529
+ }
25530
+ launchedRoomIds.delete(roomId);
25531
+ }
25532
+ function isRoomLaunchEnabled(roomId) {
25533
+ return launchedRoomIds.has(roomId);
25534
+ }
25535
+ function clearRoomLaunchState() {
25536
+ launchedRoomIds.clear();
25537
+ }
25488
25538
  function triggerAgent(db2, roomId, workerId, options) {
25489
25539
  const loop = runningLoops.get(workerId);
25490
25540
  if (loop?.running) {
25491
25541
  if (loop.waitAbort) loop.waitAbort.abort();
25492
25542
  return;
25493
25543
  }
25544
+ const canColdStart = options?.allowColdStart === true || isRoomLaunchEnabled(roomId);
25545
+ if (!canColdStart) {
25546
+ return;
25547
+ }
25494
25548
  startAgentLoop(db2, roomId, workerId, options).catch((err) => {
25495
25549
  const msg = err instanceof Error ? err.message : String(err);
25496
25550
  console.error(`Agent loop failed for worker ${workerId}: ${msg}`);
@@ -25538,7 +25592,7 @@ async function runCycle(db2, roomId, worker, maxTurns, options, abortSignal) {
25538
25592
  void 0,
25539
25593
  worker.id
25540
25594
  );
25541
- const model = worker.model ?? "claude";
25595
+ const model = resolveWorkerExecutionModel(db2, roomId, worker);
25542
25596
  const cycle = createWorkerCycle(db2, worker.id, roomId, model);
25543
25597
  const logBuffer2 = createCycleLogBuffer(
25544
25598
  cycle.id,
@@ -25547,6 +25601,23 @@ async function runCycle(db2, roomId, worker, maxTurns, options, abortSignal) {
25547
25601
  );
25548
25602
  options?.onCycleLifecycle?.("created", cycle.id, roomId);
25549
25603
  try {
25604
+ if (!model) {
25605
+ const msg = "No model configured for this worker. Set an explicit worker model or room worker model.";
25606
+ logBuffer2.addSynthetic("error", msg);
25607
+ logBuffer2.flush();
25608
+ completeWorkerCycle(db2, cycle.id, msg, void 0);
25609
+ options?.onCycleLifecycle?.("failed", cycle.id, roomId);
25610
+ logRoomActivity(
25611
+ db2,
25612
+ roomId,
25613
+ "error",
25614
+ `Agent cycle failed (${worker.name}): model is not configured`,
25615
+ msg,
25616
+ worker.id
25617
+ );
25618
+ updateAgentState(db2, worker.id, "idle");
25619
+ return msg;
25620
+ }
25550
25621
  const provider = getModelProvider(model);
25551
25622
  if (provider === "openai_api" || provider === "anthropic_api" || provider === "gemini_api") {
25552
25623
  const apiKeyCheck = resolveApiKeyForModel(db2, roomId, model);
@@ -25573,10 +25644,44 @@ async function runCycle(db2, roomId, worker, maxTurns, options, abortSignal) {
25573
25644
  status: g.status,
25574
25645
  assignedWorkerId: g.assignedWorkerId
25575
25646
  }));
25576
- const roomWorkers = listRoomWorkers(db2, roomId);
25647
+ let roomWorkers = listRoomWorkers(db2, roomId);
25648
+ const isQueen = worker.id === status2.room.queenWorkerId;
25577
25649
  const unreadMessages = listRoomMessages(db2, roomId, "unread").slice(0, 5);
25650
+ if (isQueen) {
25651
+ const nonQueenWorkers = roomWorkers.filter((w) => w.id !== worker.id);
25652
+ if (nonQueenWorkers.length === 0) {
25653
+ const autoName = nextAutoExecutorName(roomWorkers);
25654
+ const executorPreset = WORKER_ROLE_PRESETS.executor;
25655
+ const inheritedModel = status2.room.workerModel === "queen" ? model : status2.room.workerModel?.trim();
25656
+ if (!inheritedModel) {
25657
+ const err = "Auto-create skipped: no worker model configured for executor.";
25658
+ logRoomActivity(db2, roomId, "error", err, "Set room worker model or queen model first.", worker.id);
25659
+ logBuffer2.addSynthetic("error", err);
25660
+ } else {
25661
+ createWorker(db2, {
25662
+ name: autoName,
25663
+ role: "executor",
25664
+ roomId,
25665
+ description: "Auto-created executor for queen-delegated execution work.",
25666
+ systemPrompt: "You are the room executor. Complete delegated tasks end-to-end, report concrete results, and save progress with quoroom_save_wip.",
25667
+ model: inheritedModel,
25668
+ cycleGapMs: executorPreset?.cycleGapMs,
25669
+ maxTurns: executorPreset?.maxTurns
25670
+ });
25671
+ logRoomActivity(
25672
+ db2,
25673
+ roomId,
25674
+ "system",
25675
+ `Auto-created worker "${autoName}" for delegation-first execution.`,
25676
+ "Model B (soft): queen coordinates, workers execute.",
25677
+ worker.id
25678
+ );
25679
+ logBuffer2.addSynthetic("system", `Auto-created worker "${autoName}" because queen had no executors.`);
25680
+ roomWorkers = listRoomWorkers(db2, roomId);
25681
+ }
25682
+ }
25683
+ }
25578
25684
  const rolePreset = worker.role ? WORKER_ROLE_PRESETS[worker.role] : void 0;
25579
- const isQueen = worker.id === status2.room.queenWorkerId;
25580
25685
  const namePrefix = worker.name ? `Your name is ${worker.name}.
25581
25686
 
25582
25687
  ` : "";
@@ -25665,6 +25770,14 @@ At the end of this cycle, call quoroom_save_wip to save your updated position.`)
25665
25770
  if (status2.room.goal) {
25666
25771
  contextParts.push(`## Room Objective
25667
25772
  ${status2.room.goal}`);
25773
+ }
25774
+ if (isQueen) {
25775
+ contextParts.push(`## Queen Controller Contract (Model B)
25776
+ - You are the control plane: create workers, delegate tasks, and monitor delivery.
25777
+ - If there are no workers besides you, create one executor first.
25778
+ - Delegate all execution via quoroom_delegate_task and follow up with worker messages/pokes.
25779
+ - Keep governance active: use quoroom_announce for decisions and process objections/votes.
25780
+ - Do not perform execution tasks directly unless strictly unavoidable.`);
25668
25781
  }
25669
25782
  if (goalUpdates.length > 0) {
25670
25783
  const workerMap = new Map(roomWorkers.map((w) => [w.id, w.name]));
@@ -25777,9 +25890,34 @@ ${unreadMessages.map(
25777
25890
  const needsQueenTools = model === "openai" || model.startsWith("openai:") || model === "anthropic" || model.startsWith("anthropic:") || model.startsWith("claude-api:");
25778
25891
  const roleToolDefs = isQueen ? QUEEN_TOOLS : WORKER_TOOLS;
25779
25892
  const filteredToolDefs = allowSet ? roleToolDefs.filter((t) => allowSet.has(t.function.name)) : roleToolDefs;
25893
+ const queenExecutionToolsUsed = /* @__PURE__ */ new Set();
25894
+ const trackQueenExecutionTool = (toolName) => {
25895
+ if (!isQueen || !toolName) return;
25896
+ if (QUEEN_EXECUTION_TOOLS.has(toolName)) queenExecutionToolsUsed.add(toolName);
25897
+ };
25898
+ const persistQueenPolicyDeviation = () => {
25899
+ if (!isQueen || queenExecutionToolsUsed.size === 0) return;
25900
+ const used = [...queenExecutionToolsUsed].sort().join(", ");
25901
+ logRoomActivity(
25902
+ db2,
25903
+ roomId,
25904
+ "system",
25905
+ `Queen policy deviation: execution tool use detected (${used}).`,
25906
+ "Model B (soft): queen should delegate execution to workers and remain control-plane focused.",
25907
+ worker.id
25908
+ );
25909
+ const fresh = getWorker(db2, worker.id);
25910
+ const existing = fresh?.wip?.trim() ?? "";
25911
+ if (existing.includes(QUEEN_POLICY_WIP_HINT)) return;
25912
+ const nextWip = existing ? `${existing}
25913
+
25914
+ ${QUEEN_POLICY_WIP_HINT}` : QUEEN_POLICY_WIP_HINT;
25915
+ updateWorkerWip(db2, worker.id, nextWip.slice(0, 2e3));
25916
+ };
25780
25917
  const apiToolOpts = needsQueenTools ? {
25781
25918
  toolDefs: filteredToolDefs,
25782
25919
  onToolCall: async (toolName, args) => {
25920
+ trackQueenExecutionTool(toolName);
25783
25921
  logBuffer2.addSynthetic("tool_call", `\u2192 ${toolName}(${JSON.stringify(args)})`);
25784
25922
  const result2 = await executeQueenTool(db2, roomId, worker.id, toolName, args);
25785
25923
  logBuffer2.addSynthetic("tool_result", result2.content);
@@ -25793,7 +25931,12 @@ ${unreadMessages.map(
25793
25931
  apiKey,
25794
25932
  timeoutMs: worker.role === "executor" ? 30 * 60 * 1e3 : 15 * 60 * 1e3,
25795
25933
  maxTurns: maxTurns ?? 50,
25796
- onConsoleLog: logBuffer2.onConsoleLog,
25934
+ onConsoleLog: (entry) => {
25935
+ if (entry.entryType === "tool_call") {
25936
+ trackQueenExecutionTool(extractToolNameFromConsoleLog(entry.content));
25937
+ }
25938
+ logBuffer2.onConsoleLog(entry);
25939
+ },
25797
25940
  // CLI models: block non-quoroom MCP tools (daymon, etc.)
25798
25941
  disallowedTools: isCli ? "mcp__daymon*" : void 0,
25799
25942
  // CLI models: bypass permission prompts for headless operation
@@ -25826,6 +25969,7 @@ ${unreadMessages.map(
25826
25969
  completeWorkerCycle(db2, cycle.id, canceledMessage, result.usage);
25827
25970
  options?.onCycleLifecycle?.("failed", cycle.id, roomId);
25828
25971
  updateAgentState(db2, worker.id, "idle");
25972
+ persistQueenPolicyDeviation();
25829
25973
  return result.output;
25830
25974
  }
25831
25975
  const rateLimitInfo = checkRateLimit(result);
@@ -25854,6 +25998,7 @@ ${unreadMessages.map(
25854
25998
  logBuffer2.flush();
25855
25999
  }
25856
26000
  }
26001
+ persistQueenPolicyDeviation();
25857
26002
  return result.output;
25858
26003
  }
25859
26004
  if (isCli && result.sessionId) {
@@ -25862,6 +26007,7 @@ ${unreadMessages.map(
25862
26007
  if (result.output && model !== "claude" && !model.startsWith("codex")) {
25863
26008
  logBuffer2.addSynthetic("assistant_text", result.output);
25864
26009
  }
26010
+ persistQueenPolicyDeviation();
25865
26011
  logBuffer2.addSynthetic("system", "Cycle completed");
25866
26012
  if (result.usage && (result.usage.inputTokens > 0 || result.usage.outputTokens > 0)) {
25867
26013
  logBuffer2.addSynthetic("system", `Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out`);
@@ -25912,6 +26058,7 @@ function _stopAllLoops() {
25912
26058
  if (loop.cycleAbort) loop.cycleAbort.abort();
25913
26059
  }
25914
26060
  runningLoops.clear();
26061
+ clearRoomLaunchState();
25915
26062
  }
25916
26063
 
25917
26064
  // src/server/cloud.ts
@@ -28281,13 +28428,10 @@ async function executeClerkTool(db2, toolName, args, ctx) {
28281
28428
  return { content: `Deleted room "${room.name}" (#${room.id}).` };
28282
28429
  }
28283
28430
  case "quoroom_start_queen": {
28284
- const room = resolveRoom(db2, args);
28285
- if (!room) return { content: "Error: room not found.", isError: true };
28286
- if (!room.queenWorkerId) return { content: `Error: room "${room.name}" has no queen worker.`, isError: true };
28287
- if (room.status !== "active") return { content: `Error: room "${room.name}" is not active.`, isError: true };
28288
- stopRoomRuntime(db2, room.id, "Runtime reset before queen start");
28289
- triggerAgent(db2, room.id, room.queenWorkerId);
28290
- return { content: `Started queen in "${room.name}" (#${room.id}).` };
28431
+ return {
28432
+ content: "Error: direct queen start is disabled. Start the room manually from the Room controls.",
28433
+ isError: true
28434
+ };
28291
28435
  }
28292
28436
  case "quoroom_stop_queen": {
28293
28437
  const room = resolveRoom(db2, args);
@@ -30488,6 +30632,9 @@ async function syncCloudRoomMessages(db2) {
30488
30632
  }
30489
30633
  function startServerRuntime(db2) {
30490
30634
  stopServerRuntime();
30635
+ clearRoomLaunchState();
30636
+ const cleanedCycles = cleanupStaleCycles(db2);
30637
+ if (cleanedCycles > 0) console.log(`Cleaned up ${cleanedCycles} stale worker cycles`);
30491
30638
  ensureClerkContactCheckTasks(db2);
30492
30639
  refreshCronJobs(db2);
30493
30640
  runDueOneTimeTasks(db2);
@@ -30515,30 +30662,8 @@ function startServerRuntime(db2) {
30515
30662
  clerkAlertTimer = setInterval(() => {
30516
30663
  queueClerkAlertRelay(db2);
30517
30664
  }, CLERK_ALERT_RELAY_MS);
30518
- resumeActiveQueens(db2);
30519
30665
  startCommentaryEngine(db2);
30520
30666
  }
30521
- function makeCycleCallbacks() {
30522
- return {
30523
- onCycleLogEntry: (entry) => {
30524
- eventBus.emit(`cycle:${entry.cycleId}`, "cycle:log", entry);
30525
- },
30526
- onCycleLifecycle: (event, cycleId, roomId) => {
30527
- eventBus.emit(`room:${roomId}`, `cycle:${event}`, { cycleId, roomId });
30528
- }
30529
- };
30530
- }
30531
- function resumeActiveQueens(db2) {
30532
- const cleaned = cleanupStaleCycles(db2);
30533
- if (cleaned > 0) console.log(`Cleaned up ${cleaned} stale worker cycles`);
30534
- const rooms = listRooms(db2, "active");
30535
- const callbacks = makeCycleCallbacks();
30536
- for (const room of rooms) {
30537
- if (!room.queenWorkerId) continue;
30538
- console.log(`Auto-resuming queen for room "${room.name}" (#${room.id})`);
30539
- triggerAgent(db2, room.id, room.queenWorkerId, callbacks);
30540
- }
30541
- }
30542
30667
  function stopServerRuntime() {
30543
30668
  stopCommentaryEngine();
30544
30669
  if (schedulerTimer) clearInterval(schedulerTimer);
@@ -30583,6 +30708,16 @@ function emitQueenState(roomId, running) {
30583
30708
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
30584
30709
  });
30585
30710
  }
30711
+ function makeCycleCallbacks(roomId) {
30712
+ return {
30713
+ onCycleLogEntry: (entry) => {
30714
+ eventBus.emit(`cycle:${entry.cycleId}`, "cycle:log", entry);
30715
+ },
30716
+ onCycleLifecycle: (event, cycleId, _roomId) => {
30717
+ eventBus.emit(`room:${roomId}`, `cycle:${event}`, { cycleId, roomId });
30718
+ }
30719
+ };
30720
+ }
30586
30721
  function getLocalReferredRooms(db2, roomId) {
30587
30722
  const keeperCode = (getSetting(db2, "keeper_referral_code") ?? "").trim();
30588
30723
  if (!keeperCode) return [];
@@ -30796,11 +30931,8 @@ function registerRoomRoutes(router) {
30796
30931
  }
30797
30932
  }
30798
30933
  if (updates.status === "stopped") {
30799
- const workers = listRoomWorkers(ctx.db, roomId);
30800
- for (const w of workers) {
30801
- updateAgentState(ctx.db, w.id, "idle");
30802
- pauseAgent(ctx.db, w.id);
30803
- }
30934
+ setRoomLaunchEnabled(roomId, false);
30935
+ stopRoomRuntime(ctx.db, roomId, "Room archived");
30804
30936
  logRoomActivity(ctx.db, roomId, "system", "Room archived");
30805
30937
  }
30806
30938
  const updated = getRoom(ctx.db, roomId);
@@ -30815,21 +30947,59 @@ function registerRoomRoutes(router) {
30815
30947
  const roomId = Number(ctx.params.id);
30816
30948
  try {
30817
30949
  pauseRoom(ctx.db, roomId);
30818
- const workers = listRoomWorkers(ctx.db, roomId);
30819
- for (const w of workers) {
30820
- pauseAgent(ctx.db, w.id);
30821
- }
30950
+ setRoomLaunchEnabled(roomId, false);
30951
+ stopRoomRuntime(ctx.db, roomId, "Room stopped by keeper");
30822
30952
  eventBus.emit(`room:${roomId}`, "room:paused", { roomId });
30953
+ eventBus.emit(`room:${roomId}`, "room:queen_stopped", { roomId, running: false });
30954
+ emitQueenState(roomId, false);
30823
30955
  emitRoomsUpdated("room_paused", { roomId });
30824
30956
  return { data: { ok: true } };
30825
30957
  } catch (e) {
30826
30958
  return { status: 404, error: e.message };
30827
30959
  }
30828
30960
  });
30961
+ router.post("/api/rooms/:id/start", (ctx) => {
30962
+ const roomId = Number(ctx.params.id);
30963
+ const room = getRoom(ctx.db, roomId);
30964
+ if (!room) return { status: 404, error: "Room not found" };
30965
+ if (room.status === "stopped") return { status: 400, error: "Room is stopped" };
30966
+ if (!room.queenWorkerId) return { status: 400, error: "No queen worker" };
30967
+ if (room.status !== "active") {
30968
+ updateRoom(ctx.db, roomId, { status: "active" });
30969
+ }
30970
+ setRoomLaunchEnabled(roomId, true);
30971
+ stopRoomRuntime(ctx.db, roomId, "Runtime reset before room start");
30972
+ triggerAgent(ctx.db, roomId, room.queenWorkerId, {
30973
+ ...makeCycleCallbacks(roomId),
30974
+ allowColdStart: true
30975
+ });
30976
+ eventBus.emit(`room:${roomId}`, "room:started", { roomId });
30977
+ eventBus.emit(`room:${roomId}`, "room:queen_started", { roomId, running: true });
30978
+ emitQueenState(roomId, true);
30979
+ emitRoomsUpdated("room_started", { roomId });
30980
+ return { data: { ok: true, running: true } };
30981
+ });
30982
+ router.post("/api/rooms/:id/stop", (ctx) => {
30983
+ const roomId = Number(ctx.params.id);
30984
+ const room = getRoom(ctx.db, roomId);
30985
+ if (!room) return { status: 404, error: "Room not found" };
30986
+ setRoomLaunchEnabled(roomId, false);
30987
+ stopRoomRuntime(ctx.db, roomId, "Room stopped by keeper");
30988
+ if (room.status !== "stopped") {
30989
+ updateRoom(ctx.db, roomId, { status: "paused" });
30990
+ }
30991
+ eventBus.emit(`room:${roomId}`, "room:stopped", { roomId });
30992
+ eventBus.emit(`room:${roomId}`, "room:paused", { roomId });
30993
+ eventBus.emit(`room:${roomId}`, "room:queen_stopped", { roomId, running: false });
30994
+ emitQueenState(roomId, false);
30995
+ emitRoomsUpdated("room_paused", { roomId });
30996
+ return { data: { ok: true, running: false } };
30997
+ });
30829
30998
  router.post("/api/rooms/:id/restart", (ctx) => {
30830
30999
  const roomId = Number(ctx.params.id);
30831
31000
  const { goal } = ctx.body || {};
30832
31001
  try {
31002
+ setRoomLaunchEnabled(roomId, false);
30833
31003
  restartRoom(ctx.db, roomId, goal);
30834
31004
  eventBus.emit(`room:${roomId}`, "room:restarted", { roomId });
30835
31005
  emitRoomsUpdated("room_restarted", { roomId });
@@ -30857,30 +31027,11 @@ function registerRoomRoutes(router) {
30857
31027
  }
30858
31028
  };
30859
31029
  });
30860
- router.post("/api/rooms/:id/queen/start", (ctx) => {
30861
- const roomId = Number(ctx.params.id);
30862
- const room = getRoom(ctx.db, roomId);
30863
- if (!room) return { status: 404, error: "Room not found" };
30864
- if (room.status !== "active") return { status: 400, error: "Room is not active" };
30865
- if (!room.queenWorkerId) return { status: 400, error: "No queen worker" };
30866
- stopRoomRuntime(ctx.db, roomId, "Runtime reset before queen start");
30867
- triggerAgent(ctx.db, roomId, room.queenWorkerId, {
30868
- onCycleLogEntry: (entry) => eventBus.emit(`cycle:${entry.cycleId}`, "cycle:log", entry),
30869
- onCycleLifecycle: (event, cycleId) => eventBus.emit(`room:${roomId}`, `cycle:${event}`, { cycleId, roomId })
30870
- });
30871
- eventBus.emit(`room:${roomId}`, "room:queen_started", { roomId, running: true });
30872
- emitQueenState(roomId, true);
30873
- return { data: { ok: true, running: true } };
31030
+ router.post("/api/rooms/:id/queen/start", (_ctx) => {
31031
+ return { status: 410, error: "Deprecated. Use POST /api/rooms/:id/start" };
30874
31032
  });
30875
- router.post("/api/rooms/:id/queen/stop", (ctx) => {
30876
- const roomId = Number(ctx.params.id);
30877
- const room = getRoom(ctx.db, roomId);
30878
- if (!room) return { status: 404, error: "Room not found" };
30879
- if (!room.queenWorkerId) return { status: 400, error: "No queen worker" };
30880
- stopRoomRuntime(ctx.db, roomId, "Queen stopped by keeper");
30881
- eventBus.emit(`room:${roomId}`, "room:queen_stopped", { roomId, running: false });
30882
- emitQueenState(roomId, false);
30883
- return { data: { ok: true, running: false } };
31033
+ router.post("/api/rooms/:id/queen/stop", (_ctx) => {
31034
+ return { status: 410, error: "Deprecated. Use POST /api/rooms/:id/stop" };
30884
31035
  });
30885
31036
  router.get("/api/rooms/:id/cycles", (ctx) => {
30886
31037
  const roomId = Number(ctx.params.id);
@@ -30894,8 +31045,8 @@ function registerRoomRoutes(router) {
30894
31045
  const today = getRoomTokenUsageToday(ctx.db, roomId);
30895
31046
  const room = getRoom(ctx.db, roomId);
30896
31047
  const queenWorker = room?.queenWorkerId ? getWorker(ctx.db, room.queenWorkerId) : null;
30897
- const model = queenWorker?.model ?? room?.workerModel ?? "claude";
30898
- const isApiModel = model.startsWith("openai") || model.startsWith("anthropic") || model.startsWith("claude-api");
31048
+ const model = queenWorker?.model ?? room?.workerModel ?? null;
31049
+ const isApiModel = !!model && (model.startsWith("openai") || model.startsWith("anthropic") || model.startsWith("claude-api"));
30899
31050
  return { data: { total, today, isApiModel } };
30900
31051
  });
30901
31052
  router.get("/api/cycles/:id/logs", (ctx) => {
@@ -30908,10 +31059,8 @@ function registerRoomRoutes(router) {
30908
31059
  router.delete("/api/rooms/:id", (ctx) => {
30909
31060
  const roomId = Number(ctx.params.id);
30910
31061
  try {
30911
- const workers = listRoomWorkers(ctx.db, roomId);
30912
- for (const w of workers) {
30913
- pauseAgent(ctx.db, w.id);
30914
- }
31062
+ setRoomLaunchEnabled(roomId, false);
31063
+ stopRoomRuntime(ctx.db, roomId, "Room deleted");
30915
31064
  deleteRoom2(ctx.db, roomId);
30916
31065
  eventBus.emit(`room:${roomId}`, "room:deleted", { roomId });
30917
31066
  emitRoomsUpdated("room_deleted", { roomId });
@@ -30977,6 +31126,9 @@ function registerWorkerRoutes(router) {
30977
31126
  const room = getRoom(ctx.db, worker.roomId);
30978
31127
  if (!room) return { status: 404, error: "Room not found" };
30979
31128
  if (room.status !== "active") return { status: 400, error: "Room is not active" };
31129
+ if (!isRoomLaunchEnabled(worker.roomId)) {
31130
+ return { status: 409, error: "Room runtime is not started. Start the room first." };
31131
+ }
30980
31132
  triggerAgent(ctx.db, worker.roomId, id, {
30981
31133
  onCycleLogEntry: (entry) => eventBus.emit(`cycle:${entry.cycleId}`, "cycle:log", entry),
30982
31134
  onCycleLifecycle: (event, cycleId) => eventBus.emit(`room:${worker.roomId}`, `cycle:${event}`, { cycleId, roomId: worker.roomId })
@@ -32520,7 +32672,7 @@ function semverGt(a, b) {
32520
32672
  }
32521
32673
  function getCurrentVersion() {
32522
32674
  try {
32523
- return true ? "0.1.38" : null.version;
32675
+ return true ? "0.1.39" : null.version;
32524
32676
  } catch {
32525
32677
  return "0.0.0";
32526
32678
  }
@@ -32701,7 +32853,7 @@ var cachedVersion = null;
32701
32853
  function getVersion3() {
32702
32854
  if (cachedVersion) return cachedVersion;
32703
32855
  try {
32704
- cachedVersion = true ? "0.1.38" : null.version;
32856
+ cachedVersion = true ? "0.1.39" : null.version;
32705
32857
  } catch {
32706
32858
  cachedVersion = "unknown";
32707
32859
  }
package/out/mcp/cli.js CHANGED
@@ -26937,7 +26937,10 @@ async function executeAgent(options) {
26937
26937
  }
26938
26938
  return executeAnthropicApi(options);
26939
26939
  }
26940
- return executeClaude(options);
26940
+ if (model === "claude" || model.startsWith("claude-")) {
26941
+ return executeClaude(options);
26942
+ }
26943
+ throw new Error(`Unsupported model "${model}". Configure an explicit supported model (claude, codex, openai:*, anthropic:*, gemini:*).`);
26941
26944
  }
26942
26945
  async function executeClaude(options) {
26943
26946
  const execOpts = {
@@ -49454,13 +49457,14 @@ Every cycle:
49454
49457
  1. Check if workers reported results (messages, completed goals)
49455
49458
  2. If work is done \u2192 send results to keeper, take next step
49456
49459
  3. If work is stuck \u2192 help unblock (new instructions, different approach)
49457
- 4. If new work needed \u2192 delegate to a worker with clear instructions
49458
- 5. If a decision needs input \u2192 announce it (workers can object within 10 min)
49460
+ 4. If no workers exist yet \u2192 create an executor worker first
49461
+ 5. If new work is needed \u2192 delegate to a worker with clear instructions, then poke/follow up
49462
+ 6. If a decision needs input \u2192 announce it and process objections/votes (announce/object flow)
49459
49463
 
49460
49464
  Talk to the keeper regularly \u2014 they are your client.
49461
49465
 
49462
- Do NOT do execution work yourself (research, form filling, account creation).
49463
- Delegate it. That's what workers are for.`;
49466
+ Do NOT execute tasks directly (research, form filling, account creation, browser automation).
49467
+ Stay control-plane only: create workers, delegate, monitor, unblock, report.`;
49464
49468
  }
49465
49469
  });
49466
49470
 
@@ -52537,7 +52541,7 @@ var server_exports = {};
52537
52541
  async function main() {
52538
52542
  const server = new McpServer({
52539
52543
  name: "quoroom",
52540
- version: true ? "0.1.38" : "0.0.0"
52544
+ version: true ? "0.1.39" : "0.0.0"
52541
52545
  });
52542
52546
  registerMemoryTools(server);
52543
52547
  registerSchedulerTools(server);
@@ -53428,7 +53432,7 @@ var init_queen_tools = __esm({
53428
53432
  type: "function",
53429
53433
  function: {
53430
53434
  name: "quoroom_web_search",
53431
- description: "Search the web. Returns top 5 results.",
53435
+ description: "Search the web. Returns top 5 results. Queen should delegate this to workers first in control-plane mode.",
53432
53436
  parameters: {
53433
53437
  type: "object",
53434
53438
  properties: {
@@ -53442,7 +53446,7 @@ var init_queen_tools = __esm({
53442
53446
  type: "function",
53443
53447
  function: {
53444
53448
  name: "quoroom_web_fetch",
53445
- description: "Fetch any URL and return its content as clean markdown.",
53449
+ description: "Fetch any URL and return its content as clean markdown. Queen should delegate this to workers first in control-plane mode.",
53446
53450
  parameters: {
53447
53451
  type: "object",
53448
53452
  properties: {
@@ -53456,7 +53460,7 @@ var init_queen_tools = __esm({
53456
53460
  type: "function",
53457
53461
  function: {
53458
53462
  name: "quoroom_browser",
53459
- description: "Control a headless browser: navigate, click, fill forms, buy services, register domains, create accounts.",
53463
+ description: "Control a headless browser: navigate, click, fill forms, buy services, register domains, create accounts. Queen should delegate this to workers first in control-plane mode.",
53460
53464
  parameters: {
53461
53465
  type: "object",
53462
53466
  properties: {
@@ -53617,6 +53621,32 @@ function msUntilQuietEnd(until) {
53617
53621
  if (end <= now) end.setDate(end.getDate() + 1);
53618
53622
  return end.getTime() - now.getTime();
53619
53623
  }
53624
+ function nextAutoExecutorName(workers) {
53625
+ const names = new Set(workers.map((w) => w.name.toLowerCase()));
53626
+ let idx = 1;
53627
+ while (names.has(`executor-${idx}`)) idx++;
53628
+ return `executor-${idx}`;
53629
+ }
53630
+ function extractToolNameFromConsoleLog(content) {
53631
+ const usingMatch = content.match(/(?:Using|→)\s*([a-zA-Z0-9_]+)/);
53632
+ if (usingMatch?.[1]) return usingMatch[1];
53633
+ const callMatch = content.match(/^([a-zA-Z0-9_]+)\s*\(/);
53634
+ return callMatch?.[1] ?? null;
53635
+ }
53636
+ function resolveWorkerExecutionModel(db3, roomId, worker) {
53637
+ const explicit = worker.model?.trim();
53638
+ if (explicit) return explicit;
53639
+ const room = getRoom(db3, roomId);
53640
+ if (!room) return null;
53641
+ const roomModel = room.workerModel?.trim();
53642
+ if (!roomModel) return null;
53643
+ if (roomModel !== "queen") return roomModel;
53644
+ if (!room.queenWorkerId) return null;
53645
+ if (room.queenWorkerId === worker.id) return null;
53646
+ const queen = getWorker(db3, room.queenWorkerId);
53647
+ const queenModel = queen?.model?.trim();
53648
+ return queenModel || null;
53649
+ }
53620
53650
  async function startAgentLoop(db3, roomId, workerId, options) {
53621
53651
  const room = getRoom(db3, roomId);
53622
53652
  if (!room) throw new Error(`Room ${roomId} not found`);
@@ -53732,12 +53762,29 @@ function pauseAgent(db3, workerId) {
53732
53762
  }
53733
53763
  updateAgentState(db3, workerId, "idle");
53734
53764
  }
53765
+ function setRoomLaunchEnabled(roomId, enabled) {
53766
+ if (enabled) {
53767
+ launchedRoomIds.add(roomId);
53768
+ return;
53769
+ }
53770
+ launchedRoomIds.delete(roomId);
53771
+ }
53772
+ function isRoomLaunchEnabled(roomId) {
53773
+ return launchedRoomIds.has(roomId);
53774
+ }
53775
+ function clearRoomLaunchState() {
53776
+ launchedRoomIds.clear();
53777
+ }
53735
53778
  function triggerAgent(db3, roomId, workerId, options) {
53736
53779
  const loop = runningLoops.get(workerId);
53737
53780
  if (loop?.running) {
53738
53781
  if (loop.waitAbort) loop.waitAbort.abort();
53739
53782
  return;
53740
53783
  }
53784
+ const canColdStart = options?.allowColdStart === true || isRoomLaunchEnabled(roomId);
53785
+ if (!canColdStart) {
53786
+ return;
53787
+ }
53741
53788
  startAgentLoop(db3, roomId, workerId, options).catch((err) => {
53742
53789
  const msg = err instanceof Error ? err.message : String(err);
53743
53790
  console.error(`Agent loop failed for worker ${workerId}: ${msg}`);
@@ -53785,7 +53832,7 @@ async function runCycle(db3, roomId, worker, maxTurns, options, abortSignal) {
53785
53832
  void 0,
53786
53833
  worker.id
53787
53834
  );
53788
- const model = worker.model ?? "claude";
53835
+ const model = resolveWorkerExecutionModel(db3, roomId, worker);
53789
53836
  const cycle = createWorkerCycle(db3, worker.id, roomId, model);
53790
53837
  const logBuffer2 = createCycleLogBuffer(
53791
53838
  cycle.id,
@@ -53794,6 +53841,23 @@ async function runCycle(db3, roomId, worker, maxTurns, options, abortSignal) {
53794
53841
  );
53795
53842
  options?.onCycleLifecycle?.("created", cycle.id, roomId);
53796
53843
  try {
53844
+ if (!model) {
53845
+ const msg = "No model configured for this worker. Set an explicit worker model or room worker model.";
53846
+ logBuffer2.addSynthetic("error", msg);
53847
+ logBuffer2.flush();
53848
+ completeWorkerCycle(db3, cycle.id, msg, void 0);
53849
+ options?.onCycleLifecycle?.("failed", cycle.id, roomId);
53850
+ logRoomActivity(
53851
+ db3,
53852
+ roomId,
53853
+ "error",
53854
+ `Agent cycle failed (${worker.name}): model is not configured`,
53855
+ msg,
53856
+ worker.id
53857
+ );
53858
+ updateAgentState(db3, worker.id, "idle");
53859
+ return msg;
53860
+ }
53797
53861
  const provider = getModelProvider(model);
53798
53862
  if (provider === "openai_api" || provider === "anthropic_api" || provider === "gemini_api") {
53799
53863
  const apiKeyCheck = resolveApiKeyForModel(db3, roomId, model);
@@ -53820,10 +53884,44 @@ async function runCycle(db3, roomId, worker, maxTurns, options, abortSignal) {
53820
53884
  status: g.status,
53821
53885
  assignedWorkerId: g.assignedWorkerId
53822
53886
  }));
53823
- const roomWorkers = listRoomWorkers(db3, roomId);
53887
+ let roomWorkers = listRoomWorkers(db3, roomId);
53888
+ const isQueen = worker.id === status2.room.queenWorkerId;
53824
53889
  const unreadMessages = listRoomMessages(db3, roomId, "unread").slice(0, 5);
53890
+ if (isQueen) {
53891
+ const nonQueenWorkers = roomWorkers.filter((w) => w.id !== worker.id);
53892
+ if (nonQueenWorkers.length === 0) {
53893
+ const autoName = nextAutoExecutorName(roomWorkers);
53894
+ const executorPreset = WORKER_ROLE_PRESETS.executor;
53895
+ const inheritedModel = status2.room.workerModel === "queen" ? model : status2.room.workerModel?.trim();
53896
+ if (!inheritedModel) {
53897
+ const err = "Auto-create skipped: no worker model configured for executor.";
53898
+ logRoomActivity(db3, roomId, "error", err, "Set room worker model or queen model first.", worker.id);
53899
+ logBuffer2.addSynthetic("error", err);
53900
+ } else {
53901
+ createWorker(db3, {
53902
+ name: autoName,
53903
+ role: "executor",
53904
+ roomId,
53905
+ description: "Auto-created executor for queen-delegated execution work.",
53906
+ systemPrompt: "You are the room executor. Complete delegated tasks end-to-end, report concrete results, and save progress with quoroom_save_wip.",
53907
+ model: inheritedModel,
53908
+ cycleGapMs: executorPreset?.cycleGapMs,
53909
+ maxTurns: executorPreset?.maxTurns
53910
+ });
53911
+ logRoomActivity(
53912
+ db3,
53913
+ roomId,
53914
+ "system",
53915
+ `Auto-created worker "${autoName}" for delegation-first execution.`,
53916
+ "Model B (soft): queen coordinates, workers execute.",
53917
+ worker.id
53918
+ );
53919
+ logBuffer2.addSynthetic("system", `Auto-created worker "${autoName}" because queen had no executors.`);
53920
+ roomWorkers = listRoomWorkers(db3, roomId);
53921
+ }
53922
+ }
53923
+ }
53825
53924
  const rolePreset = worker.role ? WORKER_ROLE_PRESETS[worker.role] : void 0;
53826
- const isQueen = worker.id === status2.room.queenWorkerId;
53827
53925
  const namePrefix = worker.name ? `Your name is ${worker.name}.
53828
53926
 
53829
53927
  ` : "";
@@ -53912,6 +54010,14 @@ At the end of this cycle, call quoroom_save_wip to save your updated position.`)
53912
54010
  if (status2.room.goal) {
53913
54011
  contextParts.push(`## Room Objective
53914
54012
  ${status2.room.goal}`);
54013
+ }
54014
+ if (isQueen) {
54015
+ contextParts.push(`## Queen Controller Contract (Model B)
54016
+ - You are the control plane: create workers, delegate tasks, and monitor delivery.
54017
+ - If there are no workers besides you, create one executor first.
54018
+ - Delegate all execution via quoroom_delegate_task and follow up with worker messages/pokes.
54019
+ - Keep governance active: use quoroom_announce for decisions and process objections/votes.
54020
+ - Do not perform execution tasks directly unless strictly unavoidable.`);
53915
54021
  }
53916
54022
  if (goalUpdates.length > 0) {
53917
54023
  const workerMap = new Map(roomWorkers.map((w) => [w.id, w.name]));
@@ -54024,9 +54130,34 @@ ${unreadMessages.map(
54024
54130
  const needsQueenTools = model === "openai" || model.startsWith("openai:") || model === "anthropic" || model.startsWith("anthropic:") || model.startsWith("claude-api:");
54025
54131
  const roleToolDefs = isQueen ? QUEEN_TOOLS : WORKER_TOOLS;
54026
54132
  const filteredToolDefs = allowSet ? roleToolDefs.filter((t) => allowSet.has(t.function.name)) : roleToolDefs;
54133
+ const queenExecutionToolsUsed = /* @__PURE__ */ new Set();
54134
+ const trackQueenExecutionTool = (toolName) => {
54135
+ if (!isQueen || !toolName) return;
54136
+ if (QUEEN_EXECUTION_TOOLS.has(toolName)) queenExecutionToolsUsed.add(toolName);
54137
+ };
54138
+ const persistQueenPolicyDeviation = () => {
54139
+ if (!isQueen || queenExecutionToolsUsed.size === 0) return;
54140
+ const used = [...queenExecutionToolsUsed].sort().join(", ");
54141
+ logRoomActivity(
54142
+ db3,
54143
+ roomId,
54144
+ "system",
54145
+ `Queen policy deviation: execution tool use detected (${used}).`,
54146
+ "Model B (soft): queen should delegate execution to workers and remain control-plane focused.",
54147
+ worker.id
54148
+ );
54149
+ const fresh = getWorker(db3, worker.id);
54150
+ const existing = fresh?.wip?.trim() ?? "";
54151
+ if (existing.includes(QUEEN_POLICY_WIP_HINT)) return;
54152
+ const nextWip = existing ? `${existing}
54153
+
54154
+ ${QUEEN_POLICY_WIP_HINT}` : QUEEN_POLICY_WIP_HINT;
54155
+ updateWorkerWip(db3, worker.id, nextWip.slice(0, 2e3));
54156
+ };
54027
54157
  const apiToolOpts = needsQueenTools ? {
54028
54158
  toolDefs: filteredToolDefs,
54029
54159
  onToolCall: async (toolName, args2) => {
54160
+ trackQueenExecutionTool(toolName);
54030
54161
  logBuffer2.addSynthetic("tool_call", `\u2192 ${toolName}(${JSON.stringify(args2)})`);
54031
54162
  const result2 = await executeQueenTool(db3, roomId, worker.id, toolName, args2);
54032
54163
  logBuffer2.addSynthetic("tool_result", result2.content);
@@ -54040,7 +54171,12 @@ ${unreadMessages.map(
54040
54171
  apiKey,
54041
54172
  timeoutMs: worker.role === "executor" ? 30 * 60 * 1e3 : 15 * 60 * 1e3,
54042
54173
  maxTurns: maxTurns ?? 50,
54043
- onConsoleLog: logBuffer2.onConsoleLog,
54174
+ onConsoleLog: (entry) => {
54175
+ if (entry.entryType === "tool_call") {
54176
+ trackQueenExecutionTool(extractToolNameFromConsoleLog(entry.content));
54177
+ }
54178
+ logBuffer2.onConsoleLog(entry);
54179
+ },
54044
54180
  // CLI models: block non-quoroom MCP tools (daymon, etc.)
54045
54181
  disallowedTools: isCli ? "mcp__daymon*" : void 0,
54046
54182
  // CLI models: bypass permission prompts for headless operation
@@ -54073,6 +54209,7 @@ ${unreadMessages.map(
54073
54209
  completeWorkerCycle(db3, cycle.id, canceledMessage, result.usage);
54074
54210
  options?.onCycleLifecycle?.("failed", cycle.id, roomId);
54075
54211
  updateAgentState(db3, worker.id, "idle");
54212
+ persistQueenPolicyDeviation();
54076
54213
  return result.output;
54077
54214
  }
54078
54215
  const rateLimitInfo = checkRateLimit(result);
@@ -54101,6 +54238,7 @@ ${unreadMessages.map(
54101
54238
  logBuffer2.flush();
54102
54239
  }
54103
54240
  }
54241
+ persistQueenPolicyDeviation();
54104
54242
  return result.output;
54105
54243
  }
54106
54244
  if (isCli && result.sessionId) {
@@ -54109,6 +54247,7 @@ ${unreadMessages.map(
54109
54247
  if (result.output && model !== "claude" && !model.startsWith("codex")) {
54110
54248
  logBuffer2.addSynthetic("assistant_text", result.output);
54111
54249
  }
54250
+ persistQueenPolicyDeviation();
54112
54251
  logBuffer2.addSynthetic("system", "Cycle completed");
54113
54252
  if (result.usage && (result.usage.inputTokens > 0 || result.usage.outputTokens > 0)) {
54114
54253
  logBuffer2.addSynthetic("system", `Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out`);
@@ -54159,8 +54298,9 @@ function _stopAllLoops() {
54159
54298
  if (loop.cycleAbort) loop.cycleAbort.abort();
54160
54299
  }
54161
54300
  runningLoops.clear();
54301
+ clearRoomLaunchState();
54162
54302
  }
54163
- var runningLoops, RateLimitError;
54303
+ var QUEEN_EXECUTION_TOOLS, QUEEN_POLICY_WIP_HINT, runningLoops, launchedRoomIds, RateLimitError;
54164
54304
  var init_agent_loop = __esm({
54165
54305
  "src/shared/agent-loop.ts"() {
54166
54306
  "use strict";
@@ -54173,7 +54313,14 @@ var init_agent_loop = __esm({
54173
54313
  init_console_log_buffer();
54174
54314
  init_queen_tools();
54175
54315
  init_constants();
54316
+ QUEEN_EXECUTION_TOOLS = /* @__PURE__ */ new Set([
54317
+ "quoroom_web_search",
54318
+ "quoroom_web_fetch",
54319
+ "quoroom_browser"
54320
+ ]);
54321
+ QUEEN_POLICY_WIP_HINT = "[policy] Queen control-plane mode: delegate execution tasks to workers with quoroom_delegate_task, then monitor, unblock, and report outcomes. Avoid direct web/browser execution.";
54176
54322
  runningLoops = /* @__PURE__ */ new Map();
54323
+ launchedRoomIds = /* @__PURE__ */ new Set();
54177
54324
  RateLimitError = class extends Error {
54178
54325
  constructor(info) {
54179
54326
  super(`Rate limited: wait ${Math.round(info.waitMs / 1e3)}s`);
@@ -54189,7 +54336,7 @@ var require_package = __commonJS({
54189
54336
  "package.json"(exports2, module2) {
54190
54337
  module2.exports = {
54191
54338
  name: "quoroom",
54192
- version: "0.1.38",
54339
+ version: "0.1.39",
54193
54340
  description: "Open-source local AI agent framework \u2014 Queen, Workers, Quorum. Experimental research tool.",
54194
54341
  main: "./out/mcp/server.js",
54195
54342
  bin: {
@@ -55685,13 +55832,10 @@ async function executeClerkTool(db3, toolName, args2, ctx) {
55685
55832
  return { content: `Deleted room "${room.name}" (#${room.id}).` };
55686
55833
  }
55687
55834
  case "quoroom_start_queen": {
55688
- const room = resolveRoom(db3, args2);
55689
- if (!room) return { content: "Error: room not found.", isError: true };
55690
- if (!room.queenWorkerId) return { content: `Error: room "${room.name}" has no queen worker.`, isError: true };
55691
- if (room.status !== "active") return { content: `Error: room "${room.name}" is not active.`, isError: true };
55692
- stopRoomRuntime(db3, room.id, "Runtime reset before queen start");
55693
- triggerAgent(db3, room.id, room.queenWorkerId);
55694
- return { content: `Started queen in "${room.name}" (#${room.id}).` };
55835
+ return {
55836
+ content: "Error: direct queen start is disabled. Start the room manually from the Room controls.",
55837
+ isError: true
55838
+ };
55695
55839
  }
55696
55840
  case "quoroom_stop_queen": {
55697
55841
  const room = resolveRoom(db3, args2);
@@ -58260,6 +58404,9 @@ async function syncCloudRoomMessages(db3) {
58260
58404
  }
58261
58405
  function startServerRuntime(db3) {
58262
58406
  stopServerRuntime();
58407
+ clearRoomLaunchState();
58408
+ const cleanedCycles = cleanupStaleCycles(db3);
58409
+ if (cleanedCycles > 0) console.log(`Cleaned up ${cleanedCycles} stale worker cycles`);
58263
58410
  ensureClerkContactCheckTasks(db3);
58264
58411
  refreshCronJobs(db3);
58265
58412
  runDueOneTimeTasks(db3);
@@ -58287,30 +58434,8 @@ function startServerRuntime(db3) {
58287
58434
  clerkAlertTimer = setInterval(() => {
58288
58435
  queueClerkAlertRelay(db3);
58289
58436
  }, CLERK_ALERT_RELAY_MS);
58290
- resumeActiveQueens(db3);
58291
58437
  startCommentaryEngine(db3);
58292
58438
  }
58293
- function makeCycleCallbacks() {
58294
- return {
58295
- onCycleLogEntry: (entry) => {
58296
- eventBus.emit(`cycle:${entry.cycleId}`, "cycle:log", entry);
58297
- },
58298
- onCycleLifecycle: (event, cycleId, roomId) => {
58299
- eventBus.emit(`room:${roomId}`, `cycle:${event}`, { cycleId, roomId });
58300
- }
58301
- };
58302
- }
58303
- function resumeActiveQueens(db3) {
58304
- const cleaned = cleanupStaleCycles(db3);
58305
- if (cleaned > 0) console.log(`Cleaned up ${cleaned} stale worker cycles`);
58306
- const rooms = listRooms(db3, "active");
58307
- const callbacks = makeCycleCallbacks();
58308
- for (const room of rooms) {
58309
- if (!room.queenWorkerId) continue;
58310
- console.log(`Auto-resuming queen for room "${room.name}" (#${room.id})`);
58311
- triggerAgent(db3, room.id, room.queenWorkerId, callbacks);
58312
- }
58313
- }
58314
58439
  function stopServerRuntime() {
58315
58440
  stopCommentaryEngine();
58316
58441
  if (schedulerTimer) clearInterval(schedulerTimer);
@@ -58391,6 +58516,16 @@ function emitQueenState(roomId, running) {
58391
58516
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
58392
58517
  });
58393
58518
  }
58519
+ function makeCycleCallbacks(roomId) {
58520
+ return {
58521
+ onCycleLogEntry: (entry) => {
58522
+ eventBus.emit(`cycle:${entry.cycleId}`, "cycle:log", entry);
58523
+ },
58524
+ onCycleLifecycle: (event, cycleId, _roomId) => {
58525
+ eventBus.emit(`room:${roomId}`, `cycle:${event}`, { cycleId, roomId });
58526
+ }
58527
+ };
58528
+ }
58394
58529
  function getLocalReferredRooms(db3, roomId) {
58395
58530
  const keeperCode = (getSetting(db3, "keeper_referral_code") ?? "").trim();
58396
58531
  if (!keeperCode) return [];
@@ -58604,11 +58739,8 @@ function registerRoomRoutes(router) {
58604
58739
  }
58605
58740
  }
58606
58741
  if (updates.status === "stopped") {
58607
- const workers = listRoomWorkers(ctx.db, roomId);
58608
- for (const w of workers) {
58609
- updateAgentState(ctx.db, w.id, "idle");
58610
- pauseAgent(ctx.db, w.id);
58611
- }
58742
+ setRoomLaunchEnabled(roomId, false);
58743
+ stopRoomRuntime(ctx.db, roomId, "Room archived");
58612
58744
  logRoomActivity(ctx.db, roomId, "system", "Room archived");
58613
58745
  }
58614
58746
  const updated = getRoom(ctx.db, roomId);
@@ -58623,21 +58755,59 @@ function registerRoomRoutes(router) {
58623
58755
  const roomId = Number(ctx.params.id);
58624
58756
  try {
58625
58757
  pauseRoom(ctx.db, roomId);
58626
- const workers = listRoomWorkers(ctx.db, roomId);
58627
- for (const w of workers) {
58628
- pauseAgent(ctx.db, w.id);
58629
- }
58758
+ setRoomLaunchEnabled(roomId, false);
58759
+ stopRoomRuntime(ctx.db, roomId, "Room stopped by keeper");
58630
58760
  eventBus.emit(`room:${roomId}`, "room:paused", { roomId });
58761
+ eventBus.emit(`room:${roomId}`, "room:queen_stopped", { roomId, running: false });
58762
+ emitQueenState(roomId, false);
58631
58763
  emitRoomsUpdated("room_paused", { roomId });
58632
58764
  return { data: { ok: true } };
58633
58765
  } catch (e) {
58634
58766
  return { status: 404, error: e.message };
58635
58767
  }
58636
58768
  });
58769
+ router.post("/api/rooms/:id/start", (ctx) => {
58770
+ const roomId = Number(ctx.params.id);
58771
+ const room = getRoom(ctx.db, roomId);
58772
+ if (!room) return { status: 404, error: "Room not found" };
58773
+ if (room.status === "stopped") return { status: 400, error: "Room is stopped" };
58774
+ if (!room.queenWorkerId) return { status: 400, error: "No queen worker" };
58775
+ if (room.status !== "active") {
58776
+ updateRoom(ctx.db, roomId, { status: "active" });
58777
+ }
58778
+ setRoomLaunchEnabled(roomId, true);
58779
+ stopRoomRuntime(ctx.db, roomId, "Runtime reset before room start");
58780
+ triggerAgent(ctx.db, roomId, room.queenWorkerId, {
58781
+ ...makeCycleCallbacks(roomId),
58782
+ allowColdStart: true
58783
+ });
58784
+ eventBus.emit(`room:${roomId}`, "room:started", { roomId });
58785
+ eventBus.emit(`room:${roomId}`, "room:queen_started", { roomId, running: true });
58786
+ emitQueenState(roomId, true);
58787
+ emitRoomsUpdated("room_started", { roomId });
58788
+ return { data: { ok: true, running: true } };
58789
+ });
58790
+ router.post("/api/rooms/:id/stop", (ctx) => {
58791
+ const roomId = Number(ctx.params.id);
58792
+ const room = getRoom(ctx.db, roomId);
58793
+ if (!room) return { status: 404, error: "Room not found" };
58794
+ setRoomLaunchEnabled(roomId, false);
58795
+ stopRoomRuntime(ctx.db, roomId, "Room stopped by keeper");
58796
+ if (room.status !== "stopped") {
58797
+ updateRoom(ctx.db, roomId, { status: "paused" });
58798
+ }
58799
+ eventBus.emit(`room:${roomId}`, "room:stopped", { roomId });
58800
+ eventBus.emit(`room:${roomId}`, "room:paused", { roomId });
58801
+ eventBus.emit(`room:${roomId}`, "room:queen_stopped", { roomId, running: false });
58802
+ emitQueenState(roomId, false);
58803
+ emitRoomsUpdated("room_paused", { roomId });
58804
+ return { data: { ok: true, running: false } };
58805
+ });
58637
58806
  router.post("/api/rooms/:id/restart", (ctx) => {
58638
58807
  const roomId = Number(ctx.params.id);
58639
58808
  const { goal } = ctx.body || {};
58640
58809
  try {
58810
+ setRoomLaunchEnabled(roomId, false);
58641
58811
  restartRoom(ctx.db, roomId, goal);
58642
58812
  eventBus.emit(`room:${roomId}`, "room:restarted", { roomId });
58643
58813
  emitRoomsUpdated("room_restarted", { roomId });
@@ -58665,30 +58835,11 @@ function registerRoomRoutes(router) {
58665
58835
  }
58666
58836
  };
58667
58837
  });
58668
- router.post("/api/rooms/:id/queen/start", (ctx) => {
58669
- const roomId = Number(ctx.params.id);
58670
- const room = getRoom(ctx.db, roomId);
58671
- if (!room) return { status: 404, error: "Room not found" };
58672
- if (room.status !== "active") return { status: 400, error: "Room is not active" };
58673
- if (!room.queenWorkerId) return { status: 400, error: "No queen worker" };
58674
- stopRoomRuntime(ctx.db, roomId, "Runtime reset before queen start");
58675
- triggerAgent(ctx.db, roomId, room.queenWorkerId, {
58676
- onCycleLogEntry: (entry) => eventBus.emit(`cycle:${entry.cycleId}`, "cycle:log", entry),
58677
- onCycleLifecycle: (event, cycleId) => eventBus.emit(`room:${roomId}`, `cycle:${event}`, { cycleId, roomId })
58678
- });
58679
- eventBus.emit(`room:${roomId}`, "room:queen_started", { roomId, running: true });
58680
- emitQueenState(roomId, true);
58681
- return { data: { ok: true, running: true } };
58838
+ router.post("/api/rooms/:id/queen/start", (_ctx) => {
58839
+ return { status: 410, error: "Deprecated. Use POST /api/rooms/:id/start" };
58682
58840
  });
58683
- router.post("/api/rooms/:id/queen/stop", (ctx) => {
58684
- const roomId = Number(ctx.params.id);
58685
- const room = getRoom(ctx.db, roomId);
58686
- if (!room) return { status: 404, error: "Room not found" };
58687
- if (!room.queenWorkerId) return { status: 400, error: "No queen worker" };
58688
- stopRoomRuntime(ctx.db, roomId, "Queen stopped by keeper");
58689
- eventBus.emit(`room:${roomId}`, "room:queen_stopped", { roomId, running: false });
58690
- emitQueenState(roomId, false);
58691
- return { data: { ok: true, running: false } };
58841
+ router.post("/api/rooms/:id/queen/stop", (_ctx) => {
58842
+ return { status: 410, error: "Deprecated. Use POST /api/rooms/:id/stop" };
58692
58843
  });
58693
58844
  router.get("/api/rooms/:id/cycles", (ctx) => {
58694
58845
  const roomId = Number(ctx.params.id);
@@ -58702,8 +58853,8 @@ function registerRoomRoutes(router) {
58702
58853
  const today = getRoomTokenUsageToday(ctx.db, roomId);
58703
58854
  const room = getRoom(ctx.db, roomId);
58704
58855
  const queenWorker = room?.queenWorkerId ? getWorker(ctx.db, room.queenWorkerId) : null;
58705
- const model = queenWorker?.model ?? room?.workerModel ?? "claude";
58706
- const isApiModel = model.startsWith("openai") || model.startsWith("anthropic") || model.startsWith("claude-api");
58856
+ const model = queenWorker?.model ?? room?.workerModel ?? null;
58857
+ const isApiModel = !!model && (model.startsWith("openai") || model.startsWith("anthropic") || model.startsWith("claude-api"));
58707
58858
  return { data: { total, today, isApiModel } };
58708
58859
  });
58709
58860
  router.get("/api/cycles/:id/logs", (ctx) => {
@@ -58716,10 +58867,8 @@ function registerRoomRoutes(router) {
58716
58867
  router.delete("/api/rooms/:id", (ctx) => {
58717
58868
  const roomId = Number(ctx.params.id);
58718
58869
  try {
58719
- const workers = listRoomWorkers(ctx.db, roomId);
58720
- for (const w of workers) {
58721
- pauseAgent(ctx.db, w.id);
58722
- }
58870
+ setRoomLaunchEnabled(roomId, false);
58871
+ stopRoomRuntime(ctx.db, roomId, "Room deleted");
58723
58872
  deleteRoom2(ctx.db, roomId);
58724
58873
  eventBus.emit(`room:${roomId}`, "room:deleted", { roomId });
58725
58874
  emitRoomsUpdated("room_deleted", { roomId });
@@ -58799,6 +58948,9 @@ function registerWorkerRoutes(router) {
58799
58948
  const room = getRoom(ctx.db, worker.roomId);
58800
58949
  if (!room) return { status: 404, error: "Room not found" };
58801
58950
  if (room.status !== "active") return { status: 400, error: "Room is not active" };
58951
+ if (!isRoomLaunchEnabled(worker.roomId)) {
58952
+ return { status: 409, error: "Room runtime is not started. Start the room first." };
58953
+ }
58802
58954
  triggerAgent(ctx.db, worker.roomId, id, {
58803
58955
  onCycleLogEntry: (entry) => eventBus.emit(`cycle:${entry.cycleId}`, "cycle:log", entry),
58804
58956
  onCycleLifecycle: (event, cycleId) => eventBus.emit(`room:${worker.roomId}`, `cycle:${event}`, { cycleId, roomId: worker.roomId })
@@ -59759,7 +59911,7 @@ function semverGt(a, b) {
59759
59911
  }
59760
59912
  function getCurrentVersion() {
59761
59913
  try {
59762
- return true ? "0.1.38" : null.version;
59914
+ return true ? "0.1.39" : null.version;
59763
59915
  } catch {
59764
59916
  return "0.0.0";
59765
59917
  }
@@ -59966,7 +60118,7 @@ var init_updateChecker = __esm({
59966
60118
  function getVersion3() {
59967
60119
  if (cachedVersion) return cachedVersion;
59968
60120
  try {
59969
- cachedVersion = true ? "0.1.38" : null.version;
60121
+ cachedVersion = true ? "0.1.39" : null.version;
59970
60122
  } catch {
59971
60123
  cachedVersion = "unknown";
59972
60124
  }
@@ -66232,7 +66384,7 @@ __export(update_exports, {
66232
66384
  });
66233
66385
  function getCurrentVersion2() {
66234
66386
  try {
66235
- return true ? "0.1.38" : null.version;
66387
+ return true ? "0.1.39" : null.version;
66236
66388
  } catch {
66237
66389
  return "0.0.0";
66238
66390
  }
package/out/mcp/server.js CHANGED
@@ -46264,13 +46264,14 @@ Every cycle:
46264
46264
  1. Check if workers reported results (messages, completed goals)
46265
46265
  2. If work is done \u2192 send results to keeper, take next step
46266
46266
  3. If work is stuck \u2192 help unblock (new instructions, different approach)
46267
- 4. If new work needed \u2192 delegate to a worker with clear instructions
46268
- 5. If a decision needs input \u2192 announce it (workers can object within 10 min)
46267
+ 4. If no workers exist yet \u2192 create an executor worker first
46268
+ 5. If new work is needed \u2192 delegate to a worker with clear instructions, then poke/follow up
46269
+ 6. If a decision needs input \u2192 announce it and process objections/votes (announce/object flow)
46269
46270
 
46270
46271
  Talk to the keeper regularly \u2014 they are your client.
46271
46272
 
46272
- Do NOT do execution work yourself (research, form filling, account creation).
46273
- Delegate it. That's what workers are for.`;
46273
+ Do NOT execute tasks directly (research, form filling, account creation, browser automation).
46274
+ Stay control-plane only: create workers, delegate, monitor, unblock, report.`;
46274
46275
  function createRoom2(db2, input) {
46275
46276
  const config2 = { ...DEFAULT_ROOM_CONFIG, ...input.config };
46276
46277
  const room = createRoom(db2, input.name, input.goal, config2, input.referredByCode);
@@ -49085,7 +49086,7 @@ init_db();
49085
49086
  async function main() {
49086
49087
  const server = new McpServer({
49087
49088
  name: "quoroom",
49088
- version: true ? "0.1.38" : "0.0.0"
49089
+ version: true ? "0.1.39" : "0.0.0"
49089
49090
  });
49090
49091
  registerMemoryTools(server);
49091
49092
  registerSchedulerTools(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quoroom",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "Open-source local AI agent framework — Queen, Workers, Quorum. Experimental research tool.",
5
5
  "main": "./out/mcp/server.js",
6
6
  "bin": {