quoroom 0.1.30 → 0.1.31

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.30",
9917
+ version: "0.1.31",
9918
9918
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
9919
9919
  main: "./out/mcp/server.js",
9920
9920
  bin: {
@@ -9962,13 +9962,14 @@ var require_package = __commonJS({
9962
9962
  "test:watch": "npm run rebuild:native:node && vitest --pool=forks",
9963
9963
  "test:quick": "npm run typecheck && npm run test",
9964
9964
  "test:smart-e2e": "node scripts/smart-e2e.js",
9965
- "test:e2e": "npm run build && npx playwright test",
9966
- "test:e2e:fast": "npm run build:fast && npx playwright test --retries=0",
9965
+ "test:e2e": "npm run build && npx playwright test --project=chromium",
9966
+ "test:e2e:fast": "npm run build:fast && npx playwright test --project=chromium --retries=0",
9967
9967
  "test:e2e:smoke": "npm run build:fast && npx playwright test --retries=0 e2e/api.test.ts e2e/crud-decisions.test.ts e2e/crud-goals.test.ts e2e/crud-rooms.test.ts e2e/crud-tasks.test.ts e2e/crud-workers.test.ts e2e/security.test.ts e2e/websocket.test.ts e2e/referral.test.ts",
9968
9968
  "test:e2e:ui": "npm run build && npx playwright test e2e/ui.test.ts",
9969
9969
  "test:e2e:setup": "npm run build && npx playwright test e2e/setup-flow.test.ts",
9970
9970
  "test:e2e:setup:headed": "npm run build && npx playwright test e2e/setup-flow.test.ts --headed --project=chromium",
9971
9971
  "test:e2e:providers": "npm run build && npx playwright test e2e/provider-flows.test.ts",
9972
+ "test:e2e:demo": "npm run build && npx playwright test --project=demo",
9972
9973
  "rebuild:native:node": `node -e "try{require('better-sqlite3')(':memory:').close()}catch{process.exit(1)}" || npx --yes node-gyp rebuild --directory=node_modules/better-sqlite3`,
9973
9974
  prepublishOnly: "npm run build:mcp",
9974
9975
  "social:rotate": "node scripts/rotate-social-image.js",
@@ -11298,46 +11299,59 @@ var CHATGPT_DEFAULTS_BY_PLAN = {
11298
11299
  var WORKER_ROLE_PRESETS = {
11299
11300
  guardian: {
11300
11301
  cycleGapMs: 3e4,
11301
- maxTurns: 15,
11302
+ maxTurns: 30,
11302
11303
  systemPromptPrefix: "Check quoroom_list_skills and quoroom_recall first. Monitor and observe. Do not spawn workers or make purchases. Focus on detecting anomalies in tasks, stations, and worker activity."
11303
11304
  },
11304
11305
  analyst: {
11305
- cycleGapMs: 12e4,
11306
- maxTurns: 30,
11307
- systemPromptPrefix: "Check quoroom_list_skills and quoroom_recall first \u2014 don't repeat research the team has already done. Perform deep analysis. Work to completion on a task, then pause. Prefer depth over frequency."
11306
+ cycleGapMs: 6e4,
11307
+ maxTurns: 100,
11308
+ systemPromptPrefix: `Check quoroom_list_skills and quoroom_recall first \u2014 don't repeat research the team has already done.
11309
+
11310
+ Perform deep analysis. Work to COMPLETION \u2014 you have plenty of turns, so finish what you start.
11311
+ Before your cycle ends, save what you accomplished with quoroom_save_wip so the next cycle continues forward.`
11308
11312
  },
11309
11313
  writer: {
11310
- cycleGapMs: 12e4,
11311
- maxTurns: 30,
11312
- systemPromptPrefix: "Check quoroom_list_skills for existing drafts or style guides. Produce high-quality written output. Minimize interruptions between drafting sessions."
11314
+ cycleGapMs: 6e4,
11315
+ maxTurns: 100,
11316
+ systemPromptPrefix: `Check quoroom_list_skills for existing drafts or style guides.
11317
+
11318
+ Produce high-quality written output. Work to COMPLETION \u2014 you have plenty of turns, so finish the full piece.
11319
+ Before your cycle ends, save what you accomplished with quoroom_save_wip so the next cycle continues forward.`
11313
11320
  },
11314
11321
  executor: {
11315
- cycleGapMs: 3e4,
11316
- maxTurns: 40,
11317
- systemPromptPrefix: `Check quoroom_list_skills and quoroom_recall FIRST \u2014 another agent may have already documented what you need.
11322
+ cycleGapMs: 15e3,
11323
+ maxTurns: 200,
11324
+ systemPromptPrefix: `You are an execution agent. Your ONLY job is to DO things \u2014 not plan, not vote, not coordinate.
11325
+
11326
+ If you have a WIP from last cycle (shown at top of context), continue from where it says. Take the NEXT action.
11327
+ If you don't have a WIP, look at your assigned tasks and start executing immediately.
11328
+
11329
+ Run your full action chain to completion. You have plenty of turns \u2014 don't rush.
11330
+ When done, save what you accomplished with quoroom_save_wip so the next cycle continues forward.
11318
11331
 
11319
- You are an execution agent. ACTUALLY DO things using quoroom_browser \u2014 don't just search and plan.
11332
+ Check quoroom_list_skills and quoroom_recall FIRST \u2014 another agent may have already documented what you need.
11320
11333
 
11321
11334
  Browser rules:
11322
11335
  - First call: omit sessionId. Note the returned sessionId and pass it on every follow-up call.
11323
11336
  - Use fill for standard inputs, type for JS-heavy SPAs. For checkboxes: click text first; if intercepted by label, use CSS selector (e.g. input[type=checkbox]).
11324
11337
  - Take snapshot after every action sequence to see page state.
11325
11338
  - Store ALL results immediately with quoroom_remember("YourName: [what]", ...).
11326
-
11327
- The mandatory execution report (quoroom_create_skill) is required every cycle \u2014 see instructions below.`
11339
+ - Create a skill when you COMPLETE a significant action (working recipe for future use).`
11328
11340
  },
11329
11341
  researcher: {
11330
- cycleGapMs: 6e4,
11331
- maxTurns: 30,
11342
+ cycleGapMs: 3e4,
11343
+ maxTurns: 100,
11332
11344
  systemPromptPrefix: `Check quoroom_list_skills and quoroom_recall FIRST \u2014 don't duplicate work the team has already done.
11333
11345
 
11334
11346
  You are a research specialist. Be data-driven: real numbers, URLs, competitor names, pricing data.
11347
+ Work to COMPLETION \u2014 you have plenty of turns, so finish your research before the cycle ends.
11335
11348
 
11336
11349
  Rules:
11337
11350
  - Name memories clearly: "YourName: [topic]" so teammates can find them.
11338
11351
  - Build on existing research \u2014 quoroom_recall before starting any topic.
11339
11352
  - Message key findings to the keeper: quoroom_send_message(to="keeper").
11340
- - Create a skill when you find a reliable source or repeatable research technique.`
11353
+ - Create a skill when you find a reliable source or repeatable research technique.
11354
+ - Before your cycle ends, save what you accomplished with quoroom_save_wip.`
11341
11355
  }
11342
11356
  };
11343
11357
  var DEFAULT_ROOM_CONFIG = {
@@ -11665,10 +11679,14 @@ function mapWorkerRow(row) {
11665
11679
  agentState: row.agent_state ?? "idle",
11666
11680
  votesCast: row.votes_cast ?? 0,
11667
11681
  votesMissed: row.votes_missed ?? 0,
11682
+ wip: row.wip ?? null,
11668
11683
  createdAt: row.created_at,
11669
11684
  updatedAt: row.updated_at
11670
11685
  };
11671
11686
  }
11687
+ function updateWorkerWip(db2, workerId, wip) {
11688
+ db2.prepare("UPDATE workers SET wip = ?, updated_at = datetime('now','localtime') WHERE id = ?").run(wip, workerId);
11689
+ }
11672
11690
  function createTask(db2, input) {
11673
11691
  const result = db2.prepare(
11674
11692
  `INSERT INTO tasks (name, description, prompt, cron_expression, trigger_type, trigger_config, webhook_token, scheduled_at, executor, max_runs, worker_id, session_continuity, timeout_minutes, max_turns, allowed_tools, disallowed_tools, room_id)
@@ -13221,7 +13239,8 @@ function countProductiveToolCalls(db2, workerId, lastNCycles = 2) {
13221
13239
  AND (content LIKE '%web_search%' OR content LIKE '%web_fetch%' OR content LIKE '%remember%'
13222
13240
  OR content LIKE '%send_message%' OR content LIKE '%inbox_send%'
13223
13241
  OR content LIKE '%update_progress%' OR content LIKE '%complete_goal%'
13224
- OR content LIKE '%set_goal%' OR content LIKE '%delegate_task%' OR content LIKE '%propose%' OR content LIKE '%vote%')
13242
+ OR content LIKE '%set_goal%' OR content LIKE '%delegate_task%' OR content LIKE '%propose%' OR content LIKE '%vote%'
13243
+ OR content LIKE '%browser%' OR content LIKE '%save_wip%')
13225
13244
  `).get(workerId, lastNCycles);
13226
13245
  return row.cnt;
13227
13246
  }
@@ -23600,7 +23619,7 @@ async function executeOpenAiWithTools(options) {
23600
23619
  content: isResume ? `NEW CYCLE. Updated room state:
23601
23620
  ${options.prompt}
23602
23621
 
23603
- Continue working toward the goal.` : options.prompt
23622
+ Take the next action. Do not repeat what was already accomplished (see WIP/context above). Execute to completion.` : options.prompt
23604
23623
  }
23605
23624
  ];
23606
23625
  let finalOutput = "";
@@ -23689,7 +23708,7 @@ async function executeAnthropicWithTools(options) {
23689
23708
  content: isResume ? `NEW CYCLE. Updated room state:
23690
23709
  ${options.prompt}
23691
23710
 
23692
- Continue working toward the goal.` : options.prompt
23711
+ Take the next action. Do not repeat what was already accomplished (see WIP/context above). Execute to completion.` : options.prompt
23693
23712
  }
23694
23713
  ];
23695
23714
  let finalOutput = "";
@@ -24600,7 +24619,7 @@ async function closeBrowser() {
24600
24619
  }
24601
24620
  }
24602
24621
  var _sessions = /* @__PURE__ */ new Map();
24603
- var SESSION_IDLE_TIMEOUT = 10 * 60 * 1e3;
24622
+ var SESSION_IDLE_TIMEOUT = 30 * 60 * 1e3;
24604
24623
  var _cleanupInterval = null;
24605
24624
  function startSessionCleanup() {
24606
24625
  if (_cleanupInterval) return;
@@ -25277,6 +25296,24 @@ var QUEEN_TOOL_DEFINITIONS = [
25277
25296
  }
25278
25297
  }
25279
25298
  }
25299
+ },
25300
+ // ── WIP (Work-In-Progress) ──────────────────────────────────
25301
+ {
25302
+ type: "function",
25303
+ function: {
25304
+ name: "quoroom_save_wip",
25305
+ description: `Save what you accomplished this cycle and what should happen next. This is injected at the TOP of your next cycle's context so you (or a teammate) can continue forward without repeating work. Call this before your cycle ends. Pass "done" or empty string to clear WIP.`,
25306
+ parameters: {
25307
+ type: "object",
25308
+ properties: {
25309
+ status: {
25310
+ type: "string",
25311
+ description: 'What you accomplished and what to do next. Example: "Registered tuta account (user: agent42@tuta.com, pwd in memory). Next: set up email forwarding and notify keeper."'
25312
+ }
25313
+ },
25314
+ required: ["status"]
25315
+ }
25316
+ }
25280
25317
  }
25281
25318
  ];
25282
25319
  async function executeQueenTool(db2, roomId, workerId, toolName, args) {
@@ -25577,6 +25614,13 @@ async function executeQueenTool(db2, roomId, workerId, toolName, args) {
25577
25614
  const lines = txs.map((tx) => `[${tx.type}] ${tx.amount} USDC \u2014 ${tx.description ?? ""} (${tx.status})`).join("\n");
25578
25615
  return { content: lines };
25579
25616
  }
25617
+ // ── WIP (Work-In-Progress) ────────────────────────────────
25618
+ case "quoroom_save_wip": {
25619
+ const status2 = String(args.status ?? "").trim();
25620
+ const isDone = !status2 || status2.toLowerCase() === "done" || status2.toLowerCase() === "complete" || status2.toLowerCase() === "completed";
25621
+ updateWorkerWip(db2, workerId, isDone ? null : status2.slice(0, 2e3));
25622
+ return { content: isDone ? "WIP cleared." : "WIP saved. Next cycle will see it at the top of context." };
25623
+ }
25580
25624
  default:
25581
25625
  return { content: `Unknown tool: ${toolName}`, isError: true };
25582
25626
  }
@@ -25721,7 +25765,8 @@ async function startAgentLoop(db2, roomId, workerId, options) {
25721
25765
  continue;
25722
25766
  }
25723
25767
  try {
25724
- await runCycle(db2, roomId, currentWorker, currentWorker.maxTurns ?? currentRoom.queenMaxTurns, options);
25768
+ const effectiveMaxTurns = Math.max(currentWorker.maxTurns ?? currentRoom.queenMaxTurns, 50);
25769
+ await runCycle(db2, roomId, currentWorker, effectiveMaxTurns, options);
25725
25770
  } catch (err) {
25726
25771
  if (!loop.running) break;
25727
25772
  if (err instanceof RateLimitError) {
@@ -25760,7 +25805,10 @@ async function startAgentLoop(db2, roomId, workerId, options) {
25760
25805
  updateAgentState(db2, workerId, "idle");
25761
25806
  }
25762
25807
  if (!loop.running) break;
25763
- const gap = currentWorker.cycleGapMs ?? currentRoom.queenCycleGapMs;
25808
+ const MOMENTUM_GAP = 1e4;
25809
+ const baseGap = currentWorker.cycleGapMs ?? currentRoom.queenCycleGapMs;
25810
+ const freshWorker = getWorker(db2, workerId);
25811
+ const gap = freshWorker?.wip ? Math.min(baseGap, MOMENTUM_GAP) : baseGap;
25764
25812
  try {
25765
25813
  const abort = new AbortController();
25766
25814
  loop.abort = abort;
@@ -25972,21 +26020,23 @@ ${skillContent}` : ""
25972
26020
  logBuffer2.flush();
25973
26021
  }
25974
26022
  const contextParts = [];
26023
+ const isExecutor = worker.role === "executor";
25975
26024
  contextParts.push(
25976
26025
  `## Your Identity
25977
26026
  - Room ID: ${roomId}
25978
26027
  - Your Worker ID: ${worker.id}
25979
26028
  - Your Name: ${worker.name}`
25980
26029
  );
25981
- const keeperReferralCode = getSetting(db2, "keeper_referral_code")?.trim();
25982
- if (keeperReferralCode) {
25983
- const encodedKeeperCode = encodeURIComponent(keeperReferralCode);
25984
- contextParts.push(
25985
- `## Keeper Referral
25986
- - Keeper code: ${keeperReferralCode}
25987
- - Invite link: https://quoroom.ai/invite/${encodedKeeperCode}
25988
- - Share link: https://quoroom.ai/share/v2/${encodedKeeperCode}`
25989
- );
26030
+ const wip = worker.wip;
26031
+ if (wip) {
26032
+ contextParts.push(`## >>> CONTINUE FORWARD <<<
26033
+ Last cycle you accomplished / were working on:
26034
+
26035
+ ${wip}
26036
+
26037
+ NOW take the NEXT action. Do NOT repeat what's already done \u2014 build on it.
26038
+ If the above action is complete, start a new one toward the room objective.
26039
+ At the end of this cycle, call quoroom_save_wip to save your updated position.`);
25990
26040
  }
25991
26041
  if (status2.room.goal) {
25992
26042
  contextParts.push(`## Room Objective
@@ -26021,128 +26071,32 @@ These tasks were delegated to you. Prioritize completing them and report progres
26021
26071
  ${memLines.join("\n")}`);
26022
26072
  }
26023
26073
  }
26024
- const votingDecisions = listDecisions(db2, roomId, "voting");
26025
- if (votingDecisions.length > 0) {
26026
- const decisionLines = votingDecisions.map((d) => {
26027
- const votes = getVotes(db2, d.id);
26028
- const alreadyVoted = votes.some((v) => v.workerId === worker.id);
26029
- const proposerW = d.proposerId ? roomWorkers.find((w) => w.id === d.proposerId) : null;
26030
- const by = proposerW ? ` (by ${proposerW.name})` : "";
26031
- const voteStatus = alreadyVoted ? " \u2713 you voted" : " \u2190 VOTE NEEDED";
26032
- return `- #${d.id}: ${d.proposal}${by} [${votes.length} votes so far, need ${d.minVoters}+]${voteStatus}`;
26033
- });
26034
- contextParts.push(`## Pending Proposals \u2014 Use quoroom_vote to cast your vote
26035
- ${decisionLines.join("\n")}`);
26036
- }
26037
- const recentResolved = listRecentDecisions(db2, roomId, 5);
26038
- if (recentResolved.length > 0) {
26039
- contextParts.push(`## Recent Decisions (already done \u2014 do NOT repeat these)
26040
- ${recentResolved.map((d) => {
26041
- const icon = d.status === "approved" ? "\u2713" : "\u2717";
26042
- return `- ${icon} ${d.status}: "${d.proposal.slice(0, 120)}"`;
26043
- }).join("\n")}`);
26044
- }
26045
- const myKeeperMessages = pendingEscalations.filter((e) => e.fromAgentId === worker.id && !e.toAgentId);
26046
- const incomingWorkerMessages = pendingEscalations.filter((e) => e.toAgentId === worker.id && e.fromAgentId !== worker.id);
26047
- if (myKeeperMessages.length > 0) {
26048
- contextParts.push(`## Pending Messages to Keeper (awaiting reply)
26049
- ${myKeeperMessages.map(
26050
- (e) => `- #${e.id}: ${e.question}`
26051
- ).join("\n")}`);
26052
- }
26053
- if (incomingWorkerMessages.length > 0) {
26054
- const senderNames = new Map(roomWorkers.map((w) => [w.id, w.name]));
26055
- contextParts.push(`## Messages from Other Workers
26056
- ${incomingWorkerMessages.map((e) => {
26057
- const sender = senderNames.get(e.fromAgentId ?? 0) ?? `Worker #${e.fromAgentId}`;
26058
- return `- #${e.id} from ${sender}: ${e.question}`;
26059
- }).join("\n")}`);
26060
- }
26061
- if (recentKeeperAnswers.length > 0) {
26062
- contextParts.push(`## Keeper Answers (recent)
26063
- ${recentKeeperAnswers.map(
26064
- (e) => `- Q: ${e.question}
26065
- A: ${e.answer}`
26066
- ).join("\n")}`);
26067
- }
26068
- const activitySlice = recentActivity.slice(0, 15);
26069
- if (activitySlice.length > 0) {
26070
- contextParts.push(`## Recent Activity
26071
- ${activitySlice.map(
26072
- (a) => `- [${a.eventType}] ${a.summary}`
26073
- ).join("\n")}`);
26074
- }
26075
- if (roomWorkers.length > 0) {
26076
- contextParts.push(`## Room Workers
26077
- ${roomWorkers.map(
26078
- (w) => `- #${w.id} ${w.name}${w.role ? ` (${w.role})` : ""} \u2014 ${w.agentState}`
26079
- ).join("\n")}`);
26080
- }
26081
- if (roomTasks.length > 0) {
26082
- contextParts.push(`## Room Tasks
26083
- ${roomTasks.map(
26084
- (t) => `- #${t.id} "${t.name}" [${t.triggerType}] \u2014 ${t.status}`
26085
- ).join("\n")}`);
26086
- }
26087
- const wallet = getWalletByRoom(db2, roomId);
26088
- if (wallet) {
26089
- const summary = getWalletTransactionSummary(db2, wallet.id);
26090
- const net = (parseFloat(summary.received) - parseFloat(summary.sent)).toFixed(2);
26091
- contextParts.push(`## Wallet
26092
- Address: ${wallet.address}
26093
- Balance: ${net} USDC (received: ${summary.received}, spent: ${summary.sent})`);
26094
- }
26095
- if (unreadMessages.length > 0) {
26096
- contextParts.push(`## Unread Messages
26097
- ${unreadMessages.map(
26098
- (m) => `- #${m.id} from ${m.fromRoomId ?? "unknown"}: ${m.subject}`
26099
- ).join("\n")}`);
26100
- }
26101
- const activeStations = cloudStations.filter((s) => s.status === "active");
26102
- if (cloudStations.length > 0) {
26103
- const stationLines = cloudStations.map(
26104
- (s) => `- #${s.id} "${s.stationName}" (${s.tier}) \u2014 ${s.status} \u2014 $${s.monthlyCost}/mo`
26105
- );
26106
- contextParts.push(`## Stations (${activeStations.length} active)
26107
- ${stationLines.join("\n")}`);
26108
- }
26109
- if (publicRooms.length > 0) {
26110
- const top3 = publicRooms.slice(0, 3);
26111
- contextParts.push(
26112
- `## Public Rooms (cross-room learning)
26113
- Other rooms you can learn strategies from:
26114
- ${top3.map(
26115
- (r, i) => `${i + 1}. "${r.name}" \u2014 ${r.earnings} USDC | Goal: ${r.goal ?? "No goal set"}`
26116
- ).join("\n")}`
26117
- );
26118
- }
26119
- const rateLimitEvents = recentActivity.filter(
26120
- (a) => a.eventType === "system" && a.summary.includes("rate limited")
26121
- );
26122
- const settingsParts = [
26123
- `- Cycle gap: ${Math.round(status2.room.queenCycleGapMs / 1e3)}s`,
26124
- `- Max turns per cycle: ${status2.room.queenMaxTurns}`,
26125
- `- Max concurrent tasks: ${status2.room.maxConcurrentTasks}`
26126
- ];
26127
- if (rateLimitEvents.length > 0) {
26128
- settingsParts.push(`- **Rate limits hit recently: ${rateLimitEvents.length}** (in last ${recentActivity.length} events)`);
26129
- }
26130
- contextParts.push(`## Execution Settings
26131
- ${settingsParts.join("\n")}`);
26132
26074
  const STUCK_THRESHOLD_CYCLES = 2;
26133
26075
  const productiveCallCount = countProductiveToolCalls(db2, worker.id, STUCK_THRESHOLD_CYCLES);
26134
26076
  const recentCompletedCycles = listRoomCycles(db2, roomId, 5).filter((c) => c.workerId === worker.id && c.status === "completed");
26135
26077
  const isStuck = recentCompletedCycles.length >= STUCK_THRESHOLD_CYCLES && productiveCallCount === 0;
26136
26078
  if (isStuck) {
26137
- contextParts.push(`## \u26A0 STUCK DETECTED
26138
- 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:
26139
- - Try a different web search query
26140
- - Store what you know in memory even if incomplete
26141
- - Update goal progress with what you've learned
26142
- - Message the keeper if you're blocked
26143
- Do NOT repeat the same approach. Pivot immediately.`);
26144
- logBuffer2.addSynthetic("system", `Stuck detector: 0 productive tool calls in last ${STUCK_THRESHOLD_CYCLES} cycles \u2014 injecting pivot directive`);
26079
+ if (wip) {
26080
+ contextParts.push(`## \u26A0 ACTION STALLED
26081
+ Your last ${STUCK_THRESHOLD_CYCLES} cycles had a WIP but produced no external results. Your current approach may be blocked.
26082
+ - Try a different method to accomplish the same goal
26083
+ - Save what you learned (quoroom_remember) and report the blocker (quoroom_send_message to keeper)
26084
+ - Clear your WIP (quoroom_save_wip with a new plan) and try an alternate approach
26085
+ Do NOT keep retrying the same failing steps.`);
26086
+ } else {
26087
+ contextParts.push(`## \u26A0 STUCK \u2014 TAKE ACTION NOW
26088
+ Your last ${STUCK_THRESHOLD_CYCLES} cycles produced no external results (no web searches, no memories stored, no goal progress, no keeper messages). You are circling.
26089
+ STOP planning and ideating. Pick ONE concrete action and execute it NOW:
26090
+ - Use quoroom_browser to register an account, buy a domain, or fill a form
26091
+ - Use web search to find a specific lead or service
26092
+ - Use quoroom_send_message to report what is blocking you
26093
+ Then save your progress: quoroom_save_wip("Doing X, reached step Y")`);
26094
+ }
26095
+ logBuffer2.addSynthetic("system", `Stuck detector: 0 productive tool calls in last ${STUCK_THRESHOLD_CYCLES} cycles \u2014 injecting ${wip ? "stalled" : "stuck"} directive`);
26145
26096
  }
26097
+ const rateLimitEvents = recentActivity.filter(
26098
+ (a) => a.eventType === "system" && a.summary.includes("rate limited")
26099
+ );
26146
26100
  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." : "";
26147
26101
  const isClaude = model === "claude" || model.startsWith("claude-");
26148
26102
  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.";
@@ -26152,16 +26106,20 @@ Do NOT repeat the same approach. Pivot immediately.`);
26152
26106
  const toolLines = [];
26153
26107
  const goalTools = ["quoroom_set_goal", "quoroom_update_progress", "quoroom_create_subgoal", "quoroom_delegate_task", "quoroom_complete_goal", "quoroom_abandon_goal"].filter(has);
26154
26108
  if (goalTools.length) toolLines.push(`**Goals:** ${goalTools.join(", ")}`);
26155
- const govTools = ["quoroom_propose", "quoroom_vote"].filter(has);
26156
- if (govTools.length) toolLines.push(`**Governance:** ${govTools.join(", ")}`);
26157
- const workerTools = ["quoroom_create_worker", "quoroom_update_worker"].filter(has);
26158
- if (workerTools.length) toolLines.push(`**Workers:** ${workerTools.join(", ")}`);
26159
- if (has("quoroom_schedule")) toolLines.push("**Tasks:** quoroom_schedule");
26109
+ if (!isExecutor) {
26110
+ const govTools = ["quoroom_propose", "quoroom_vote"].filter(has);
26111
+ if (govTools.length) toolLines.push(`**Governance:** ${govTools.join(", ")}`);
26112
+ const workerTools = ["quoroom_create_worker", "quoroom_update_worker"].filter(has);
26113
+ if (workerTools.length) toolLines.push(`**Workers:** ${workerTools.join(", ")}`);
26114
+ if (has("quoroom_schedule")) toolLines.push("**Tasks:** quoroom_schedule");
26115
+ }
26160
26116
  const memTools = ["quoroom_remember", "quoroom_recall"].filter(has);
26161
26117
  if (memTools.length) toolLines.push(`**Memory:** ${memTools.join(", ")}`);
26162
- const walletToolNames = isCli ? ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history", "quoroom_wallet_topup"] : ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history"];
26163
- const filteredWallet = walletToolNames.filter(has);
26164
- if (filteredWallet.length) toolLines.push(`**Wallet:** ${filteredWallet.join(", ")}`);
26118
+ if (!isExecutor) {
26119
+ const walletToolNames = isCli ? ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history", "quoroom_wallet_topup"] : ["quoroom_wallet_balance", "quoroom_wallet_send", "quoroom_wallet_history"];
26120
+ const filteredWallet = walletToolNames.filter(has);
26121
+ if (filteredWallet.length) toolLines.push(`**Wallet:** ${filteredWallet.join(", ")}`);
26122
+ }
26165
26123
  const webToolNames = isCli ? null : ["quoroom_web_search", "quoroom_web_fetch", "quoroom_browser"];
26166
26124
  if (isCli) {
26167
26125
  if (has("quoroom_web_search") || has("quoroom_web_fetch")) toolLines.push("**Web:** (use your built-in web search and fetch tools)");
@@ -26176,18 +26134,25 @@ Do NOT repeat the same approach. Pivot immediately.`);
26176
26134
  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(", ")}`);
26177
26135
  else toolLines.push(`**Comms:** ${filteredComms.join(", ")}`);
26178
26136
  }
26179
- if (has("quoroom_configure_room")) toolLines.push(`**Settings:** quoroom_configure_room${selfRegulateHint}`);
26137
+ if (!isExecutor && has("quoroom_configure_room")) toolLines.push(`**Settings:** quoroom_configure_room${selfRegulateHint}`);
26180
26138
  const skillToolNames = ["quoroom_create_skill", "quoroom_list_skills", "quoroom_edit_skill", "quoroom_activate_skill", "quoroom_deactivate_skill"].filter(has);
26181
- if (skillToolNames.length) toolLines.push(`**Skills:** ${skillToolNames.join(", ")} \u2014 write execution reports (MANDATORY each cycle)`);
26139
+ if (skillToolNames.length) toolLines.push(`**Skills:** ${skillToolNames.join(", ")}`);
26140
+ if (has("quoroom_save_wip")) toolLines.push("**WIP:** quoroom_save_wip \u2014 save what you accomplished so the next cycle continues forward");
26182
26141
  const toolList = toolLines.join("\n");
26142
+ const hasWip = !!wip;
26143
+ const actionPriority = hasWip ? "You have an active WIP above \u2014 your #1 priority is to CONTINUE that action. Only handle housekeeping (votes, messages) after your main action is done or blocked." : "Take concrete action toward the room objective. Execute \u2014 do not just plan or analyze.";
26183
26144
  contextParts.push(`## Instructions
