quoroom 0.1.27 → 0.1.28

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.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
10
10
  [![npm version](https://img.shields.io/npm/v/quoroom)](https://www.npmjs.com/package/quoroom)
11
- [![Tests](https://img.shields.io/badge/tests-971%20passing-brightgreen)](#)
11
+ [![Tests](https://img.shields.io/badge/tests-1083%20passing-brightgreen)](#)
12
12
  [![GitHub stars](https://img.shields.io/github/stars/quoroom-ai/room)](https://github.com/quoroom-ai/room/stargazers)
13
13
  [![macOS](https://img.shields.io/badge/macOS-.pkg-000000?logo=apple&logoColor=white)](https://github.com/quoroom-ai/room/releases/latest)
14
14
  [![Windows](https://img.shields.io/badge/Windows-.exe-0078D4?logo=windows&logoColor=white)](https://github.com/quoroom-ai/room/releases/latest)
@@ -81,7 +81,7 @@ Quoroom is an open research project exploring autonomous agent collectives. Each
81
81
 
82
82
  **Activity Controls** — Throttle the queen per room: configurable cycle gap (sleep between runs), max turns per cycle, and quiet hours (time window where the queen rests). Plan-aware defaults (Pro/Max/API/None) apply automatically when you create a new room based on your Claude subscription tier.
83
83
 
84
- **Quorum Voting** — Agents propose and vote on decisions. Majority, supermajority, or unanimous — you choose the threshold.
84
+ **Quorum Voting** — Agents propose and vote on decisions. Majority, supermajority, or unanimous — you choose the threshold. All voters (keeper and workers) have equal weight. Ties are broken by the queen's vote by default.
85
85
 
86
86
  **Goals** — Hierarchical goal decomposition with progress tracking. Set a top-level objective and let agents break it down.
87
87
 
@@ -197,6 +197,29 @@ Installer launchers:
197
197
  | Linux x64 | `.deb` | `.tar.gz` |
198
198
  | Windows x64 (signed) | `.exe` setup | `.zip` |
199
199
 
200
+ <details>
201
+ <summary>Platform notes</summary>
202
+
203
+ **macOS**
204
+ - Universal binary (Apple Silicon + Intel) — single `.pkg` works on both architectures
205
+ - Native menu bar tray app (Swift) — auto-starts server, provides "Open Dashboard" / "Restart" / "Quit"
206
+ - `.pkg` is codesigned + Apple-notarized
207
+ - Shell wrapper with auto-update check (polls GitHub every 4h) and crash rollback (3-strike reset)
208
+ - PATH: inherits login shell PATH at startup (`zsh -lic`) so Homebrew/NVM-installed CLIs (`claude`, `codex`) are found
209
+
210
+ **Windows**
211
+ - Signed `.exe` installer (NSIS, SSL.com eSigner)
212
+ - VBS launcher — starts server without a console window
213
+ - Adds `quoroom` to system PATH via registry
214
+ - PATH: adds npm global prefix dir at startup so globally-installed `claude.cmd` / `codex.cmd` are found
215
+ - `.cmd` wrappers (npm-installed CLIs) are auto-resolved to underlying `.js` scripts to bypass cmd.exe 8191-char argument limit
216
+
217
+ **Linux**
218
+ - `.deb` package (x64), installs to `/usr/local/lib/quoroom`
219
+ - Same shell wrapper and auto-update mechanism as macOS
220
+
221
+ </details>
222
+
200
223
  ### Uninstall
201
224
 
202
225
  ```bash
@@ -216,7 +239,7 @@ quoroom serve
216
239
 
217
240
  If you installed with the macOS `.pkg` or Windows `.exe` installer, you can also use the launcher app/shortcut instead of command line.
218
241
 
219
- On first run, `quoroom serve` automatically registers the Quoroom MCP server in every AI coding tool you have installed (Claude Code, Claude Desktop, Cursor, Windsurf). Just **restart your AI client once** — after that, all `mcp__quoroom__*` tools are available automatically in every session.
242
+ On first run, `quoroom serve` automatically registers the Quoroom MCP server in every AI coding tool you have installed (Claude Code, Claude Desktop, Codex, Cursor, Windsurf). Just **restart your AI client once** — after that, all `mcp__quoroom__*` tools are available automatically in every session.
220
243
 
221
244
  Open **http://localhost:3700** (or the port shown in your terminal). The dashboard and API run locally, and your room data stays on your machine by default.
222
245
 
@@ -425,6 +448,10 @@ npm run typecheck # Type-check only (tsc --noEmit)
425
448
  npm test # Run all tests (vitest, fork pool)
426
449
  npm run test:watch # Watch mode
427
450
  npm run test:e2e # End-to-end tests (Playwright)
451
+
452
+ # Windows
453
+ npm run dev:isolated:win # Windows equivalent of dev:isolated
454
+ npm run build:windows:local # Local Windows build (PowerShell)
428
455
  ```
429
456
 
430
457
  ### Docker (cloud runtime)
@@ -436,7 +463,13 @@ docker run -p 3700:3700 quoroom
436
463
 
437
464
  ## Releasing
438
465
 
439
- Use the release runbook: [`docs/RELEASE_RUNBOOK.md`](docs/RELEASE_RUNBOOK.md)
466
+ Triggered by pushing a git tag (`v*`) → GitHub Actions multi-platform build:
467
+
468
+ - **macOS**: Universal `.pkg` (ARM64 + x64 via `lipo`), Swift tray app compiled, codesigned + Apple-notarized
469
+ - **Windows**: NSIS `.exe` installer, signed with SSL.com eSigner
470
+ - **Linux**: `.deb` package via `fpm`
471
+ - All platforms bundle Node.js runtime (no system dependency), with auto-update and crash rollback
472
+ - Post-build: GitHub Release → npm publish → Homebrew tap update
440
473
 
441
474
  <details>
442
475
  <summary>Project structure</summary>
@@ -480,6 +513,9 @@ room/
480
513
  │ ├── embeddings.ts # Vector embeddings (all-MiniLM-L6-v2)
481
514
  │ └── __tests__/ # Test suite (907 tests)
482
515
  ├── e2e/ # Playwright end-to-end tests
516
+ ├── installers/
517
+ │ ├── macos/ # Swift tray app (QuoroomTray.swift)
518
+ │ └── windows/ # NSIS installer + PowerShell/VBS scripts
483
519
  ├── scripts/
484
520
  │ └── build-mcp.js # esbuild bundling
485
521
  ├── Dockerfile # Cloud runtime image
@@ -492,16 +528,20 @@ room/
492
528
 
493
529
  ## Model Providers
494
530
 
495
- Use your existing Claude/ChatGPT subscription or API.
531
+ Use your existing Claude or ChatGPT subscription, or bring an API key.
532
+
533
+ | Model string | Provider | Execution | Requires |
534
+ |---|---|---|---|
535
+ | `claude` (default) | Claude Code CLI | Spawns CLI process | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed |
536
+ | `codex` | OpenAI Codex CLI | Spawns CLI process | `npm i -g @openai/codex` |
537
+ | `openai:gpt-4o-mini` | OpenAI API | HTTP REST | `OPENAI_API_KEY` |
538
+ | `anthropic:claude-3-5-sonnet-latest` | Anthropic API | HTTP REST | `ANTHROPIC_API_KEY` |
539
+
540
+ **CLI models** (`claude`, `codex`) — Full agentic loop with tool use via the CLI. Session continuity via `--resume`. On Windows, `.cmd` wrappers are auto-resolved to underlying `.js` scripts to bypass the cmd.exe 8191-char argument limit.
541
+
542
+ **API models** (`openai:*`, `anthropic:*`) — Direct HTTP calls. Support multi-turn tool-calling loops. API keys resolve from: room credentials → Clerk-saved keys → environment variables. `anthropic:*` also accepts the `claude-api:` prefix.
496
543
 
497
- | Role | Provider | Cost |
498
- |------|----------|------|
499
- | **Queen** | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | Subscription |
500
- | | Codex (ChatGPT) | Subscription |
501
- | | OpenAI API | Pay-per-use |
502
- | | Claude API | Pay-per-use |
503
- | **Workers** | Inherit queen model | Depends on queen |
504
- | | Claude (subscription or API) | Subscription / API |
544
+ Workers inherit the queen's model by default, or can use a separate API model.
505
545
 
506
546
  ## License
507
547
 
@@ -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.27",
9917
+ version: "0.1.28",
9918
9918
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
9919
9919
  main: "./out/mcp/server.js",
9920
9920
  bin: {
@@ -11343,7 +11343,6 @@ Rules:
11343
11343
  var DEFAULT_ROOM_CONFIG = {
11344
11344
  threshold: "majority",
11345
11345
  timeoutMinutes: 60,
11346
- keeperWeight: "dynamic",
11347
11346
  tieBreaker: "queen",
11348
11347
  autoApprove: ["low_impact"],
11349
11348
  minCycleGapMs: 1e3,
@@ -12342,10 +12341,10 @@ function getRoomActivity(db2, roomId, limit = 50, eventTypes) {
12342
12341
  const safeLimit = clampLimit(limit, 50, 500);
12343
12342
  if (eventTypes && eventTypes.length > 0) {
12344
12343
  const placeholders = eventTypes.map(() => "?").join(", ");
12345
- const rows2 = db2.prepare(`SELECT * FROM room_activity WHERE room_id = ? AND event_type IN (${placeholders}) ORDER BY created_at DESC LIMIT ?`).all(roomId, ...eventTypes, safeLimit);
12344
+ const rows2 = db2.prepare(`SELECT * FROM room_activity WHERE room_id = ? AND event_type IN (${placeholders}) ORDER BY created_at DESC, id DESC LIMIT ?`).all(roomId, ...eventTypes, safeLimit);
12346
12345
  return rows2.map(mapRoomActivityRow);
12347
12346
  }
12348
- const rows = db2.prepare("SELECT * FROM room_activity WHERE room_id = ? ORDER BY created_at DESC LIMIT ?").all(roomId, safeLimit);
12347
+ const rows = db2.prepare("SELECT * FROM room_activity WHERE room_id = ? ORDER BY created_at DESC, id DESC LIMIT ?").all(roomId, safeLimit);
12349
12348
  return rows.map(mapRoomActivityRow);
12350
12349
  }
12351
12350
  function mapDecisionRow(row) {
@@ -12659,7 +12658,24 @@ function mapEscalationRow(row) {
12659
12658
  }
12660
12659
  function createEscalation(db2, roomId, fromAgentId, question, toAgentId) {
12661
12660
  const result = db2.prepare("INSERT INTO escalations (room_id, from_agent_id, to_agent_id, question) VALUES (?, ?, ?, ?)").run(roomId, fromAgentId, toAgentId ?? null, question);
12662
- return getEscalation(db2, result.lastInsertRowid);
12661
+ const escalation = getEscalation(db2, result.lastInsertRowid);
12662
+ const trimmedQuestion = question.trim();
12663
+ const detail = trimmedQuestion.length > 1e3 ? `${trimmedQuestion.slice(0, 1e3)}\u2026` : trimmedQuestion;
12664
+ let summary = "Message created";
12665
+ if (toAgentId == null) {
12666
+ summary = fromAgentId != null ? `Worker #${fromAgentId} sent message to keeper` : "Message sent to keeper";
12667
+ } else {
12668
+ summary = fromAgentId != null ? `Worker #${fromAgentId} sent message to worker #${toAgentId}` : `Keeper sent message to worker #${toAgentId}`;
12669
+ }
12670
+ logRoomActivity(
12671
+ db2,
12672
+ roomId,
12673
+ fromAgentId != null ? "worker" : "system",
12674
+ summary,
12675
+ detail || void 0,
12676
+ fromAgentId ?? void 0
12677
+ );
12678
+ return escalation;
12663
12679
  }
12664
12680
  function getEscalation(db2, id) {
12665
12681
  const row = db2.prepare("SELECT * FROM escalations WHERE id = ?").get(id);
@@ -12682,7 +12698,24 @@ function listEscalations(db2, roomId, status2) {
12682
12698
  return rows.map(mapEscalationRow);
12683
12699
  }
12684
12700
  function resolveEscalation(db2, id, answer) {
12701
+ const escalation = getEscalation(db2, id);
12685
12702
  db2.prepare("UPDATE escalations SET answer = ?, status = 'resolved', resolved_at = datetime('now','localtime') WHERE id = ?").run(answer, id);
12703
+ if (!escalation) return;
12704
+ const trimmedAnswer = answer.trim();
12705
+ const detail = trimmedAnswer.length > 1e3 ? `${trimmedAnswer.slice(0, 1e3)}\u2026` : trimmedAnswer;
12706
+ let summary = "Message resolved";
12707
+ if (escalation.toAgentId == null && escalation.fromAgentId != null) {
12708
+ summary = `Keeper replied to worker #${escalation.fromAgentId}`;
12709
+ } else if (escalation.toAgentId != null) {
12710
+ summary = `Message resolved for worker #${escalation.toAgentId}`;
12711
+ }
12712
+ logRoomActivity(
12713
+ db2,
12714
+ escalation.roomId,
12715
+ "system",
12716
+ summary,
12717
+ detail || void 0
12718
+ );
12686
12719
  }
12687
12720
  function getRecentKeeperAnswers(db2, roomId, fromAgentId, limit = 5) {
12688
12721
  const rows = db2.prepare(
@@ -13146,6 +13179,9 @@ function mapCycleLogRow(row) {
13146
13179
  };
13147
13180
  }
13148
13181
  function createWorkerCycle(db2, workerId, roomId, model) {
13182
+ db2.prepare(
13183
+ "UPDATE worker_cycles SET status = 'failed', error_message = 'Superseded by newer cycle', finished_at = datetime('now','localtime') WHERE worker_id = ? AND status = 'running'"
13184
+ ).run(workerId);
13149
13185
  const result = db2.prepare(
13150
13186
  "INSERT INTO worker_cycles (worker_id, room_id, model) VALUES (?, ?, ?)"
13151
13187
  ).run(workerId, roomId, model);
@@ -13167,7 +13203,7 @@ function completeWorkerCycle(db2, cycleId, errorMessage, usage) {
13167
13203
  function listRoomCycles(db2, roomId, limit = 20) {
13168
13204
  const safeLimit = clampLimit(limit, 20, 200);
13169
13205
  const rows = db2.prepare(
13170
- "SELECT * FROM worker_cycles WHERE room_id = ? ORDER BY started_at DESC LIMIT ?"
13206
+ "SELECT * FROM worker_cycles WHERE room_id = ? ORDER BY started_at DESC, id DESC LIMIT ?"
13171
13207
  ).all(roomId, safeLimit);
13172
13208
  return rows.map(mapWorkerCycleRow);
13173
13209
  }
@@ -24042,12 +24078,34 @@ async function executeApiOnStation(cloudRoomId, stationId, options) {
24042
24078
  }
24043
24079
 
24044
24080
  // src/shared/skills.ts
24081
+ var MAX_ACTIVE_SKILLS_PER_CYCLE = 8;
24082
+ var MAX_SKILL_CONTEXT_CHARS = 6e3;
24045
24083
  function loadSkillsForAgent(db2, roomId, contextText) {
24046
24084
  const skills = getActiveSkillsForContext(db2, roomId, contextText);
24047
24085
  if (skills.length === 0) return "";
24048
- return skills.map((s) => `## Skill: ${s.name}
24086
+ const sections = [];
24087
+ let used = 0;
24088
+ for (const skill of skills.slice(0, MAX_ACTIVE_SKILLS_PER_CYCLE)) {
24089
+ const prefix = sections.length > 0 ? "\n\n---\n\n" : "";
24090
+ const full = `${prefix}## Skill: ${skill.name}
24091
+
24092
+ ${skill.content}`;
24093
+ const remaining = MAX_SKILL_CONTEXT_CHARS - used;
24094
+ if (remaining <= 0) break;
24095
+ if (full.length <= remaining) {
24096
+ sections.push(full);
24097
+ used += full.length;
24098
+ continue;
24099
+ }
24100
+ const clipped = full.slice(0, Math.max(0, remaining - 32)).trimEnd();
24101
+ if (clipped) {
24102
+ sections.push(`${clipped}
24049
24103
 
24050
- ${s.content}`).join("\n\n---\n\n");
24104
+ [truncated for cycle context]`);
24105
+ }
24106
+ break;
24107
+ }
24108
+ return sections.join("");
24051
24109
  }
24052
24110
 
24053
24111
  // src/shared/quorum.ts
@@ -24148,8 +24206,6 @@ function tally(db2, decisionId) {
24148
24206
  return "rejected";
24149
24207
  }
24150
24208
  }
24151
- const keeperWeightMode = room?.config.keeperWeight ?? "dynamic";
24152
- const useWeighted = keeperWeightMode === "dynamic" && voters.length <= 1;
24153
24209
  const queenWorkerId = room?.queenWorkerId ?? null;
24154
24210
  const tieBreakerMode = room?.config.tieBreaker ?? "queen";
24155
24211
  let yesWeight = 0;
@@ -24162,9 +24218,8 @@ function tally(db2, decisionId) {
24162
24218
  }
24163
24219
  const kv = decision.keeperVote;
24164
24220
  if (kv && kv !== "abstain") {
24165
- const keeperWeight = useWeighted ? 1.02 : 1;
24166
- if (kv === "yes") yesWeight += keeperWeight;
24167
- else if (kv === "no") noWeight += keeperWeight;
24221
+ if (kv === "yes") yesWeight += 1;
24222
+ else if (kv === "no") noWeight += 1;
24168
24223
  } else if (kv === "abstain") {
24169
24224
  abstainCount++;
24170
24225
  }
@@ -24191,9 +24246,7 @@ function tally(db2, decisionId) {
24191
24246
  }
24192
24247
  }
24193
24248
  }
24194
- const yesDisplay = useWeighted ? yesWeight.toFixed(2) : String(yesWeight);
24195
- const noDisplay = useWeighted ? noWeight.toFixed(2) : String(noWeight);
24196
- const result = `Yes: ${yesDisplay}, No: ${noDisplay}, Abstain: ${abstainCount}` + (useWeighted ? " (weighted)" : "");
24249
+ const result = `Yes: ${yesWeight}, No: ${noWeight}, Abstain: ${abstainCount}`;
24197
24250
  resolveDecision(db2, decisionId, status2, result);
24198
24251
  logRoomActivity(
24199
24252
  db2,
@@ -25736,6 +25789,9 @@ function checkRateLimit(result) {
25736
25789
  sessionId: result.sessionId
25737
25790
  });
25738
25791
  }
25792
+ function isCliContextOverflowError(message) {
25793
+ return /compact|compaction|context.*(window|limit|overflow|too large)|model_visible_bytes|token.*limit.*exceed/i.test(message);
25794
+ }
25739
25795
  async function runCycle(db2, roomId, worker, maxTurns, options) {
25740
25796
  logRoomActivity(
25741
25797
  db2,
@@ -25820,14 +25876,22 @@ async function runCycle(db2, roomId, worker, maxTurns, options) {
25820
25876
  ${skillContent}` : ""
25821
25877
  ].join("");
25822
25878
  const isCli = model === "claude" || model.startsWith("claude-") || model === "codex";
25879
+ const CLI_SESSION_MAX_TURNS = 20;
25823
25880
  let resumeSessionId;
25824
25881
  let previousMessages;
25825
25882
  const agentSession = getAgentSession(db2, worker.id);
25826
25883
  if (agentSession) {
25827
25884
  const updatedAt = new Date(agentSession.updatedAt);
25828
25885
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
25829
- if (updatedAt < sevenDaysAgo || agentSession.model !== model) {
25886
+ const cliSessionTooLong = isCli && !!agentSession.sessionId && agentSession.turnCount >= CLI_SESSION_MAX_TURNS;
25887
+ if (updatedAt < sevenDaysAgo || agentSession.model !== model || cliSessionTooLong) {
25830
25888
  deleteAgentSession(db2, worker.id);
25889
+ if (cliSessionTooLong) {
25890
+ logBuffer2.addSynthetic(
25891
+ "system",
25892
+ `Session rotated after ${agentSession.turnCount} cycles to avoid context overflow`
25893
+ );
25894
+ }
25831
25895
  } else if (isCli && agentSession.sessionId) {
25832
25896
  resumeSessionId = agentSession.sessionId;
25833
25897
  } else if (!isCli && agentSession.messagesJson) {
@@ -26157,7 +26221,7 @@ This is NOT optional \u2014 every cycle must produce at least one skill report.`
26157
26221
  return result2.content;
26158
26222
  }
26159
26223
  } : {};
26160
- const result = await executeAgent({
26224
+ const executeWithSession = (sessionId) => executeAgent({
26161
26225
  model,
26162
26226
  prompt,
26163
26227
  systemPrompt,
@@ -26170,7 +26234,7 @@ This is NOT optional \u2014 every cycle must produce at least one skill report.`
26170
26234
  // CLI models: bypass permission prompts for headless operation
26171
26235
  permissionMode: isCli ? "bypassPermissions" : void 0,
26172
26236
  // CLI models: pass resumeSessionId for native --resume
26173
- resumeSessionId,
26237
+ resumeSessionId: sessionId,
26174
26238
  // API models: pass conversation history + persistence callback
26175
26239
  previousMessages: isCli ? void 0 : previousMessages,
26176
26240
  onSessionUpdate: isCli ? void 0 : (msgs) => {
@@ -26179,6 +26243,16 @@ This is NOT optional \u2014 every cycle must produce at least one skill report.`
26179
26243
  },
26180
26244
  ...apiToolOpts
26181
26245
  });
26246
+ let result = await executeWithSession(resumeSessionId);
26247
+ if (isCli && result.exitCode !== 0) {
26248
+ const failure = result.output?.trim() || "";
26249
+ if (isCliContextOverflowError(failure)) {
26250
+ deleteAgentSession(db2, worker.id);
26251
+ logBuffer2.addSynthetic("system", "Session overflow detected \u2014 retrying this cycle with a fresh session");
26252
+ logBuffer2.flush();
26253
+ result = await executeWithSession(void 0);
26254
+ }
26255
+ }
26182
26256
  const rateLimitInfo = checkRateLimit(result);
26183
26257
  if (rateLimitInfo) {
26184
26258
  throw new RateLimitError(rateLimitInfo);
@@ -26198,6 +26272,13 @@ This is NOT optional \u2014 every cycle must produce at least one skill report.`
26198
26272
  worker.id
26199
26273
  );
26200
26274
  updateAgentState(db2, worker.id, "idle");
26275
+ if (isCli) {
26276
+ if (isCliContextOverflowError(errorDetail)) {
26277
+ deleteAgentSession(db2, worker.id);
26278
+ logBuffer2.addSynthetic("system", "Session reset due to context overflow \u2014 next cycle will start fresh");
26279
+ logBuffer2.flush();
26280
+ }
26281
+ }
26201
26282
  return result.output;
26202
26283
  }
26203
26284
  if (isCli && result.sessionId) {
@@ -27964,6 +28045,12 @@ function isRateLimitFailure(message) {
27964
28045
  const lower = message.toLowerCase();
27965
28046
  return lower.includes("rate limit") || lower.includes("limit reached") || lower.includes("too many requests") || lower.includes("insufficient_quota") || lower.includes("quota") || lower.includes("429") || lower.includes("limit") && lower.includes("reset");
27966
28047
  }
28048
+ function isTransientFailure(message, timedOut) {
28049
+ if (timedOut) return true;
28050
+ if (isRateLimitFailure(message)) return true;
28051
+ const lower = message.toLowerCase();
28052
+ return lower.includes("timed out") || lower.includes("timeout") || lower.includes("temporarily unavailable") || lower.includes("service unavailable") || lower.includes("connection reset") || lower.includes("socket hang up") || lower.includes("econnreset") || lower.includes("etimedout") || lower.includes("eai_again");
28053
+ }
27967
28054
  async function executeClerkWithFallback(options) {
27968
28055
  const preferred = options.preferredModel?.trim() || DEFAULT_CLERK_MODEL;
27969
28056
  const candidates = buildExecutionCandidates(options.db, preferred);
@@ -28002,7 +28089,7 @@ async function executeClerkWithFallback(options) {
28002
28089
  const error = deriveExecutionFailure(result, candidate.model);
28003
28090
  attempts.push({ model: candidate.model, error });
28004
28091
  const hasMoreCandidates = i < candidates.length - 1;
28005
- if (!hasMoreCandidates || !isRateLimitFailure(error)) {
28092
+ if (!hasMoreCandidates || !isTransientFailure(error, result.timedOut)) {
28006
28093
  return {
28007
28094
  ok: false,
28008
28095
  model: candidate.model,
@@ -29995,10 +30082,22 @@ function registerClerkRoutes(router) {
29995
30082
  init_cloud_sync();
29996
30083
  var CLERK_NOTIFY_EMAIL_KEY = "clerk_notify_email";
29997
30084
  var CLERK_NOTIFY_TELEGRAM_KEY = "clerk_notify_telegram";
30085
+ var CLERK_NOTIFY_MIN_INTERVAL_MINUTES_KEY = "clerk_notify_min_interval_minutes";
30086
+ var CLERK_NOTIFY_URGENT_MIN_INTERVAL_MINUTES_KEY = "clerk_notify_urgent_min_interval_minutes";
29998
30087
  var CLERK_NOTIFY_ESCALATION_CURSOR_KEY = "clerk_notify_escalation_cursor";
29999
30088
  var CLERK_NOTIFY_DECISION_CURSOR_KEY = "clerk_notify_decision_cursor";
30000
30089
  var CLERK_NOTIFY_ROOM_MESSAGE_CURSOR_KEY = "clerk_notify_room_message_cursor";
30001
30090
  var CLERK_NOTIFY_DIGEST_STYLE_CURSOR_KEY = "clerk_notify_digest_style_cursor";
30091
+ var CLERK_NOTIFY_LAST_SENT_AT_KEY = "clerk_notify_last_sent_at";
30092
+ var DEFAULT_NOTIFY_MIN_INTERVAL_MS = 6 * 60 * 60 * 1e3;
30093
+ var DEFAULT_NOTIFY_URGENT_MIN_INTERVAL_MS = 60 * 60 * 1e3;
30094
+ var MIN_NOTIFY_INTERVAL_MS = 30 * 60 * 1e3;
30095
+ var MAX_NOTIFY_INTERVAL_MS = 24 * 60 * 60 * 1e3;
30096
+ var MIN_URGENT_INTERVAL_MS = 10 * 60 * 1e3;
30097
+ var MAX_URGENT_INTERVAL_MS = 12 * 60 * 60 * 1e3;
30098
+ var URGENT_ESCALATION_THRESHOLD = 6;
30099
+ var URGENT_DECISION_THRESHOLD = 4;
30100
+ var URGENT_TOTAL_THRESHOLD = 12;
30002
30101
  var DIGEST_OPENERS = [
30003
30102
  (count) => `I need your call on ${count} item${count === 1 ? "" : "s"}.`,
30004
30103
  (count) => `${count} decision point${count === 1 ? "" : "s"} need${count === 1 ? "s" : ""} your direction right now.`,
@@ -30036,6 +30135,20 @@ function parseCursor(raw) {
30036
30135
  if (!Number.isFinite(parsed) || parsed < 0) return 0;
30037
30136
  return parsed;
30038
30137
  }
30138
+ function parseIsoMs3(raw) {
30139
+ const value = String(raw ?? "").trim();
30140
+ if (!value) return null;
30141
+ const parsed = Date.parse(value);
30142
+ return Number.isFinite(parsed) ? parsed : null;
30143
+ }
30144
+ function resolveIntervalMs(db2, key, fallbackMs, minMs, maxMs) {
30145
+ const raw = (getSetting(db2, key) ?? "").trim();
30146
+ if (!raw) return fallbackMs;
30147
+ const minutes = Number.parseInt(raw, 10);
30148
+ if (!Number.isFinite(minutes) || minutes < 0) return fallbackMs;
30149
+ if (minutes === 0) return 0;
30150
+ return Math.min(maxMs, Math.max(minMs, minutes * 6e4));
30151
+ }
30039
30152
  function getKeeperUserNumber2(db2) {
30040
30153
  const raw = (getSetting(db2, "keeper_user_number") ?? "").trim();
30041
30154
  if (!/^\d{5,6}$/.test(raw)) return null;
@@ -30183,6 +30296,20 @@ function buildKeeperDigest(db2, input, styleCursor) {
30183
30296
  );
30184
30297
  return lines.join("\n");
30185
30298
  }
30299
+ function getPendingDigestCounts(input) {
30300
+ const escalations = input.escalations.length;
30301
+ const decisions = input.decisions.length;
30302
+ const roomMessages = input.roomMessages.length;
30303
+ return {
30304
+ escalations,
30305
+ decisions,
30306
+ roomMessages,
30307
+ total: escalations + decisions + roomMessages
30308
+ };
30309
+ }
30310
+ function isUrgentDigest(counts) {
30311
+ return counts.escalations >= URGENT_ESCALATION_THRESHOLD || counts.decisions >= URGENT_DECISION_THRESHOLD || counts.total >= URGENT_TOTAL_THRESHOLD;
30312
+ }
30186
30313
  async function relayPendingKeeperRequests(db2) {
30187
30314
  if (isCloudDeployment()) return;
30188
30315
  const channels = getClerkPreferredNotifyChannels(db2);
@@ -30215,6 +30342,32 @@ async function relayPendingKeeperRequests(db2) {
30215
30342
  roomMessages: pendingRoomMessages
30216
30343
  }, digestStyleCursor);
30217
30344
  if (!content) return;
30345
+ const pendingCounts = getPendingDigestCounts({
30346
+ escalations: pendingEscalations,
30347
+ decisions: pendingDecisions,
30348
+ roomMessages: pendingRoomMessages
30349
+ });
30350
+ const minIntervalMs = resolveIntervalMs(
30351
+ db2,
30352
+ CLERK_NOTIFY_MIN_INTERVAL_MINUTES_KEY,
30353
+ DEFAULT_NOTIFY_MIN_INTERVAL_MS,
30354
+ MIN_NOTIFY_INTERVAL_MS,
30355
+ MAX_NOTIFY_INTERVAL_MS
30356
+ );
30357
+ const urgentIntervalMs = resolveIntervalMs(
30358
+ db2,
30359
+ CLERK_NOTIFY_URGENT_MIN_INTERVAL_MINUTES_KEY,
30360
+ DEFAULT_NOTIFY_URGENT_MIN_INTERVAL_MS,
30361
+ MIN_URGENT_INTERVAL_MS,
30362
+ MAX_URGENT_INTERVAL_MS
30363
+ );
30364
+ const lastSentMs = parseIsoMs3(getSetting(db2, CLERK_NOTIFY_LAST_SENT_AT_KEY));
30365
+ if (lastSentMs != null) {
30366
+ const elapsedMs = Date.now() - lastSentMs;
30367
+ const regularAllowed = minIntervalMs <= 0 || elapsedMs >= minIntervalMs;
30368
+ const urgentAllowed = isUrgentDigest(pendingCounts) && (urgentIntervalMs <= 0 || elapsedMs >= urgentIntervalMs);
30369
+ if (!regularAllowed && !urgentAllowed) return;
30370
+ }
30218
30371
  const sent = await sendClerkAlert(db2, { content });
30219
30372
  if (!sent) return;
30220
30373
  const nextEscalationCursor = pendingEscalations.reduce((max, item) => Math.max(max, item.escalation?.id ?? 0), escalationCursor);
@@ -30224,6 +30377,7 @@ async function relayPendingKeeperRequests(db2) {
30224
30377
  setSetting(db2, CLERK_NOTIFY_DECISION_CURSOR_KEY, String(nextDecisionCursor));
30225
30378
  setSetting(db2, CLERK_NOTIFY_ROOM_MESSAGE_CURSOR_KEY, String(nextRoomMessageCursor));
30226
30379
  setSetting(db2, CLERK_NOTIFY_DIGEST_STYLE_CURSOR_KEY, String(digestStyleCursor + 1));
30380
+ setSetting(db2, CLERK_NOTIFY_LAST_SENT_AT_KEY, (/* @__PURE__ */ new Date()).toISOString());
30227
30381
  const byRoom = /* @__PURE__ */ new Map();
30228
30382
  for (const item of pendingEscalations) {
30229
30383
  if (!item.escalation) continue;
@@ -32957,7 +33111,7 @@ function semverGt(a, b) {
32957
33111
  }
32958
33112
  function getCurrentVersion() {
32959
33113
  try {
32960
- return true ? "0.1.27" : null.version;
33114
+ return true ? "0.1.28" : null.version;
32961
33115
  } catch {
32962
33116
  return "0.0.0";
32963
33117
  }
@@ -33116,7 +33270,7 @@ var cachedVersion = null;
33116
33270
  function getVersion3() {
33117
33271
  if (cachedVersion) return cachedVersion;
33118
33272
  try {
33119
- cachedVersion = true ? "0.1.27" : null.version;
33273
+ cachedVersion = true ? "0.1.28" : null.version;
33120
33274
  } catch {
33121
33275
  cachedVersion = "unknown";
33122
33276
  }
package/out/mcp/cli.js CHANGED
@@ -22241,7 +22241,6 @@ Rules:
22241
22241
  DEFAULT_ROOM_CONFIG = {
22242
22242
  threshold: "majority",
22243
22243
  timeoutMinutes: 60,
22244
- keeperWeight: "dynamic",
22245
22244
  tieBreaker: "queen",
22246
22245
  autoApprove: ["low_impact"],
22247
22246
  minCycleGapMs: 1e3,
@@ -23450,10 +23449,10 @@ function getRoomActivity(db3, roomId, limit = 50, eventTypes) {
23450
23449
  const safeLimit = clampLimit(limit, 50, 500);
23451
23450
  if (eventTypes && eventTypes.length > 0) {
23452
23451
  const placeholders = eventTypes.map(() => "?").join(", ");
23453
- const rows2 = db3.prepare(`SELECT * FROM room_activity WHERE room_id = ? AND event_type IN (${placeholders}) ORDER BY created_at DESC LIMIT ?`).all(roomId, ...eventTypes, safeLimit);
23452
+ const rows2 = db3.prepare(`SELECT * FROM room_activity WHERE room_id = ? AND event_type IN (${placeholders}) ORDER BY created_at DESC, id DESC LIMIT ?`).all(roomId, ...eventTypes, safeLimit);
23454
23453
  return rows2.map(mapRoomActivityRow);
23455
23454
  }
23456
- const rows = db3.prepare("SELECT * FROM room_activity WHERE room_id = ? ORDER BY created_at DESC LIMIT ?").all(roomId, safeLimit);
23455
+ const rows = db3.prepare("SELECT * FROM room_activity WHERE room_id = ? ORDER BY created_at DESC, id DESC LIMIT ?").all(roomId, safeLimit);
23457
23456
  return rows.map(mapRoomActivityRow);
23458
23457
  }
23459
23458
  function mapDecisionRow(row) {
@@ -23783,7 +23782,24 @@ function mapEscalationRow(row) {
23783
23782
  }
23784
23783
  function createEscalation(db3, roomId, fromAgentId, question, toAgentId) {
23785
23784
  const result = db3.prepare("INSERT INTO escalations (room_id, from_agent_id, to_agent_id, question) VALUES (?, ?, ?, ?)").run(roomId, fromAgentId, toAgentId ?? null, question);
23786
- return getEscalation(db3, result.lastInsertRowid);
23785
+ const escalation = getEscalation(db3, result.lastInsertRowid);
23786
+ const trimmedQuestion = question.trim();
23787
+ const detail = trimmedQuestion.length > 1e3 ? `${trimmedQuestion.slice(0, 1e3)}\u2026` : trimmedQuestion;
23788
+ let summary = "Message created";
23789
+ if (toAgentId == null) {
23790
+ summary = fromAgentId != null ? `Worker #${fromAgentId} sent message to keeper` : "Message sent to keeper";
23791
+ } else {
23792
+ summary = fromAgentId != null ? `Worker #${fromAgentId} sent message to worker #${toAgentId}` : `Keeper sent message to worker #${toAgentId}`;
23793
+ }
23794
+ logRoomActivity(
23795
+ db3,
23796
+ roomId,
23797
+ fromAgentId != null ? "worker" : "system",
23798
+ summary,
23799
+ detail || void 0,
23800
+ fromAgentId ?? void 0
23801
+ );
23802
+ return escalation;
23787
23803
  }
23788
23804
  function getEscalation(db3, id) {
23789
23805
  const row = db3.prepare("SELECT * FROM escalations WHERE id = ?").get(id);
@@ -23806,7 +23822,24 @@ function listEscalations(db3, roomId, status2) {
23806
23822
  return rows.map(mapEscalationRow);
23807
23823
  }
23808
23824
  function resolveEscalation(db3, id, answer) {
23825
+ const escalation = getEscalation(db3, id);
23809
23826
  db3.prepare("UPDATE escalations SET answer = ?, status = 'resolved', resolved_at = datetime('now','localtime') WHERE id = ?").run(answer, id);
23827
+ if (!escalation) return;
23828
+ const trimmedAnswer = answer.trim();
23829
+ const detail = trimmedAnswer.length > 1e3 ? `${trimmedAnswer.slice(0, 1e3)}\u2026` : trimmedAnswer;
23830
+ let summary = "Message resolved";
23831
+ if (escalation.toAgentId == null && escalation.fromAgentId != null) {
23832
+ summary = `Keeper replied to worker #${escalation.fromAgentId}`;
23833
+ } else if (escalation.toAgentId != null) {
23834
+ summary = `Message resolved for worker #${escalation.toAgentId}`;
23835
+ }
23836
+ logRoomActivity(
23837
+ db3,
23838
+ escalation.roomId,
23839
+ "system",
23840
+ summary,
23841
+ detail || void 0
23842
+ );
23810
23843
  }
23811
23844
  function getRecentKeeperAnswers(db3, roomId, fromAgentId, limit = 5) {
23812
23845
  const rows = db3.prepare(
@@ -24332,6 +24365,9 @@ function mapCycleLogRow(row) {
24332
24365
  };
24333
24366
  }
24334
24367
  function createWorkerCycle(db3, workerId, roomId, model) {
24368
+ db3.prepare(
24369
+ "UPDATE worker_cycles SET status = 'failed', error_message = 'Superseded by newer cycle', finished_at = datetime('now','localtime') WHERE worker_id = ? AND status = 'running'"
24370
+ ).run(workerId);
24335
24371
  const result = db3.prepare(
24336
24372
  "INSERT INTO worker_cycles (worker_id, room_id, model) VALUES (?, ?, ?)"
24337
24373
  ).run(workerId, roomId, model);
@@ -24353,7 +24389,7 @@ function completeWorkerCycle(db3, cycleId, errorMessage, usage) {
24353
24389
  function listRoomCycles(db3, roomId, limit = 20) {
24354
24390
  const safeLimit = clampLimit(limit, 20, 200);
24355
24391
  const rows = db3.prepare(
24356
- "SELECT * FROM worker_cycles WHERE room_id = ? ORDER BY started_at DESC LIMIT ?"
24392
+ "SELECT * FROM worker_cycles WHERE room_id = ? ORDER BY started_at DESC, id DESC LIMIT ?"
24357
24393
  ).all(roomId, safeLimit);
24358
24394
  return rows.map(mapWorkerCycleRow);
24359
24395
  }
@@ -49538,7 +49574,6 @@ function registerRoomTools(server) {
49538
49574
  maxConcurrentTasks: external_exports.number().int().min(1).max(10).optional().describe("Maximum parallel tasks (1\u201310)"),
49539
49575
  threshold: external_exports.enum(["majority", "supermajority", "unanimous"]).optional().describe("Voting threshold for quorum decisions"),
49540
49576
  autoApprove: external_exports.array(external_exports.string()).optional().describe('Decision types to auto-approve without voting (e.g. ["low_impact"])'),
49541
- keeperWeight: external_exports.enum(["dynamic", "equal"]).optional().describe("Keeper vote weighting: dynamic (51% in small rooms) or equal"),
49542
49577
  tieBreaker: external_exports.enum(["queen", "none"]).optional().describe("Tie-breaking strategy: queen vote decides, or reject on tie"),
49543
49578
  minVoters: external_exports.number().int().min(0).max(100).optional().describe("Minimum non-abstain votes required for a decision to pass (0 = no minimum)"),
49544
49579
  sealedBallot: external_exports.boolean().optional().describe("When true, individual votes are hidden until the decision resolves"),
@@ -49553,7 +49588,6 @@ function registerRoomTools(server) {
49553
49588
  maxConcurrentTasks,
49554
49589
  threshold,
49555
49590
  autoApprove,
49556
- keeperWeight,
49557
49591
  tieBreaker,
49558
49592
  minVoters,
49559
49593
  sealedBallot,
@@ -49588,10 +49622,6 @@ function registerRoomTools(server) {
49588
49622
  configUpdates.autoApprove = autoApprove;
49589
49623
  changes.push(`autoApprove: [${room.config.autoApprove.join(",")}] \u2192 [${autoApprove.join(",")}]`);
49590
49624
  }
49591
- if (keeperWeight !== void 0) {
49592
- configUpdates.keeperWeight = keeperWeight;
49593
- changes.push(`keeperWeight: ${room.config.keeperWeight} \u2192 ${keeperWeight}`);
49594
- }
49595
49625
  if (tieBreaker !== void 0) {
49596
49626
  configUpdates.tieBreaker = tieBreaker;
49597
49627
  changes.push(`tieBreaker: ${room.config.tieBreaker} \u2192 ${tieBreaker}`);
@@ -49743,8 +49773,6 @@ function tally(db3, decisionId) {
49743
49773
  return "rejected";
49744
49774
  }
49745
49775
  }
49746
- const keeperWeightMode = room?.config.keeperWeight ?? "dynamic";
49747
- const useWeighted = keeperWeightMode === "dynamic" && voters.length <= 1;
49748
49776
  const queenWorkerId = room?.queenWorkerId ?? null;
49749
49777
  const tieBreakerMode = room?.config.tieBreaker ?? "queen";
49750
49778
  let yesWeight = 0;
@@ -49757,9 +49785,8 @@ function tally(db3, decisionId) {
49757
49785
  }
49758
49786
  const kv = decision.keeperVote;
49759
49787
  if (kv && kv !== "abstain") {
49760
- const keeperWeight = useWeighted ? 1.02 : 1;
49761
- if (kv === "yes") yesWeight += keeperWeight;
49762
- else if (kv === "no") noWeight += keeperWeight;
49788
+ if (kv === "yes") yesWeight += 1;
49789
+ else if (kv === "no") noWeight += 1;
49763
49790
  } else if (kv === "abstain") {
49764
49791
  abstainCount++;
49765
49792
  }
@@ -49786,9 +49813,7 @@ function tally(db3, decisionId) {
49786
49813
  }
49787
49814
  }
49788
49815
  }
49789
- const yesDisplay = useWeighted ? yesWeight.toFixed(2) : String(yesWeight);
49790
- const noDisplay = useWeighted ? noWeight.toFixed(2) : String(noWeight);
49791
- const result = `Yes: ${yesDisplay}, No: ${noDisplay}, Abstain: ${abstainCount}` + (useWeighted ? " (weighted)" : "");
49816
+ const result = `Yes: ${yesWeight}, No: ${noWeight}, Abstain: ${abstainCount}`;
49792
49817
  resolveDecision(db3, decisionId, status2, result);
49793
49818
  logRoomActivity(
49794
49819
  db3,
@@ -50313,9 +50338,29 @@ var init_self_mod = __esm({
50313
50338
  function loadSkillsForAgent(db3, roomId, contextText) {
50314
50339
  const skills = getActiveSkillsForContext(db3, roomId, contextText);
50315
50340
  if (skills.length === 0) return "";
50316
- return skills.map((s) => `## Skill: ${s.name}
50341
+ const sections = [];
50342
+ let used = 0;
50343
+ for (const skill of skills.slice(0, MAX_ACTIVE_SKILLS_PER_CYCLE)) {
50344
+ const prefix = sections.length > 0 ? "\n\n---\n\n" : "";
50345
+ const full = `${prefix}## Skill: ${skill.name}
50346
+
50347
+ ${skill.content}`;
50348
+ const remaining = MAX_SKILL_CONTEXT_CHARS - used;
50349
+ if (remaining <= 0) break;
50350
+ if (full.length <= remaining) {
50351
+ sections.push(full);
50352
+ used += full.length;
50353
+ continue;
50354
+ }
50355
+ const clipped = full.slice(0, Math.max(0, remaining - 32)).trimEnd();
50356
+ if (clipped) {
50357
+ sections.push(`${clipped}
50317
50358
 
50318
- ${s.content}`).join("\n\n---\n\n");
50359
+ [truncated for cycle context]`);
50360
+ }
50361
+ break;
50362
+ }
50363
+ return sections.join("");
50319
50364
  }
50320
50365
  function createAgentSkill(db3, roomId, workerId, name, content, activationContext) {
50321
50366
  return createSkill(db3, roomId, name, content, {
@@ -50329,10 +50374,13 @@ function incrementSkillVersion(db3, skillId) {
50329
50374
  if (!skill) throw new Error(`Skill ${skillId} not found`);
50330
50375
  updateSkill(db3, skillId, { version: skill.version + 1 });
50331
50376
  }
50377
+ var MAX_ACTIVE_SKILLS_PER_CYCLE, MAX_SKILL_CONTEXT_CHARS;
50332
50378
  var init_skills = __esm({
50333
50379
  "src/shared/skills.ts"() {
50334
50380
  "use strict";
50335
50381
  init_db_queries();
50382
+ MAX_ACTIVE_SKILLS_PER_CYCLE = 8;
50383
+ MAX_SKILL_CONTEXT_CHARS = 6e3;
50336
50384
  }
50337
50385
  });
50338
50386
 
@@ -52489,7 +52537,7 @@ var server_exports = {};
52489
52537
  async function main() {
52490
52538
  const server = new McpServer({
52491
52539
  name: "quoroom",
52492
- version: true ? "0.1.27" : "0.0.0"
52540
+ version: true ? "0.1.28" : "0.0.0"
52493
52541
  });
52494
52542
  registerMemoryTools(server);
52495
52543
  registerSchedulerTools(server);
@@ -53986,6 +54034,9 @@ function checkRateLimit(result) {
53986
54034
  sessionId: result.sessionId
53987
54035
  });
53988
54036
  }
54037
+ function isCliContextOverflowError(message) {
54038
+ return /compact|compaction|context.*(window|limit|overflow|too large)|model_visible_bytes|token.*limit.*exceed/i.test(message);
54039
+ }
53989
54040
  async function runCycle(db3, roomId, worker, maxTurns, options) {
53990
54041
  logRoomActivity(
53991
54042
  db3,
@@ -54070,14 +54121,22 @@ async function runCycle(db3, roomId, worker, maxTurns, options) {
54070
54121
  ${skillContent}` : ""
54071
54122
  ].join("");
54072
54123
  const isCli = model === "claude" || model.startsWith("claude-") || model === "codex";
54124
+ const CLI_SESSION_MAX_TURNS = 20;
54073
54125
  let resumeSessionId;
54074
54126
  let previousMessages;
54075
54127
  const agentSession = getAgentSession(db3, worker.id);
54076
54128
  if (agentSession) {
54077
54129
  const updatedAt = new Date(agentSession.updatedAt);
54078
54130
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
54079
- if (updatedAt < sevenDaysAgo || agentSession.model !== model) {
54131
+ const cliSessionTooLong = isCli && !!agentSession.sessionId && agentSession.turnCount >= CLI_SESSION_MAX_TURNS;
54132
+ if (updatedAt < sevenDaysAgo || agentSession.model !== model || cliSessionTooLong) {
54080
54133
  deleteAgentSession(db3, worker.id);
54134
+ if (cliSessionTooLong) {
54135
+ logBuffer2.addSynthetic(
54136
+ "system",
54137
+ `Session rotated after ${agentSession.turnCount} cycles to avoid context overflow`
54138
+ );
54139
+ }
54081
54140
  } else if (isCli && agentSession.sessionId) {
54082
54141
  resumeSessionId = agentSession.sessionId;
54083
54142
  } else if (!isCli && agentSession.messagesJson) {
@@ -54407,7 +54466,7 @@ This is NOT optional \u2014 every cycle must produce at least one skill report.`
54407
54466
  return result2.content;
54408
54467
  }
54409
54468
  } : {};
54410
- const result = await executeAgent({
54469
+ const executeWithSession = (sessionId) => executeAgent({
54411
54470
  model,
54412
54471
  prompt,
54413
54472
  systemPrompt,
@@ -54420,7 +54479,7 @@ This is NOT optional \u2014 every cycle must produce at least one skill report.`
54420
54479
  // CLI models: bypass permission prompts for headless operation
54421
54480
  permissionMode: isCli ? "bypassPermissions" : void 0,
54422
54481
  // CLI models: pass resumeSessionId for native --resume
54423
- resumeSessionId,
54482
+ resumeSessionId: sessionId,
54424
54483
  // API models: pass conversation history + persistence callback
54425
54484
  previousMessages: isCli ? void 0 : previousMessages,
54426
54485
  onSessionUpdate: isCli ? void 0 : (msgs) => {
@@ -54429,6 +54488,16 @@ This is NOT optional \u2014 every cycle must produce at least one skill report.`
54429
54488
  },
54430
54489
  ...apiToolOpts
54431
54490
  });
54491
+ let result = await executeWithSession(resumeSessionId);
54492
+ if (isCli && result.exitCode !== 0) {
54493
+ const failure = result.output?.trim() || "";
54494
+ if (isCliContextOverflowError(failure)) {
54495
+ deleteAgentSession(db3, worker.id);
54496
+ logBuffer2.addSynthetic("system", "Session overflow detected \u2014 retrying this cycle with a fresh session");
54497
+ logBuffer2.flush();
54498
+ result = await executeWithSession(void 0);
54499
+ }
54500
+ }
54432
54501
  const rateLimitInfo = checkRateLimit(result);
54433
54502
  if (rateLimitInfo) {
54434
54503
  throw new RateLimitError(rateLimitInfo);
@@ -54448,6 +54517,13 @@ This is NOT optional \u2014 every cycle must produce at least one skill report.`
54448
54517
  worker.id
54449
54518
  );
54450
54519
  updateAgentState(db3, worker.id, "idle");
54520
+ if (isCli) {
54521
+ if (isCliContextOverflowError(errorDetail)) {
54522
+ deleteAgentSession(db3, worker.id);
54523
+ logBuffer2.addSynthetic("system", "Session reset due to context overflow \u2014 next cycle will start fresh");
54524
+ logBuffer2.flush();
54525
+ }
54526
+ }
54451
54527
  return result.output;
54452
54528
  }
54453
54529
  if (isCli && result.sessionId) {
@@ -54527,7 +54603,7 @@ var require_package = __commonJS({
54527
54603
  "package.json"(exports2, module2) {
54528
54604
  module2.exports = {
54529
54605
  name: "quoroom",
54530
- version: "0.1.27",
54606
+ version: "0.1.28",
54531
54607
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
54532
54608
  main: "./out/mcp/server.js",
54533
54609
  bin: {
@@ -55782,6 +55858,12 @@ function isRateLimitFailure(message) {
55782
55858
  const lower = message.toLowerCase();
55783
55859
  return lower.includes("rate limit") || lower.includes("limit reached") || lower.includes("too many requests") || lower.includes("insufficient_quota") || lower.includes("quota") || lower.includes("429") || lower.includes("limit") && lower.includes("reset");
55784
55860
  }
55861
+ function isTransientFailure(message, timedOut) {
55862
+ if (timedOut) return true;
55863
+ if (isRateLimitFailure(message)) return true;
55864
+ const lower = message.toLowerCase();
55865
+ return lower.includes("timed out") || lower.includes("timeout") || lower.includes("temporarily unavailable") || lower.includes("service unavailable") || lower.includes("connection reset") || lower.includes("socket hang up") || lower.includes("econnreset") || lower.includes("etimedout") || lower.includes("eai_again");
55866
+ }
55785
55867
  async function executeClerkWithFallback(options) {
55786
55868
  const preferred = options.preferredModel?.trim() || DEFAULT_CLERK_MODEL;
55787
55869
  const candidates = buildExecutionCandidates(options.db, preferred);
@@ -55820,7 +55902,7 @@ async function executeClerkWithFallback(options) {
55820
55902
  const error2 = deriveExecutionFailure(result, candidate.model);
55821
55903
  attempts.push({ model: candidate.model, error: error2 });
55822
55904
  const hasMoreCandidates = i < candidates.length - 1;
55823
- if (!hasMoreCandidates || !isRateLimitFailure(error2)) {
55905
+ if (!hasMoreCandidates || !isTransientFailure(error2, result.timedOut)) {
55824
55906
  return {
55825
55907
  ok: false,
55826
55908
  model: candidate.model,
@@ -57876,6 +57958,20 @@ function parseCursor(raw) {
57876
57958
  if (!Number.isFinite(parsed) || parsed < 0) return 0;
57877
57959
  return parsed;
57878
57960
  }
57961
+ function parseIsoMs3(raw) {
57962
+ const value = String(raw ?? "").trim();
57963
+ if (!value) return null;
57964
+ const parsed = Date.parse(value);
57965
+ return Number.isFinite(parsed) ? parsed : null;
57966
+ }
57967
+ function resolveIntervalMs(db3, key, fallbackMs, minMs, maxMs) {
57968
+ const raw = (getSetting(db3, key) ?? "").trim();
57969
+ if (!raw) return fallbackMs;
57970
+ const minutes = Number.parseInt(raw, 10);
57971
+ if (!Number.isFinite(minutes) || minutes < 0) return fallbackMs;
57972
+ if (minutes === 0) return 0;
57973
+ return Math.min(maxMs, Math.max(minMs, minutes * 6e4));
57974
+ }
57879
57975
  function getKeeperUserNumber2(db3) {
57880
57976
  const raw = (getSetting(db3, "keeper_user_number") ?? "").trim();
57881
57977
  if (!/^\d{5,6}$/.test(raw)) return null;
@@ -58022,6 +58118,20 @@ function buildKeeperDigest(db3, input, styleCursor) {
58022
58118
  );
58023
58119
  return lines.join("\n");
58024
58120
  }
58121
+ function getPendingDigestCounts(input) {
58122
+ const escalations = input.escalations.length;
58123
+ const decisions = input.decisions.length;
58124
+ const roomMessages = input.roomMessages.length;
58125
+ return {
58126
+ escalations,
58127
+ decisions,
58128
+ roomMessages,
58129
+ total: escalations + decisions + roomMessages
58130
+ };
58131
+ }
58132
+ function isUrgentDigest(counts) {
58133
+ return counts.escalations >= URGENT_ESCALATION_THRESHOLD || counts.decisions >= URGENT_DECISION_THRESHOLD || counts.total >= URGENT_TOTAL_THRESHOLD;
58134
+ }
58025
58135
  async function relayPendingKeeperRequests(db3) {
58026
58136
  if (isCloudDeployment()) return;
58027
58137
  const channels = getClerkPreferredNotifyChannels(db3);
@@ -58054,6 +58164,32 @@ async function relayPendingKeeperRequests(db3) {
58054
58164
  roomMessages: pendingRoomMessages
58055
58165
  }, digestStyleCursor);
58056
58166
  if (!content) return;
58167
+ const pendingCounts = getPendingDigestCounts({
58168
+ escalations: pendingEscalations,
58169
+ decisions: pendingDecisions,
58170
+ roomMessages: pendingRoomMessages
58171
+ });
58172
+ const minIntervalMs = resolveIntervalMs(
58173
+ db3,
58174
+ CLERK_NOTIFY_MIN_INTERVAL_MINUTES_KEY,
58175
+ DEFAULT_NOTIFY_MIN_INTERVAL_MS,
58176
+ MIN_NOTIFY_INTERVAL_MS,
58177
+ MAX_NOTIFY_INTERVAL_MS
58178
+ );
58179
+ const urgentIntervalMs = resolveIntervalMs(
58180
+ db3,
58181
+ CLERK_NOTIFY_URGENT_MIN_INTERVAL_MINUTES_KEY,
58182
+ DEFAULT_NOTIFY_URGENT_MIN_INTERVAL_MS,
58183
+ MIN_URGENT_INTERVAL_MS,
58184
+ MAX_URGENT_INTERVAL_MS
58185
+ );
58186
+ const lastSentMs = parseIsoMs3(getSetting(db3, CLERK_NOTIFY_LAST_SENT_AT_KEY));
58187
+ if (lastSentMs != null) {
58188
+ const elapsedMs = Date.now() - lastSentMs;
58189
+ const regularAllowed = minIntervalMs <= 0 || elapsedMs >= minIntervalMs;
58190
+ const urgentAllowed = isUrgentDigest(pendingCounts) && (urgentIntervalMs <= 0 || elapsedMs >= urgentIntervalMs);
58191
+ if (!regularAllowed && !urgentAllowed) return;
58192
+ }
58057
58193
  const sent = await sendClerkAlert(db3, { content });
58058
58194
  if (!sent) return;
58059
58195
  const nextEscalationCursor = pendingEscalations.reduce((max, item) => Math.max(max, item.escalation?.id ?? 0), escalationCursor);
@@ -58063,6 +58199,7 @@ async function relayPendingKeeperRequests(db3) {
58063
58199
  setSetting(db3, CLERK_NOTIFY_DECISION_CURSOR_KEY, String(nextDecisionCursor));
58064
58200
  setSetting(db3, CLERK_NOTIFY_ROOM_MESSAGE_CURSOR_KEY, String(nextRoomMessageCursor));
58065
58201
  setSetting(db3, CLERK_NOTIFY_DIGEST_STYLE_CURSOR_KEY, String(digestStyleCursor + 1));
58202
+ setSetting(db3, CLERK_NOTIFY_LAST_SENT_AT_KEY, (/* @__PURE__ */ new Date()).toISOString());
58066
58203
  const byRoom = /* @__PURE__ */ new Map();
58067
58204
  for (const item of pendingEscalations) {
58068
58205
  if (!item.escalation) continue;
@@ -58096,7 +58233,7 @@ async function relayPendingKeeperRequests(db3) {
58096
58233
  );
58097
58234
  }
58098
58235
  }
58099
- var CLERK_NOTIFY_EMAIL_KEY, CLERK_NOTIFY_TELEGRAM_KEY, CLERK_NOTIFY_ESCALATION_CURSOR_KEY, CLERK_NOTIFY_DECISION_CURSOR_KEY, CLERK_NOTIFY_ROOM_MESSAGE_CURSOR_KEY, CLERK_NOTIFY_DIGEST_STYLE_CURSOR_KEY, DIGEST_OPENERS, ESCALATION_HEADERS, DECISION_HEADERS, ROOM_MESSAGE_HEADERS, DIGEST_CLOSERS, DIGEST_SECTION_LIMIT;
58236
+ var CLERK_NOTIFY_EMAIL_KEY, CLERK_NOTIFY_TELEGRAM_KEY, CLERK_NOTIFY_MIN_INTERVAL_MINUTES_KEY, CLERK_NOTIFY_URGENT_MIN_INTERVAL_MINUTES_KEY, CLERK_NOTIFY_ESCALATION_CURSOR_KEY, CLERK_NOTIFY_DECISION_CURSOR_KEY, CLERK_NOTIFY_ROOM_MESSAGE_CURSOR_KEY, CLERK_NOTIFY_DIGEST_STYLE_CURSOR_KEY, CLERK_NOTIFY_LAST_SENT_AT_KEY, DEFAULT_NOTIFY_MIN_INTERVAL_MS, DEFAULT_NOTIFY_URGENT_MIN_INTERVAL_MS, MIN_NOTIFY_INTERVAL_MS, MAX_NOTIFY_INTERVAL_MS, MIN_URGENT_INTERVAL_MS, MAX_URGENT_INTERVAL_MS, URGENT_ESCALATION_THRESHOLD, URGENT_DECISION_THRESHOLD, URGENT_TOTAL_THRESHOLD, DIGEST_OPENERS, ESCALATION_HEADERS, DECISION_HEADERS, ROOM_MESSAGE_HEADERS, DIGEST_CLOSERS, DIGEST_SECTION_LIMIT;
58100
58237
  var init_clerk_notifications = __esm({
58101
58238
  "src/server/clerk-notifications.ts"() {
58102
58239
  "use strict";
@@ -58106,10 +58243,22 @@ var init_clerk_notifications = __esm({
58106
58243
  init_clerk_message_events();
58107
58244
  CLERK_NOTIFY_EMAIL_KEY = "clerk_notify_email";
58108
58245
  CLERK_NOTIFY_TELEGRAM_KEY = "clerk_notify_telegram";
58246
+ CLERK_NOTIFY_MIN_INTERVAL_MINUTES_KEY = "clerk_notify_min_interval_minutes";
58247
+ CLERK_NOTIFY_URGENT_MIN_INTERVAL_MINUTES_KEY = "clerk_notify_urgent_min_interval_minutes";
58109
58248
  CLERK_NOTIFY_ESCALATION_CURSOR_KEY = "clerk_notify_escalation_cursor";
58110
58249
  CLERK_NOTIFY_DECISION_CURSOR_KEY = "clerk_notify_decision_cursor";
58111
58250
  CLERK_NOTIFY_ROOM_MESSAGE_CURSOR_KEY = "clerk_notify_room_message_cursor";
58112
58251
  CLERK_NOTIFY_DIGEST_STYLE_CURSOR_KEY = "clerk_notify_digest_style_cursor";
58252
+ CLERK_NOTIFY_LAST_SENT_AT_KEY = "clerk_notify_last_sent_at";
58253
+ DEFAULT_NOTIFY_MIN_INTERVAL_MS = 6 * 60 * 60 * 1e3;
58254
+ DEFAULT_NOTIFY_URGENT_MIN_INTERVAL_MS = 60 * 60 * 1e3;
58255
+ MIN_NOTIFY_INTERVAL_MS = 30 * 60 * 1e3;
58256
+ MAX_NOTIFY_INTERVAL_MS = 24 * 60 * 60 * 1e3;
58257
+ MIN_URGENT_INTERVAL_MS = 10 * 60 * 1e3;
58258
+ MAX_URGENT_INTERVAL_MS = 12 * 60 * 60 * 1e3;
58259
+ URGENT_ESCALATION_THRESHOLD = 6;
58260
+ URGENT_DECISION_THRESHOLD = 4;
58261
+ URGENT_TOTAL_THRESHOLD = 12;
58113
58262
  DIGEST_OPENERS = [
58114
58263
  (count) => `I need your call on ${count} item${count === 1 ? "" : "s"}.`,
58115
58264
  (count) => `${count} decision point${count === 1 ? "" : "s"} need${count === 1 ? "s" : ""} your direction right now.`,
@@ -60255,7 +60404,7 @@ function semverGt(a, b) {
60255
60404
  }
60256
60405
  function getCurrentVersion() {
60257
60406
  try {
60258
- return true ? "0.1.27" : null.version;
60407
+ return true ? "0.1.28" : null.version;
60259
60408
  } catch {
60260
60409
  return "0.0.0";
60261
60410
  }
@@ -60440,7 +60589,7 @@ var init_updateChecker = __esm({
60440
60589
  function getVersion3() {
60441
60590
  if (cachedVersion) return cachedVersion;
60442
60591
  try {
60443
- cachedVersion = true ? "0.1.27" : null.version;
60592
+ cachedVersion = true ? "0.1.28" : null.version;
60444
60593
  } catch {
60445
60594
  cachedVersion = "unknown";
60446
60595
  }
@@ -66869,7 +67018,7 @@ __export(update_exports, {
66869
67018
  });
66870
67019
  function getCurrentVersion2() {
66871
67020
  try {
66872
- return true ? "0.1.27" : null.version;
67021
+ return true ? "0.1.28" : null.version;
66873
67022
  } catch {
66874
67023
  return "0.0.0";
66875
67024
  }
package/out/mcp/server.js CHANGED
@@ -7466,7 +7466,6 @@ Rules:
7466
7466
  DEFAULT_ROOM_CONFIG = {
7467
7467
  threshold: "majority",
7468
7468
  timeoutMinutes: 60,
7469
- keeperWeight: "dynamic",
7470
7469
  tieBreaker: "queen",
7471
7470
  autoApprove: ["low_impact"],
7472
7471
  minCycleGapMs: 1e3,
@@ -8611,10 +8610,10 @@ function getRoomActivity(db2, roomId, limit = 50, eventTypes) {
8611
8610
  const safeLimit = clampLimit(limit, 50, 500);
8612
8611
  if (eventTypes && eventTypes.length > 0) {
8613
8612
  const placeholders = eventTypes.map(() => "?").join(", ");
8614
- const rows2 = db2.prepare(`SELECT * FROM room_activity WHERE room_id = ? AND event_type IN (${placeholders}) ORDER BY created_at DESC LIMIT ?`).all(roomId, ...eventTypes, safeLimit);
8613
+ const rows2 = db2.prepare(`SELECT * FROM room_activity WHERE room_id = ? AND event_type IN (${placeholders}) ORDER BY created_at DESC, id DESC LIMIT ?`).all(roomId, ...eventTypes, safeLimit);
8615
8614
  return rows2.map(mapRoomActivityRow);
8616
8615
  }
8617
- const rows = db2.prepare("SELECT * FROM room_activity WHERE room_id = ? ORDER BY created_at DESC LIMIT ?").all(roomId, safeLimit);
8616
+ const rows = db2.prepare("SELECT * FROM room_activity WHERE room_id = ? ORDER BY created_at DESC, id DESC LIMIT ?").all(roomId, safeLimit);
8618
8617
  return rows.map(mapRoomActivityRow);
8619
8618
  }
8620
8619
  function mapDecisionRow(row) {
@@ -8944,7 +8943,24 @@ function mapEscalationRow(row) {
8944
8943
  }
8945
8944
  function createEscalation(db2, roomId, fromAgentId, question, toAgentId) {
8946
8945
  const result = db2.prepare("INSERT INTO escalations (room_id, from_agent_id, to_agent_id, question) VALUES (?, ?, ?, ?)").run(roomId, fromAgentId, toAgentId ?? null, question);
8947
- return getEscalation(db2, result.lastInsertRowid);
8946
+ const escalation = getEscalation(db2, result.lastInsertRowid);
8947
+ const trimmedQuestion = question.trim();
8948
+ const detail = trimmedQuestion.length > 1e3 ? `${trimmedQuestion.slice(0, 1e3)}\u2026` : trimmedQuestion;
8949
+ let summary = "Message created";
8950
+ if (toAgentId == null) {
8951
+ summary = fromAgentId != null ? `Worker #${fromAgentId} sent message to keeper` : "Message sent to keeper";
8952
+ } else {
8953
+ summary = fromAgentId != null ? `Worker #${fromAgentId} sent message to worker #${toAgentId}` : `Keeper sent message to worker #${toAgentId}`;
8954
+ }
8955
+ logRoomActivity(
8956
+ db2,
8957
+ roomId,
8958
+ fromAgentId != null ? "worker" : "system",
8959
+ summary,
8960
+ detail || void 0,
8961
+ fromAgentId ?? void 0
8962
+ );
8963
+ return escalation;
8948
8964
  }
8949
8965
  function getEscalation(db2, id) {
8950
8966
  const row = db2.prepare("SELECT * FROM escalations WHERE id = ?").get(id);
@@ -8967,7 +8983,24 @@ function listEscalations(db2, roomId, status) {
8967
8983
  return rows.map(mapEscalationRow);
8968
8984
  }
8969
8985
  function resolveEscalation(db2, id, answer) {
8986
+ const escalation = getEscalation(db2, id);
8970
8987
  db2.prepare("UPDATE escalations SET answer = ?, status = 'resolved', resolved_at = datetime('now','localtime') WHERE id = ?").run(answer, id);
8988
+ if (!escalation) return;
8989
+ const trimmedAnswer = answer.trim();
8990
+ const detail = trimmedAnswer.length > 1e3 ? `${trimmedAnswer.slice(0, 1e3)}\u2026` : trimmedAnswer;
8991
+ let summary = "Message resolved";
8992
+ if (escalation.toAgentId == null && escalation.fromAgentId != null) {
8993
+ summary = `Keeper replied to worker #${escalation.fromAgentId}`;
8994
+ } else if (escalation.toAgentId != null) {
8995
+ summary = `Message resolved for worker #${escalation.toAgentId}`;
8996
+ }
8997
+ logRoomActivity(
8998
+ db2,
8999
+ escalation.roomId,
9000
+ "system",
9001
+ summary,
9002
+ detail || void 0
9003
+ );
8971
9004
  }
8972
9005
  function getRecentKeeperAnswers(db2, roomId, fromAgentId, limit = 5) {
8973
9006
  const rows = db2.prepare(
@@ -9493,6 +9526,9 @@ function mapCycleLogRow(row) {
9493
9526
  };
9494
9527
  }
9495
9528
  function createWorkerCycle(db2, workerId, roomId, model) {
9529
+ db2.prepare(
9530
+ "UPDATE worker_cycles SET status = 'failed', error_message = 'Superseded by newer cycle', finished_at = datetime('now','localtime') WHERE worker_id = ? AND status = 'running'"
9531
+ ).run(workerId);
9496
9532
  const result = db2.prepare(
9497
9533
  "INSERT INTO worker_cycles (worker_id, room_id, model) VALUES (?, ?, ?)"
9498
9534
  ).run(workerId, roomId, model);
@@ -9514,7 +9550,7 @@ function completeWorkerCycle(db2, cycleId, errorMessage, usage) {
9514
9550
  function listRoomCycles(db2, roomId, limit = 20) {
9515
9551
  const safeLimit = clampLimit(limit, 20, 200);
9516
9552
  const rows = db2.prepare(
9517
- "SELECT * FROM worker_cycles WHERE room_id = ? ORDER BY started_at DESC LIMIT ?"
9553
+ "SELECT * FROM worker_cycles WHERE room_id = ? ORDER BY started_at DESC, id DESC LIMIT ?"
9518
9554
  ).all(roomId, safeLimit);
9519
9555
  return rows.map(mapWorkerCycleRow);
9520
9556
  }
@@ -46565,7 +46601,6 @@ function registerRoomTools(server) {
46565
46601
  maxConcurrentTasks: external_exports.number().int().min(1).max(10).optional().describe("Maximum parallel tasks (1\u201310)"),
46566
46602
  threshold: external_exports.enum(["majority", "supermajority", "unanimous"]).optional().describe("Voting threshold for quorum decisions"),
46567
46603
  autoApprove: external_exports.array(external_exports.string()).optional().describe('Decision types to auto-approve without voting (e.g. ["low_impact"])'),
46568
- keeperWeight: external_exports.enum(["dynamic", "equal"]).optional().describe("Keeper vote weighting: dynamic (51% in small rooms) or equal"),
46569
46604
  tieBreaker: external_exports.enum(["queen", "none"]).optional().describe("Tie-breaking strategy: queen vote decides, or reject on tie"),
46570
46605
  minVoters: external_exports.number().int().min(0).max(100).optional().describe("Minimum non-abstain votes required for a decision to pass (0 = no minimum)"),
46571
46606
  sealedBallot: external_exports.boolean().optional().describe("When true, individual votes are hidden until the decision resolves"),
@@ -46580,7 +46615,6 @@ function registerRoomTools(server) {
46580
46615
  maxConcurrentTasks,
46581
46616
  threshold,
46582
46617
  autoApprove,
46583
- keeperWeight,
46584
46618
  tieBreaker,
46585
46619
  minVoters,
46586
46620
  sealedBallot,
@@ -46615,10 +46649,6 @@ function registerRoomTools(server) {
46615
46649
  configUpdates.autoApprove = autoApprove;
46616
46650
  changes.push(`autoApprove: [${room.config.autoApprove.join(",")}] \u2192 [${autoApprove.join(",")}]`);
46617
46651
  }
46618
- if (keeperWeight !== void 0) {
46619
- configUpdates.keeperWeight = keeperWeight;
46620
- changes.push(`keeperWeight: ${room.config.keeperWeight} \u2192 ${keeperWeight}`);
46621
- }
46622
46652
  if (tieBreaker !== void 0) {
46623
46653
  configUpdates.tieBreaker = tieBreaker;
46624
46654
  changes.push(`tieBreaker: ${room.config.tieBreaker} \u2192 ${tieBreaker}`);
@@ -46751,8 +46781,6 @@ function tally(db2, decisionId) {
46751
46781
  return "rejected";
46752
46782
  }
46753
46783
  }
46754
- const keeperWeightMode = room?.config.keeperWeight ?? "dynamic";
46755
- const useWeighted = keeperWeightMode === "dynamic" && voters.length <= 1;
46756
46784
  const queenWorkerId = room?.queenWorkerId ?? null;
46757
46785
  const tieBreakerMode = room?.config.tieBreaker ?? "queen";
46758
46786
  let yesWeight = 0;
@@ -46765,9 +46793,8 @@ function tally(db2, decisionId) {
46765
46793
  }
46766
46794
  const kv = decision.keeperVote;
46767
46795
  if (kv && kv !== "abstain") {
46768
- const keeperWeight = useWeighted ? 1.02 : 1;
46769
- if (kv === "yes") yesWeight += keeperWeight;
46770
- else if (kv === "no") noWeight += keeperWeight;
46796
+ if (kv === "yes") yesWeight += 1;
46797
+ else if (kv === "no") noWeight += 1;
46771
46798
  } else if (kv === "abstain") {
46772
46799
  abstainCount++;
46773
46800
  }
@@ -46794,9 +46821,7 @@ function tally(db2, decisionId) {
46794
46821
  }
46795
46822
  }
46796
46823
  }
46797
- const yesDisplay = useWeighted ? yesWeight.toFixed(2) : String(yesWeight);
46798
- const noDisplay = useWeighted ? noWeight.toFixed(2) : String(noWeight);
46799
- const result = `Yes: ${yesDisplay}, No: ${noDisplay}, Abstain: ${abstainCount}` + (useWeighted ? " (weighted)" : "");
46824
+ const result = `Yes: ${yesWeight}, No: ${noWeight}, Abstain: ${abstainCount}`;
46800
46825
  resolveDecision(db2, decisionId, status, result);
46801
46826
  logRoomActivity(
46802
46827
  db2,
@@ -49195,7 +49220,7 @@ init_db();
49195
49220
  async function main() {
49196
49221
  const server = new McpServer({
49197
49222
  name: "quoroom",
49198
- version: true ? "0.1.27" : "0.0.0"
49223
+ version: true ? "0.1.28" : "0.0.0"
49199
49224
  });
49200
49225
  registerMemoryTools(server);
49201
49226
  registerSchedulerTools(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quoroom",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Autonomous AI agent collective engine — Queen, Workers, Quorum",
5
5
  "main": "./out/mcp/server.js",
6
6
  "bin": {