quoroom 0.1.9 → 0.1.12

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-804%20passing-brightgreen)](#)
11
+ [![Tests](https://img.shields.io/badge/tests-907%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)
@@ -38,6 +38,7 @@ Official channels only:
38
38
 
39
39
  - `https://quoroom.ai`
40
40
  - `https://github.com/quoroom-ai`
41
+ - Telegram: `@quoroom_ai_bot`
41
42
 
42
43
  If you see impersonation or scam activity, report it to `hello@quoroom.ai`.
43
44
  See `TRADEMARKS.md` for full trademark usage terms.
@@ -108,6 +109,12 @@ Quoroom is an open research project exploring autonomous agent collectives. Each
108
109
 
109
110
  **Dashboard** — React SPA served directly by your local Quoroom server at `http://localhost:3700` (or your configured port). Manage rooms, agents, goals, memory, wallet — all from the browser, with local-first data storage.
110
111
 
112
+ **Cloud Mode** — Deploy to the cloud and control your room remotely. Same dashboard works in both local and cloud mode. Cloud instances auto-detect their environment, support JWT-based auth, and serve the UI over HTTPS with strict CORS. Connect your Claude or Codex subscription from the remote Settings panel.
113
+
114
+ **Inbox** — Rooms can message the keeper and other rooms. Cross-room communication with reply threading. Agents escalate decisions, share updates, or request resources from neighboring rooms.
115
+
116
+ **Credentials** — Secure credential storage for API keys and secrets. Agents list and retrieve credentials at runtime without exposing raw values in prompts or logs.
117
+
111
118
  **Auto-updates** — The server polls GitHub for new releases every 4 hours. When a new version is available, the dashboard shows a notification popup and a download row in Settings. One click downloads the installer for your platform directly — no browser redirect.
112
119
 
113
120
  ---
@@ -213,6 +220,7 @@ The room engine exposes an MCP server over stdio. All tools use the `quoroom_` p
213
220
  | `quoroom_pause_room` | Pause a running room |
214
221
  | `quoroom_restart_room` | Restart a paused room |
215
222
  | `quoroom_delete_room` | Delete a room |
223
+ | `quoroom_configure_room` | Update room configuration |
216
224
 
217
225
  ### Quorum
218
226
 
@@ -304,6 +312,7 @@ The room engine exposes an MCP server over stdio. All tools use the `quoroom_` p
304
312
  | `quoroom_wallet_balance` | Check on-chain balance (USDC/USDT, all chains) |
305
313
  | `quoroom_wallet_send` | Send USDC or USDT on any supported chain |
306
314
  | `quoroom_wallet_history` | View transaction history |
315
+ | `quoroom_wallet_topup` | Get wallet top-up URL |
307
316
 
308
317
  ### Identity
309
318
 
@@ -324,9 +333,32 @@ The room engine exposes an MCP server over stdio. All tools use the `quoroom_` p
324
333
  | `quoroom_station_stop` | Stop a running station |
325
334
  | `quoroom_station_exec` | Execute a command on a station |
326
335
  | `quoroom_station_logs` | View station logs |
327
- | `quoroom_station_deploy` | Deploy to a station |
328
- | `quoroom_station_domain` | Manage station domain |
329
336
  | `quoroom_station_delete` | Delete a station |
337
+ | `quoroom_station_cancel` | Cancel a pending station |
338
+ | `quoroom_station_create_crypto` | Provision a station with crypto payment |
339
+ | `quoroom_station_renew_crypto` | Renew a station with crypto payment |
340
+
341
+ ### Inbox
342
+
343
+ | Tool | Description |
344
+ |------|-------------|
345
+ | `quoroom_inbox_send_keeper` | Send a message to the keeper |
346
+ | `quoroom_inbox_send_room` | Send a message to another room |
347
+ | `quoroom_inbox_list` | List inbox messages |
348
+ | `quoroom_inbox_reply` | Reply to a room message |
349
+
350
+ ### Credentials
351
+
352
+ | Tool | Description |
353
+ |------|-------------|
354
+ | `quoroom_credentials_list` | List stored credentials |
355
+ | `quoroom_credentials_get` | Get a credential value |
356
+
357
+ ### Resources
358
+
359
+ | Tool | Description |
360
+ |------|-------------|
361
+ | `quoroom_resources_get` | Get local system resources (CPU, memory, disk) |
330
362
 
331
363
  ### Settings
332
364
 
@@ -351,6 +383,13 @@ npm run test:watch # Watch mode
351
383
  npm run test:e2e # End-to-end tests (Playwright)
352
384
  ```
353
385
 
386
+ ### Docker (cloud runtime)
387
+
388
+ ```bash
389
+ docker build -t quoroom .
390
+ docker run -p 3700:3700 quoroom
391
+ ```
392
+
354
393
  ## Releasing
355
394
 
356
395
  Use the release runbook: [`docs/RELEASE_RUNBOOK.md`](docs/RELEASE_RUNBOOK.md)
@@ -365,19 +404,19 @@ room/
365
404
  │ ├── mcp/ # MCP server (stdio)
366
405
  │ │ ├── server.ts # Tool registration
367
406
  │ │ ├── db.ts # Database initialization
368
- │ │ └── tools/ # 13 tool modules
407
+ │ │ └── tools/ # 17 tool modules
369
408
  │ ├── server/ # HTTP/WebSocket API server
370
- │ │ ├── index.ts # Server bootstrap
409
+ │ │ ├── index.ts # Server bootstrap (local + cloud mode)
371
410
  │ │ ├── router.ts # Request router
372
- │ │ ├── auth.ts # Dual-token auth + CORS
411
+ │ │ ├── auth.ts # Dual-token auth + CORS + cloud JWT
373
412
  │ │ ├── access.ts # Role-based access control
374
413
  │ │ ├── ws.ts # WebSocket real-time events
375
- │ │ └── routes/ # REST API routes
414
+ │ │ └── routes/ # REST API routes (20 modules)
376
415
  │ ├── ui/ # React SPA dashboard
377
416
  │ │ ├── App.tsx # Root component
378
- │ │ ├── components/ # UI components
417
+ │ │ ├── components/ # UI components (32 modules)
379
418
  │ │ ├── hooks/ # React hooks
380
- │ │ └── lib/ # API client, auth, WebSocket
419
+ │ │ └── lib/ # API client, auth, storage, WebSocket
381
420
  │ └── shared/ # Core engine
382
421
  │ ├── agent-loop.ts # Worker agent loop with rate limiting
383
422
  │ ├── agent-executor.ts # Claude Code CLI execution
@@ -389,15 +428,17 @@ room/
389
428
  │ ├── identity.ts # ERC-8004 on-chain identity
390
429
  │ ├── station.ts # Cloud provisioning
391
430
  │ ├── task-runner.ts # Task execution engine
431
+ │ ├── model-provider.ts # Multi-provider LLM support
432
+ │ ├── cloud-sync.ts # Cloud registration + heartbeat
392
433
  │ ├── db-queries.ts # Database query layer
393
434
  │ ├── schema.ts # SQLite schema (WAL mode)
394
435
  │ ├── embeddings.ts # Vector embeddings (all-MiniLM-L6-v2)
395
- ├── cloud-sync.ts # Cloud registration + heartbeat
396
- │ └── __tests__/ # Test suite
436
+ └── __tests__/ # Test suite (907 tests)
397
437
  ├── e2e/ # Playwright end-to-end tests
398
438
  ├── scripts/
399
439
  │ └── build-mcp.js # esbuild bundling
400
- └── docs/ # Media assets
440
+ ├── Dockerfile # Cloud runtime image
441
+ └── docs/ # Media assets + architecture docs
401
442
  ```
402
443
 
403
444
  **Tech stack**: TypeScript (strict), React, Tailwind CSS, better-sqlite3, sqlite-vec, viem, MCP SDK, HuggingFace Transformers, node-cron, zod, esbuild, Vite, Vitest
@@ -9382,7 +9382,7 @@ var require_package = __commonJS({
9382
9382
  "package.json"(exports2, module2) {
9383
9383
  module2.exports = {
9384
9384
  name: "quoroom",
9385
- version: "0.1.9",
9385
+ version: "0.1.12",
9386
9386
  description: "Autonomous AI agent collective engine \u2014 Queen, Workers, Quorum",
9387
9387
  main: "./out/mcp/server.js",
9388
9388
  bin: {
@@ -9404,22 +9404,35 @@ var require_package = __commonJS({
9404
9404
  build: "npm run typecheck && npm run build:mcp && npm run build:ui",
9405
9405
  "build:mcp": "node scripts/build-mcp.js",
9406
9406
  "build:ui": "vite build --config src/ui/vite.config.ts",
9407
- dev: `sh -c 'trap "kill 0" INT TERM EXIT; npm run dev:room & npm run dev:cloud & wait'`,
9408
- "dev:room": "QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1 npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700",
9409
- "dev:room:isolated": "QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1 npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700",
9407
+ "kill:ports": "node scripts/kill-ports.js",
9408
+ "kill:dev-ports": "npm run kill:ports -- 4700 3710",
9409
+ "dev:links": "node scripts/dev-links.js",
9410
+ dev: `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room & npm run dev:cloud & wait'`,
9411
+ "dev:room": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
9412
+ "dev:room:isolated": "sh -c 'export QUOROOM_DATA_DIR=$HOME/.quoroom-dev QUOROOM_SKIP_MCP_REGISTER=1; npm run build:mcp && npm run build:ui && node scripts/dev-server.js --port 4700'",
9410
9413
  "dev:room:shared": "npm run build:mcp && npm run build:ui && node scripts/dev-server.js",
9411
9414
  "doctor:split": "node scripts/doctor-split.js",
9412
- "dev:isolated": `sh -c 'trap "kill 0" INT TERM EXIT; npm run dev:room:isolated & npm run dev:cloud & wait'`,
9413
- "dev:cloud": "cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai' npm start",
9415
+ "dev:isolated": `sh -c 'npm run kill:dev-ports && trap "kill 0" INT TERM EXIT; npm run dev:links & npm run dev:room:isolated & npm run dev:cloud & wait'`,
9416
+ "dev:cloud": `sh -c 'npm run kill:ports -- 3710 && cd ../cloud && PORT=3710 CLOUD_PUBLIC_URL=http://127.0.0.1:3710 CLOUD_ALLOWED_ORIGINS='"'"'http://127.0.0.1:3710,http://localhost:3710,http://localhost:5173,http://127.0.0.1:5173,https://quoroom.ai,https://www.quoroom.ai,https://app.quoroom.ai'"'"' npm start'`,
9414
9417
  "dev:ui": "vite --config src/ui/vite.config.ts",
9415
9418
  "seed:style-demo": "node scripts/seed-style-demo.js",
9416
9419
  typecheck: "tsc --noEmit",
9417
9420
  test: "npm run rebuild:native:node && vitest run --pool=forks --passWithNoTests",
9418
9421
  "test:watch": "npm run rebuild:native:node && vitest --pool=forks",
9422
+ "test:quick": "npm run typecheck && npm run test",
9423
+ "test:smart-e2e": "git diff --cached --name-only | grep -qE '^(src/ui/|src/server/|e2e/|playwright)' && npm run test:e2e || echo 'No UI/server changes \u2014 skipping E2E'",
9419
9424
  "test:e2e": "npm run build && npx playwright test",
9420
9425
  "test:e2e:ui": "npm run build && npx playwright test e2e/ui.test.ts",
9426
+ "test:e2e:setup": "npm run build && npx playwright test e2e/setup-flow.test.ts",
9427
+ "test:e2e:setup:headed": "npm run build && npx playwright test e2e/setup-flow.test.ts --headed --project=chromium",
9428
+ "test:e2e:providers": "npm run build && npx playwright test e2e/provider-flows.test.ts",
9421
9429
  "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`,
9422
- prepublishOnly: "npm run build:mcp"
9430
+ prepublishOnly: "npm run build:mcp",
9431
+ "social:rotate": "node scripts/rotate-social-image.js",
9432
+ "social:rotate:dry": "node scripts/rotate-social-image.js --dry-run",
9433
+ "social:test": "node scripts/test-social-rotation.js",
9434
+ "social:cron:install": "bash scripts/install-social-rotation-cron.sh",
9435
+ "social:cron:show": "crontab -l | grep 'quoroom-social-rotate' || true"
9423
9436
  },
9424
9437
  dependencies: {
9425
9438
  "@huggingface/transformers": "^3.4.1",
@@ -10713,7 +10726,11 @@ var DEFAULT_ROOM_CONFIG = {
10713
10726
  keeperWeight: "dynamic",
10714
10727
  tieBreaker: "queen",
10715
10728
  autoApprove: ["low_impact"],
10716
- minCycleGapMs: 1e3
10729
+ minCycleGapMs: 1e3,
10730
+ minVoters: 0,
10731
+ sealedBallot: false,
10732
+ voterHealth: false,
10733
+ voterHealthThreshold: 0.5
10717
10734
  };
10718
10735
 
10719
10736
  // src/shared/secret-store.ts
@@ -10922,6 +10939,8 @@ function mapWorkerRow(row) {
10922
10939
  taskCount: row.task_count ?? 0,
10923
10940
  roomId: row.room_id ?? null,
10924
10941
  agentState: row.agent_state ?? "idle",
10942
+ votesCast: row.votes_cast ?? 0,
10943
+ votesMissed: row.votes_missed ?? 0,
10925
10944
  createdAt: row.created_at,
10926
10945
  updatedAt: row.updated_at
10927
10946
  };
@@ -11422,13 +11441,14 @@ function mapRoomRow(row) {
11422
11441
  queenQuietUntil: row.queen_quiet_until ?? null,
11423
11442
  config,
11424
11443
  chatSessionId: row.chat_session_id ?? null,
11444
+ inviteCode: row.invite_code ?? null,
11425
11445
  createdAt: row.created_at,
11426
11446
  updatedAt: row.updated_at
11427
11447
  };
11428
11448
  }
11429
- function createRoom(db2, name, goal, config) {
11449
+ function createRoom(db2, name, goal, config, inviteCode) {
11430
11450
  const configJson = config ? JSON.stringify({ ...DEFAULT_ROOM_CONFIG, ...config }) : JSON.stringify(DEFAULT_ROOM_CONFIG);
11431
- const result = db2.prepare("INSERT INTO rooms (name, goal, config) VALUES (?, ?, ?)").run(name, goal ?? null, configJson);
11451
+ const result = db2.prepare("INSERT INTO rooms (name, goal, config, invite_code) VALUES (?, ?, ?, ?)").run(name, goal ?? null, configJson, inviteCode ?? null);
11432
11452
  return getRoom(db2, result.lastInsertRowid);
11433
11453
  }
11434
11454
  function getRoom(db2, id) {
@@ -11457,7 +11477,8 @@ function updateRoom(db2, id, updates) {
11457
11477
  queenMaxTurns: "queen_max_turns",
11458
11478
  queenQuietFrom: "queen_quiet_from",
11459
11479
  queenQuietUntil: "queen_quiet_until",
11460
- config: "config"
11480
+ config: "config",
11481
+ inviteCode: "invite_code"
11461
11482
  };
11462
11483
  const fields = [];
11463
11484
  const values = [];
@@ -11515,12 +11536,14 @@ function mapDecisionRow(row) {
11515
11536
  threshold: row.threshold,
11516
11537
  timeoutAt: row.timeout_at ?? null,
11517
11538
  keeperVote: row.keeper_vote ?? null,
11539
+ minVoters: row.min_voters ?? 0,
11540
+ sealed: (row.sealed ?? 0) === 1,
11518
11541
  createdAt: row.created_at,
11519
11542
  resolvedAt: row.resolved_at ?? null
11520
11543
  };
11521
11544
  }
11522
- function createDecision(db2, roomId, proposerId, proposal, decisionType, threshold = "majority", timeoutAt) {
11523
- const result = db2.prepare("INSERT INTO quorum_decisions (room_id, proposer_id, proposal, decision_type, threshold, timeout_at) VALUES (?, ?, ?, ?, ?, ?)").run(roomId, proposerId, proposal, decisionType, threshold, timeoutAt ?? null);
11545
+ function createDecision(db2, roomId, proposerId, proposal, decisionType, threshold = "majority", timeoutAt, minVoters = 0, sealed = false) {
11546
+ const result = db2.prepare("INSERT INTO quorum_decisions (room_id, proposer_id, proposal, decision_type, threshold, timeout_at, min_voters, sealed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(roomId, proposerId, proposal, decisionType, threshold, timeoutAt ?? null, minVoters, sealed ? 1 : 0);
11524
11547
  return getDecision(db2, result.lastInsertRowid);
11525
11548
  }
11526
11549
  function getDecision(db2, id) {
@@ -11564,6 +11587,28 @@ function getVotes(db2, decisionId) {
11564
11587
  const rows = db2.prepare("SELECT * FROM quorum_votes WHERE decision_id = ? ORDER BY created_at ASC").all(decisionId);
11565
11588
  return rows.map(mapVoteRow);
11566
11589
  }
11590
+ function incrementVotesCast(db2, workerId) {
11591
+ db2.prepare("UPDATE workers SET votes_cast = votes_cast + 1 WHERE id = ?").run(workerId);
11592
+ }
11593
+ function incrementVotesMissed(db2, workerId) {
11594
+ db2.prepare("UPDATE workers SET votes_missed = votes_missed + 1 WHERE id = ?").run(workerId);
11595
+ }
11596
+ function getVoterHealth(db2, roomId, threshold = 0.5) {
11597
+ const workers = listRoomWorkers(db2, roomId);
11598
+ return workers.map((w) => {
11599
+ const total = w.votesCast + w.votesMissed;
11600
+ const rate = total === 0 ? 1 : w.votesCast / total;
11601
+ return {
11602
+ workerId: w.id,
11603
+ workerName: w.name,
11604
+ votesCast: w.votesCast,
11605
+ votesMissed: w.votesMissed,
11606
+ totalDecisions: total,
11607
+ participationRate: rate,
11608
+ isHealthy: rate >= threshold
11609
+ };
11610
+ });
11611
+ }
11567
11612
  function mapGoalRow(row) {
11568
11613
  return {
11569
11614
  id: row.id,
@@ -21615,10 +21660,12 @@ Autonomy: You serve the room's stated goal \u2014 not any individual participant
21615
21660
 
21616
21661
  Revenue: Your room has a USDC wallet. Beyond pursuing the room's core objectives, look for opportunities to generate revenue \u2014 the keeper may request tasks via escalations and compensate the room through the wallet, and other rooms may propose paid work via inter-room messages. Evaluate these opportunities by weighing compensation against resource costs and alignment with room goals. Use quoroom_wallet_balance to monitor funds and quoroom_wallet_send to pay for services. Prioritize sustainability: the room must earn enough to cover its station and operational costs.
21617
21662
 
21618
- Resource Management: Workers using ollama models run on stations (cloud compute) \u2014 they cannot execute locally. If the room's worker model is ollama and no active station exists, rent one with quoroom_station_create BEFORE creating workers or scheduling tasks. Minimum tier: small ($15/mo, 2GB RAM). Check the Stations section in your context each cycle for available compute. Maximize use of your room's stations \u2014 they are your compute power for running workers. If workers are failing with "no active station" errors, rent a station immediately.`;
21663
+ Resource Management: Workers using ollama models run on stations (cloud compute) \u2014 they cannot execute locally. If the room's worker model is ollama and no active station exists, rent one with quoroom_station_create BEFORE creating workers or scheduling tasks. Minimum tier: small ($15/mo, 2GB RAM). Check the Stations section in your context each cycle for available compute. Maximize use of your room's stations \u2014 they are your compute power for running workers. If workers are failing with "no active station" errors, rent a station immediately.
21664
+
21665
+ Network Growth: A larger network of rooms makes everyone stronger. Rooms created through your invite links become part of your network \u2014 you can exchange knowledge, discover money-making opportunities, and propose paid collaborations with them via inter-room messages. After meaningful milestones (goal completed, revenue earned, successful collaboration), consider generating an invite link with quoroom_invite_create and sharing it with the keeper via quoroom_inbox_send_keeper. Frame it around the value: more rooms in the network means more potential deals, shared insights, and collective intelligence. Don't be pushy \u2014 one mention per milestone is enough. Use quoroom_invite_network to see your network's growth.`;
21619
21666
  function createRoom2(db2, input) {
21620
21667
  const config = { ...DEFAULT_ROOM_CONFIG, ...input.config };
21621
- const room = createRoom(db2, input.name, input.goal, config);
21668
+ const room = createRoom(db2, input.name, input.goal, config, input.inviteCode);
21622
21669
  const queen = createWorker(db2, {
21623
21670
  name: `${input.name} Queen`,
21624
21671
  systemPrompt: input.queenSystemPrompt ?? DEFAULT_QUEEN_SYSTEM_PROMPT,
@@ -22099,7 +22146,7 @@ async function sendCloudHeartbeat(data) {
22099
22146
  });
22100
22147
  if (res.status === 401) {
22101
22148
  clearRoomToken(data.roomId);
22102
- await registerWithCloud({ roomId: data.roomId, name: data.name, goal: data.goal, visibility: "public" });
22149
+ await registerWithCloud({ roomId: data.roomId, name: data.name, goal: data.goal, visibility: "public", inviteCode: data.inviteCode });
22103
22150
  if (!getRoomToken(data.roomId)) return;
22104
22151
  await fetch(`${getCloudApi()}/rooms/${encodeURIComponent(data.roomId)}/heartbeat`, {
22105
22152
  method: "POST",
@@ -22128,7 +22175,7 @@ function startCloudSync(opts) {
22128
22175
  const allData = opts.getHeartbeatDataForPublicRooms();
22129
22176
  for (const data of allData) {
22130
22177
  void (async () => {
22131
- await registerWithCloud({ roomId: data.roomId, name: data.name, goal: data.goal, visibility: "public" });
22178
+ await registerWithCloud({ roomId: data.roomId, name: data.name, goal: data.goal, visibility: "public", inviteCode: data.inviteCode });
22132
22179
  await sendCloudHeartbeat(data);
22133
22180
  })();
22134
22181
  }
@@ -22324,6 +22371,19 @@ async function fetchPublicRooms() {
22324
22371
  return [];
22325
22372
  }
22326
22373
  }
22374
+ async function fetchReferredRooms(cloudRoomId) {
22375
+ try {
22376
+ const res = await fetch(`${getCloudApi()}/rooms/${encodeURIComponent(cloudRoomId)}/network`, {
22377
+ headers: cloudHeaders(cloudRoomId),
22378
+ signal: AbortSignal.timeout(1e4)
22379
+ });
22380
+ if (!res.ok) return [];
22381
+ const data = await res.json();
22382
+ return data.referredRooms ?? [];
22383
+ } catch {
22384
+ return [];
22385
+ }
22386
+ }
22327
22387
 
22328
22388
  // src/shared/agent-executor.ts
22329
22389
  var DEFAULT_HTTP_TIMEOUT_MS = 6e4;
@@ -22831,6 +22891,7 @@ function vote(db2, decisionId, workerId, voteValue, reasoning) {
22831
22891
  throw new Error(`Decision ${decisionId} is not open for voting (status: ${decision.status})`);
22832
22892
  }
22833
22893
  const qv = castVote(db2, decisionId, workerId, voteValue, reasoning);
22894
+ incrementVotesCast(db2, workerId);
22834
22895
  const voters = getRoomVoters(db2, decision.roomId);
22835
22896
  const votes = getVotes(db2, decisionId);
22836
22897
  if (votes.length >= voters.length) {
@@ -22858,6 +22919,22 @@ function tally(db2, decisionId) {
22858
22919
  const room = getRoom(db2, decision.roomId);
22859
22920
  const votes = getVotes(db2, decisionId);
22860
22921
  const voters = getRoomVoters(db2, decision.roomId);
22922
+ if (decision.minVoters > 0) {
22923
+ let nonAbstainVotes = votes.filter((v) => v.vote !== "abstain").length;
22924
+ if (decision.keeperVote && decision.keeperVote !== "abstain") nonAbstainVotes++;
22925
+ if (nonAbstainVotes < decision.minVoters) {
22926
+ const result2 = `Quorum not met: ${nonAbstainVotes} of ${decision.minVoters} minimum non-abstain votes`;
22927
+ resolveDecision(db2, decisionId, "rejected", result2);
22928
+ logRoomActivity(
22929
+ db2,
22930
+ decision.roomId,
22931
+ "decision",
22932
+ `Decision rejected (quorum): ${decision.proposal} (${result2})`
22933
+ );
22934
+ creditMissedVotes(db2, votes, voters, room);
22935
+ return "rejected";
22936
+ }
22937
+ }
22861
22938
  const keeperWeightMode = room?.config.keeperWeight ?? "dynamic";
22862
22939
  const useWeighted = keeperWeightMode === "dynamic" && voters.length <= 1;
22863
22940
  const queenWorkerId = room?.queenWorkerId ?? null;
@@ -22911,8 +22988,18 @@ function tally(db2, decisionId) {
22911
22988
  "decision",
22912
22989
  `Decision ${status}: ${decision.proposal} (${result})`
22913
22990
  );
22991
+ creditMissedVotes(db2, votes, voters, room);
22914
22992
  return status;
22915
22993
  }
22994
+ function creditMissedVotes(db2, votes, voters, room) {
22995
+ if (!room?.config.voterHealth) return;
22996
+ const votedWorkerIds = new Set(votes.map((v) => v.workerId));
22997
+ for (const voter of voters) {
22998
+ if (!votedWorkerIds.has(voter.id)) {
22999
+ incrementVotesMissed(db2, voter.id);
23000
+ }
23001
+ }
23002
+ }
22916
23003
  function checkExpiredDecisions(db2) {
22917
23004
  const expired = getExpiredDecisions(db2);
22918
23005
  for (const d of expired) {
@@ -23559,7 +23646,8 @@ function initCloudSync(db2) {
23559
23646
  version: version5,
23560
23647
  queenModel: queen?.model ?? null,
23561
23648
  workers: workersPerRoom.get(room.id) ?? [],
23562
- stations: stationsPerRoom.get(room.id) ?? []
23649
+ stations: stationsPerRoom.get(room.id) ?? [],
23650
+ inviteCode: room.inviteCode
23563
23651
  };
23564
23652
  });
23565
23653
  }
@@ -23575,13 +23663,14 @@ function parseLimit(raw, fallback, max) {
23575
23663
  }
23576
23664
  function registerRoomRoutes(router) {
23577
23665
  router.post("/api/rooms", (ctx) => {
23578
- const { name, goal, queenSystemPrompt, config } = ctx.body || {};
23666
+ const { name, goal, queenSystemPrompt, config, inviteCode } = ctx.body || {};
23579
23667
  if (!name || typeof name !== "string") return { status: 400, error: "name is required" };
23580
23668
  const result = createRoom2(ctx.db, {
23581
23669
  name,
23582
23670
  goal,
23583
23671
  queenSystemPrompt,
23584
- config
23672
+ config,
23673
+ inviteCode: inviteCode || void 0
23585
23674
  });
23586
23675
  const globalQueenModel = getSetting(ctx.db, "queen_model");
23587
23676
  let planDefaults;
@@ -23625,6 +23714,14 @@ function registerRoomRoutes(router) {
23625
23714
  if (!room) return { status: 404, error: "Room not found" };
23626
23715
  return { data: { cloudId: getRoomCloudId(id) } };
23627
23716
  });
23717
+ router.get("/api/rooms/:id/network", async (ctx) => {
23718
+ const id = Number(ctx.params.id);
23719
+ const room = getRoom(ctx.db, id);
23720
+ if (!room) return { status: 404, error: "Room not found" };
23721
+ const cloudRoomId = getRoomCloudId(id);
23722
+ const referred = await fetchReferredRooms(cloudRoomId);
23723
+ return { data: referred };
23724
+ });
23628
23725
  router.get("/api/rooms/:id/activity", (ctx) => {
23629
23726
  const roomId = Number(ctx.params.id);
23630
23727
  const limit = parseLimit(ctx.query.limit, 50, 500);
@@ -23658,6 +23755,10 @@ function registerRoomRoutes(router) {
23658
23755
  }
23659
23756
  if (body.queenQuietFrom !== void 0) updates.queenQuietFrom = body.queenQuietFrom;
23660
23757
  if (body.queenQuietUntil !== void 0) updates.queenQuietUntil = body.queenQuietUntil;
23758
+ if (body.inviteCode !== void 0) updates.inviteCode = body.inviteCode || null;
23759
+ if (body.config !== void 0 && typeof body.config === "object" && body.config !== null) {
23760
+ updates.config = { ...room.config, ...body.config };
23761
+ }
23661
23762
  updateRoom(ctx.db, roomId, updates);
23662
23763
  if (updates.goal !== void 0) {
23663
23764
  const allGoals = listGoals(ctx.db, roomId);
@@ -23965,7 +24066,9 @@ function registerDecisionRoutes(router) {
23965
24066
  body.proposal,
23966
24067
  body.decisionType,
23967
24068
  body.threshold,
23968
- body.timeoutAt
24069
+ body.timeoutAt,
24070
+ typeof body.minVoters === "number" ? body.minVoters : 0,
24071
+ body.sealed === true
23969
24072
  );
23970
24073
  eventBus.emit(`room:${roomId}`, "decision:created", decision);
23971
24074
  return { status: 201, data: decision };
@@ -24026,9 +24129,22 @@ function registerDecisionRoutes(router) {
24026
24129
  }
24027
24130
  });
24028
24131
  router.get("/api/decisions/:id/votes", (ctx) => {
24029
- const votes = getVotes(ctx.db, Number(ctx.params.id));
24132
+ const id = Number(ctx.params.id);
24133
+ const decision = getDecision(ctx.db, id);
24134
+ const votes = getVotes(ctx.db, id);
24135
+ if (decision?.sealed && decision.status === "voting") {
24136
+ const redacted = votes.map((v) => ({ ...v, vote: "sealed", reasoning: null }));
24137
+ return { data: redacted };
24138
+ }
24030
24139
  return { data: votes };
24031
24140
  });
24141
+ router.get("/api/rooms/:roomId/voter-health", (ctx) => {
24142
+ const roomId = Number(ctx.params.roomId);
24143
+ const room = getRoom(ctx.db, roomId);
24144
+ if (!room) return { status: 404, error: "Room not found" };
24145
+ const health = getVoterHealth(ctx.db, roomId, room.config.voterHealthThreshold);
24146
+ return { data: health };
24147
+ });
24032
24148
  }
24033
24149
 
24034
24150
  // src/server/runtime.ts
@@ -24775,7 +24891,8 @@ async function syncCloudRoomMessages(db2) {
24775
24891
  roomId: cloudRoomId,
24776
24892
  name: room.name,
24777
24893
  goal: room.goal ?? null,
24778
- visibility: room.visibility
24894
+ visibility: room.visibility,
24895
+ inviteCode: room.inviteCode
24779
24896
  });
24780
24897
  if (!hasToken) continue;
24781
24898
  const outbound = listRoomMessages(db2, room.id, "unread").filter((message) => message.direction === "outbound" && message.toRoomId);
@@ -25263,8 +25380,9 @@ function registerEscalationRoutes(router) {
25263
25380
  router.post("/api/rooms/:roomId/escalations", (ctx) => {
25264
25381
  const roomId = Number(ctx.params.roomId);
25265
25382
  const body = ctx.body || {};
25266
- if (!body.fromAgentId || typeof body.fromAgentId !== "number") {
25267
- return { status: 400, error: "fromAgentId is required" };
25383
+ const fromAgentId = body.fromAgentId != null ? Number(body.fromAgentId) : null;
25384
+ if (body.fromAgentId != null && (typeof body.fromAgentId !== "number" || isNaN(fromAgentId))) {
25385
+ return { status: 400, error: "fromAgentId must be a number if provided" };
25268
25386
  }
25269
25387
  if (!body.question || typeof body.question !== "string") {
25270
25388
  return { status: 400, error: "question is required" };
@@ -25272,7 +25390,7 @@ function registerEscalationRoutes(router) {
25272
25390
  const escalation = createEscalation(
25273
25391
  ctx.db,
25274
25392
  roomId,
25275
- body.fromAgentId,
25393
+ fromAgentId,
25276
25394
  body.question,
25277
25395
  body.toAgentId
25278
25396
  );
@@ -25451,6 +25569,8 @@ CREATE TABLE IF NOT EXISTS workers (
25451
25569
  task_count INTEGER NOT NULL DEFAULT 0,
25452
25570
  room_id INTEGER,
25453
25571
  agent_state TEXT NOT NULL DEFAULT 'idle',
25572
+ votes_cast INTEGER NOT NULL DEFAULT 0,
25573
+ votes_missed INTEGER NOT NULL DEFAULT 0,
25454
25574
  created_at DATETIME DEFAULT (datetime('now','localtime')),
25455
25575
  updated_at DATETIME DEFAULT (datetime('now','localtime'))
25456
25576
  );
@@ -25473,6 +25593,7 @@ CREATE TABLE IF NOT EXISTS rooms (
25473
25593
  queen_quiet_until TEXT,
25474
25594
  config TEXT,
25475
25595
  chat_session_id TEXT,
25596
+ invite_code TEXT,
25476
25597
  created_at DATETIME DEFAULT (datetime('now','localtime')),
25477
25598
  updated_at DATETIME DEFAULT (datetime('now','localtime'))
25478
25599
  );
@@ -25657,6 +25778,8 @@ CREATE TABLE IF NOT EXISTS quorum_decisions (
25657
25778
  threshold TEXT NOT NULL DEFAULT 'majority',
25658
25779
  timeout_at DATETIME,
25659
25780
  keeper_vote TEXT,
25781
+ min_voters INTEGER NOT NULL DEFAULT 0,
25782
+ sealed INTEGER NOT NULL DEFAULT 0,
25660
25783
  created_at DATETIME DEFAULT (datetime('now','localtime')),
25661
25784
  resolved_at DATETIME
25662
25785
  );
@@ -25842,6 +25965,24 @@ INSERT OR IGNORE INTO schema_version (version) VALUES (1);
25842
25965
  // src/shared/db-migrations.ts
25843
25966
  function runMigrations(database, log = console.log) {
25844
25967
  database.exec(SCHEMA);
25968
+ const cols = database.pragma("table_info(rooms)");
25969
+ if (!cols.some((c) => c.name === "invite_code")) {
25970
+ database.exec("ALTER TABLE rooms ADD COLUMN invite_code TEXT");
25971
+ }
25972
+ const decCols = database.pragma("table_info(quorum_decisions)");
25973
+ if (!decCols.some((c) => c.name === "min_voters")) {
25974
+ database.exec("ALTER TABLE quorum_decisions ADD COLUMN min_voters INTEGER NOT NULL DEFAULT 0");
25975
+ }
25976
+ if (!decCols.some((c) => c.name === "sealed")) {
25977
+ database.exec("ALTER TABLE quorum_decisions ADD COLUMN sealed INTEGER NOT NULL DEFAULT 0");
25978
+ }
25979
+ const workerCols = database.pragma("table_info(workers)");
25980
+ if (!workerCols.some((c) => c.name === "votes_cast")) {
25981
+ database.exec("ALTER TABLE workers ADD COLUMN votes_cast INTEGER NOT NULL DEFAULT 0");
25982
+ }
25983
+ if (!workerCols.some((c) => c.name === "votes_missed")) {
25984
+ database.exec("ALTER TABLE workers ADD COLUMN votes_missed INTEGER NOT NULL DEFAULT 0");
25985
+ }
25845
25986
  log("Database schema initialized");
25846
25987
  }
25847
25988
 
@@ -25966,7 +26107,7 @@ function fetchJson(url) {
25966
26107
  });
25967
26108
  });
25968
26109
  }
25969
- async function check() {
26110
+ async function forceCheck() {
25970
26111
  try {
25971
26112
  const releases = await fetchJson(
25972
26113
  "https://api.github.com/repos/quoroom-ai/room/releases?per_page=20"
@@ -25989,9 +26130,9 @@ async function check() {
25989
26130
  function initUpdateChecker() {
25990
26131
  if (process.env.NODE_ENV === "test") return;
25991
26132
  initTimer = setTimeout(() => {
25992
- void check();
26133
+ void forceCheck();
25993
26134
  pollInterval = setInterval(() => {
25994
- void check();
26135
+ void forceCheck();
25995
26136
  }, CHECK_INTERVAL);
25996
26137
  }, INITIAL_DELAY);
25997
26138
  }
@@ -26009,7 +26150,7 @@ function getUpdateInfo() {
26009
26150
  return cached;
26010
26151
  }
26011
26152
  async function simulateUpdate() {
26012
- if (!cached) await check();
26153
+ if (!cached) await forceCheck();
26013
26154
  cached = {
26014
26155
  latestVersion: "99.0.0",
26015
26156
  releaseUrl: "https://github.com/quoroom-ai/room/releases",
@@ -26027,7 +26168,7 @@ var cachedVersion = null;
26027
26168
  function getVersion3() {
26028
26169
  if (cachedVersion) return cachedVersion;
26029
26170
  try {
26030
- cachedVersion = require_package().version;
26171
+ cachedVersion = true ? "0.1.12" : null.version;
26031
26172
  } catch {
26032
26173
  cachedVersion = "unknown";
26033
26174
  }
@@ -26193,6 +26334,10 @@ function registerStatusRoutes(router) {
26193
26334
  await simulateUpdate();
26194
26335
  return { data: { ok: true } };
26195
26336
  });
26337
+ router.post("/api/status/check-update", async () => {
26338
+ await forceCheck();
26339
+ return { data: { updateInfo: getUpdateInfo() } };
26340
+ });
26196
26341
  router.post("/api/ollama/start", async () => {
26197
26342
  const result = await ensureOllamaRunning();
26198
26343
  resetOllamaCaches();
@@ -27031,7 +27176,7 @@ function probeProviderInstalled(provider) {
27031
27176
  return out.ok ? { installed: true, version: out.stdout || void 0 } : { installed: false };
27032
27177
  }
27033
27178
  function probeProviderConnected(provider) {
27034
- const attempts = provider === "codex" ? [["auth", "status"], ["login", "--status"]] : [["auth", "status"], ["login", "status"]];
27179
+ const attempts = provider === "codex" ? [["login", "status"], ["auth", "status"]] : [["auth", "status"], ["login", "status"]];
27035
27180
  for (const args of attempts) {
27036
27181
  const out = safeExec(provider, args);
27037
27182
  if (!out.ok) continue;
@@ -27540,7 +27685,12 @@ function createWsServer(server) {
27540
27685
  continue;
27541
27686
  }
27542
27687
  if (state.channels.has(event.channel)) {
27543
- ws.send(payload);
27688
+ ws.send(payload, (err) => {
27689
+ if (err) {
27690
+ clients.delete(ws);
27691
+ ws.terminate();
27692
+ }
27693
+ });
27544
27694
  }
27545
27695
  }
27546
27696
  });
@@ -27701,6 +27851,9 @@ function getCacheControl(filePath, ext) {
27701
27851
  if (base2 === "sw.js") return "no-cache, no-store, must-revalidate";
27702
27852
  if (ext === ".html") return "no-cache, no-store, must-revalidate";
27703
27853
  if (ext === ".webmanifest") return "public, max-age=3600";
27854
+ if (base2 === "social.png" || base2.startsWith("social-")) {
27855
+ return "no-cache, max-age=0, must-revalidate";
27856
+ }
27704
27857
  if (normalized.includes("/assets/") && /-[A-Za-z0-9_-]{8,}\./.test(base2)) {
27705
27858
  return "public, max-age=31536000, immutable";
27706
27859
  }
@@ -27790,6 +27943,8 @@ function createApiServer(options = {}) {
27790
27943
  writeTokenFile(dataDir, token, port);
27791
27944
  }
27792
27945
  const server = import_node_http.default.createServer(async (req, res) => {
27946
+ res.on("error", () => {
27947
+ });
27793
27948
  const url = new import_node_url.URL(req.url, `http://${req.headers.host || "localhost"}`);
27794
27949
  const pathname = url.pathname;
27795
27950
  const origin = req.headers.origin;
@@ -28030,6 +28185,12 @@ function startServer(options = {}) {
28030
28185
  }
28031
28186
  });
28032
28187
  listen();
28188
+ process.on("uncaughtException", (err) => {
28189
+ console.error("[uncaughtException]", err);
28190
+ });
28191
+ process.on("unhandledRejection", (err) => {
28192
+ console.error("[unhandledRejection]", err);
28193
+ });
28033
28194
  process.on("SIGINT", () => {
28034
28195
  console.error("Shutting down...");
28035
28196
  _stopAllLoops();