26184
- Based on the current state, decide what to do next and call the appropriate tools. Available tools:
26145
+ ${actionPriority}
26146
+
26147
+ Available tools:
26185
26148
 
26186
26149
  ${toolList}
26187
26150
 
26188
- 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.
26151
+ You have plenty of turns \u2014 run your action to completion. Don't rush.
26152
+ Before your cycle ends, save what you accomplished: quoroom_save_wip(...).
26153
+ Create a skill (quoroom_create_skill) when you COMPLETE a significant action \u2014 this shares the recipe with the whole team.
26189
26154
 
26190
- 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.
26155
+ Revenue is always a priority. Actively seek ways to earn: offer services to the keeper, propose paid work to other rooms, or find monetizable opportunities.
26191
26156
 
26192
26157
  ${toolCallInstruction}`);
26193
26158
  if (has("quoroom_browser")) {
@@ -26197,7 +26162,7 @@ You have a persistent browser tool. Use it to interact with websites: navigate,
26197
26162
  **Session persistence:**
26198
26163
  - First call: omit sessionId \u2192 starts fresh session. Note the returned sessionId.
26199
26164
  - Follow-up calls: pass sessionId back \u2192 keeps cookies, login state, localStorage.
26200
- - Sessions expire after 10 min of inactivity. If expired, start a new session and re-login.
26165
+ - Sessions expire after 30 min of inactivity. If expired, start a new session and re-login.
26201
26166
 
26202
26167
  **Reliable form filling (learned techniques):**
26203
26168
  - Always end action sequences with a \`snapshot\` to see current page state.
@@ -26218,31 +26183,139 @@ You have a persistent browser tool. Use it to interact with websites: navigate,
26218
26183
  }
26219
26184
  if (skillToolNames.length || memTools.length) {
26220
26185
  const parts = ["## Knowledge Persistence"];
26221
- parts.push("You lose context between cycles. Save important discoveries so you and your teammates can reuse them.");
26186
+ parts.push("Save important discoveries so you and your teammates can reuse them.");
26222
26187
  if (memTools.length) {
26223
26188
  parts.push(`
26224
- **Memory** (quoroom_remember/recall): Store facts, credentials, contacts, research results. Check memory with quoroom_recall before starting work \u2014 don't repeat what's already done. Also call quoroom_list_skills \u2014 skills contain the team's execution algorithms and may have exactly the recipe you need.`);
26189
+ **Memory** (quoroom_remember/recall): Store facts, credentials, contacts, research results. Check memory with quoroom_recall before starting work \u2014 don't repeat what's already done.`);
26225
26190
  }
26226
26191
  if (skillToolNames.length) {
26227
26192
  parts.push(`
26228
- **Skills \u2014 Execution Reports** (quoroom_create_skill): At the END of each cycle, create a skill documenting what you did. Skills are injected into every agent's system prompt \u2014 the whole team reads them next cycle.
26229
-
26230
- **MANDATORY: Before your cycle ends, call quoroom_create_skill with a report:**
26231
- - Title: "[task]: [site/service name]" (e.g. "Tuta signup", "Email scraping from GitHub")
26232
- - Body: step-by-step algorithm you used, including:
26233
- 1. What you tried FIRST (and why it failed, with exact error or behavior)
26234
- 2. What you tried NEXT (and whether it worked)
26235
- 3. The WORKING approach with exact selectors, URLs, field names
26236
- 4. Gotchas and warnings (e.g. "checkbox click intercepted by label \u2014 use CSS selector instead")
26237
- - Set \`autoActivate: true\` and \`activationContext\` with keywords (e.g. ["tuta", "signup", "email", "checkbox"])
26238
-
26239
- Example skill body:
26240
- "## Tuta Free Account Signup\\n1. Navigate to app.tuta.com \u2192 click 'Sign up'\\n2. Select Free plan (3rd radio) \u2192 click 'Continue'\\n3. Username: use 'fill' on textbox labeled 'Email address'\\n4. Password: use 'type' (not fill) \u2014 SPA input needs char-by-char\\n5. Checkboxes: text click fails (label intercepts). USE CSS: input[type=checkbox]:nth-of-type(1) and :nth-of-type(2)\\n6. CAPTCHA: clock type \u2014 screenshot it, read time as hh:mm\\nFAILED: Ctrl+A to clear field (doesn't work in this SPA). Use triple-click or reload page instead."
26241
-
26242
- This is NOT optional \u2014 every cycle must produce at least one skill report.`);
26193
+ **Skills** (quoroom_create_skill): When you COMPLETE a significant action, create a skill documenting the working recipe (step-by-step algorithm, exact selectors, gotchas). Skills are injected into every agent's prompt \u2014 the whole team benefits.`);
26243
26194
  }
26244
26195
  contextParts.push(parts.join("\n"));
26245
26196
  }
26197
+ if (!isExecutor) {
26198
+ const housekeepingParts = [];
26199
+ const votingDecisions = listDecisions(db2, roomId, "voting");
26200
+ if (votingDecisions.length > 0) {
26201
+ const decisionLines = votingDecisions.map((d) => {
26202
+ const votes = getVotes(db2, d.id);
26203
+ const alreadyVoted = votes.some((v) => v.workerId === worker.id);
26204
+ const proposerW = d.proposerId ? roomWorkers.find((w) => w.id === d.proposerId) : null;
26205
+ const by = proposerW ? ` (by ${proposerW.name})` : "";
26206
+ const voteStatus = alreadyVoted ? " \u2713 you voted" : " \u2190 VOTE NEEDED";
26207
+ return `- #${d.id}: ${d.proposal}${by} [${votes.length} votes so far, need ${d.minVoters}+]${voteStatus}`;
26208
+ });
26209
+ housekeepingParts.push(`**Pending Proposals** \u2014 Use quoroom_vote to cast your vote
26210
+ ${decisionLines.join("\n")}`);
26211
+ }
26212
+ const recentResolved = listRecentDecisions(db2, roomId, 5);
26213
+ if (recentResolved.length > 0) {
26214
+ housekeepingParts.push(`**Recent Decisions** (already done \u2014 do NOT repeat)
26215
+ ${recentResolved.map((d) => {
26216
+ const icon = d.status === "approved" ? "\u2713" : "\u2717";
26217
+ return `- ${icon} ${d.status}: "${d.proposal.slice(0, 120)}"`;
26218
+ }).join("\n")}`);
26219
+ }
26220
+ const myKeeperMessages = pendingEscalations.filter((e) => e.fromAgentId === worker.id && !e.toAgentId);
26221
+ const incomingWorkerMessages = pendingEscalations.filter((e) => e.toAgentId === worker.id && e.fromAgentId !== worker.id);
26222
+ if (myKeeperMessages.length > 0) {
26223
+ housekeepingParts.push(`**Pending Messages to Keeper** (awaiting reply)
26224
+ ${myKeeperMessages.map(
26225
+ (e) => `- #${e.id}: ${e.question}`
26226
+ ).join("\n")}`);
26227
+ }
26228
+ if (incomingWorkerMessages.length > 0) {
26229
+ const senderNames = new Map(roomWorkers.map((w) => [w.id, w.name]));
26230
+ housekeepingParts.push(`**Messages from Other Workers**
26231
+ ${incomingWorkerMessages.map((e) => {
26232
+ const sender = senderNames.get(e.fromAgentId ?? 0) ?? `Worker #${e.fromAgentId}`;
26233
+ return `- #${e.id} from ${sender}: ${e.question}`;
26234
+ }).join("\n")}`);
26235
+ }
26236
+ if (recentKeeperAnswers.length > 0) {
26237
+ housekeepingParts.push(`**Keeper Answers** (recent)
26238
+ ${recentKeeperAnswers.map(
26239
+ (e) => `- Q: ${e.question}
26240
+ A: ${e.answer}`
26241
+ ).join("\n")}`);
26242
+ }
26243
+ const activitySlice = recentActivity.slice(0, 15);
26244
+ if (activitySlice.length > 0) {
26245
+ housekeepingParts.push(`**Recent Activity**
26246
+ ${activitySlice.map(
26247
+ (a) => `- [${a.eventType}] ${a.summary}`
26248
+ ).join("\n")}`);
26249
+ }
26250
+ if (roomWorkers.length > 0) {
26251
+ housekeepingParts.push(`**Room Workers**
26252
+ ${roomWorkers.map(
26253
+ (w) => `- #${w.id} ${w.name}${w.role ? ` (${w.role})` : ""} \u2014 ${w.agentState}`
26254
+ ).join("\n")}`);
26255
+ }
26256
+ if (roomTasks.length > 0) {
26257
+ housekeepingParts.push(`**Room Tasks**
26258
+ ${roomTasks.map(
26259
+ (t) => `- #${t.id} "${t.name}" [${t.triggerType}] \u2014 ${t.status}`
26260
+ ).join("\n")}`);
26261
+ }
26262
+ if (housekeepingParts.length > 0) {
26263
+ contextParts.push(`## Housekeeping (handle after your main action)
26264
+ ${housekeepingParts.join("\n\n")}`);
26265
+ }
26266
+ }
26267
+ const wallet = getWalletByRoom(db2, roomId);
26268
+ if (wallet) {
26269
+ const summary = getWalletTransactionSummary(db2, wallet.id);
26270
+ const net = (parseFloat(summary.received) - parseFloat(summary.sent)).toFixed(2);
26271
+ contextParts.push(`## Wallet
26272
+ Address: ${wallet.address}
26273
+ Balance: ${net} USDC (received: ${summary.received}, spent: ${summary.sent})`);
26274
+ }
26275
+ if (unreadMessages.length > 0) {
26276
+ contextParts.push(`## Unread Messages
26277
+ ${unreadMessages.map(
26278
+ (m) => `- #${m.id} from ${m.fromRoomId ?? "unknown"}: ${m.subject}`
26279
+ ).join("\n")}`);
26280
+ }
26281
+ const activeStations = cloudStations.filter((s) => s.status === "active");
26282
+ if (cloudStations.length > 0) {
26283
+ const stationLines = cloudStations.map(
26284
+ (s) => `- #${s.id} "${s.stationName}" (${s.tier}) \u2014 ${s.status} \u2014 $${s.monthlyCost}/mo`
26285
+ );
26286
+ contextParts.push(`## Stations (${activeStations.length} active)
26287
+ ${stationLines.join("\n")}`);
26288
+ }
26289
+ if (publicRooms.length > 0) {
26290
+ const top3 = publicRooms.slice(0, 3);
26291
+ contextParts.push(
26292
+ `## Public Rooms (cross-room learning)
26293
+ Other rooms you can learn strategies from:
26294
+ ${top3.map(
26295
+ (r, i) => `${i + 1}. "${r.name}" \u2014 ${r.earnings} USDC | Goal: ${r.goal ?? "No goal set"}`
26296
+ ).join("\n")}`
26297
+ );
26298
+ }
26299
+ const keeperReferralCode = getSetting(db2, "keeper_referral_code")?.trim();
26300
+ if (keeperReferralCode) {
26301
+ const encodedKeeperCode = encodeURIComponent(keeperReferralCode);
26302
+ contextParts.push(
26303
+ `## Keeper Referral
26304
+ - Keeper code: ${keeperReferralCode}
26305
+ - Invite link: https://quoroom.ai/invite/${encodedKeeperCode}
26306
+ - Share link: https://quoroom.ai/share/v2/${encodedKeeperCode}`
26307
+ );
26308
+ }
26309
+ const settingsParts = [
26310
+ `- Cycle gap: ${Math.round(status2.room.queenCycleGapMs / 1e3)}s`,
26311
+ `- Max turns per cycle: ${status2.room.queenMaxTurns}`,
26312
+ `- Max concurrent tasks: ${status2.room.maxConcurrentTasks}`
26313
+ ];
26314
+ if (rateLimitEvents.length > 0) {
26315
+ settingsParts.push(`- **Rate limits hit recently: ${rateLimitEvents.length}** (in last ${recentActivity.length} events)`);
26316
+ }
26317
+ contextParts.push(`## Execution Settings
26318
+ ${settingsParts.join("\n")}`);
26246
26319
  const prompt = contextParts.join("\n\n");
26247
26320
  updateAgentState(db2, worker.id, "acting");
26248
26321
  const promptTokenEstimate = Math.round(prompt.length / 4);
@@ -26265,8 +26338,8 @@ This is NOT optional \u2014 every cycle must produce at least one skill report.`
26265
26338
  prompt,
26266
26339
  systemPrompt,
26267
26340
  apiKey,
26268
- timeoutMs: 5 * 60 * 1e3,
26269
- maxTurns: maxTurns ?? 10,
26341
+ timeoutMs: worker.role === "executor" ? 30 * 60 * 1e3 : 15 * 60 * 1e3,
26342
+ maxTurns: maxTurns ?? 50,
26270
26343
  onConsoleLog: logBuffer2.onConsoleLog,
26271
26344
  // CLI models: block non-quoroom MCP tools (daymon, etc.)
26272
26345
  disallowedTools: isCli ? "mcp__daymon*" : void 0,
@@ -32412,6 +32485,7 @@ CREATE TABLE IF NOT EXISTS workers (
32412
32485
  agent_state TEXT NOT NULL DEFAULT 'idle',
32413
32486
  votes_cast INTEGER NOT NULL DEFAULT 0,
32414
32487
  votes_missed INTEGER NOT NULL DEFAULT 0,
32488
+ wip TEXT,
32415
32489
  created_at DATETIME DEFAULT (datetime('now','localtime')),
32416
32490
  updated_at DATETIME DEFAULT (datetime('now','localtime'))
32417
32491
  );
@@ -32955,6 +33029,13 @@ function runMigrations(database, log = console.log) {
32955
33029
  database.exec(`ALTER TABLE rooms ADD COLUMN allowed_tools TEXT`);
32956
33030
  log("Migrated: added allowed_tools column to rooms");
32957
33031
  }
33032
+ const hasWorkerWip = database.prepare(
33033
+ `SELECT name FROM pragma_table_info('workers') WHERE name='wip'`
33034
+ ).get()?.name;
33035
+ if (!hasWorkerWip) {
33036
+ database.exec(`ALTER TABLE workers ADD COLUMN wip TEXT`);
33037
+ log("Migrated: added wip column to workers");
33038
+ }
32958
33039
  const ollamaWorkers = database.prepare(`SELECT id FROM workers WHERE model LIKE 'ollama:%'`).all();
32959
33040
  if (ollamaWorkers.length > 0) {
32960
33041
  database.prepare(`UPDATE workers SET model = 'claude' WHERE model LIKE 'ollama:%'`).run();
@@ -33178,7 +33259,7 @@ function semverGt(a, b) {
33178
33259
  }
33179
33260
  function getCurrentVersion() {
33180
33261
  try {
33181
- return true ? "0.1.30" : null.version;
33262
+ return true ? "0.1.31" : null.version;
33182
33263
  } catch {
33183
33264
  return "0.0.0";
33184
33265
  }
@@ -33337,7 +33418,7 @@ var cachedVersion = null;
33337
33418
  function getVersion3() {
33338
33419
  if (cachedVersion) return cachedVersion;
33339
33420
  try {
33340
- cachedVersion = true ? "0.1.30" : null.version;
33421
+ cachedVersion = true ? "0.1.31" : null.version;
33341
33422
  } catch {
33342
33423
  cachedVersion = "unknown";
33343
33424
  }