quoroom 0.1.11 → 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.
@@ -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.11",
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: {
@@ -9427,7 +9427,12 @@ var require_package = __commonJS({
9427
9427
  "test:e2e:setup:headed": "npm run build && npx playwright test e2e/setup-flow.test.ts --headed --project=chromium",
9428
9428
  "test:e2e:providers": "npm run build && npx playwright test e2e/provider-flows.test.ts",
9429
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`,
9430
- 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"
9431
9436
  },
9432
9437
  dependencies: {
9433
9438
  "@huggingface/transformers": "^3.4.1",
@@ -10721,7 +10726,11 @@ var DEFAULT_ROOM_CONFIG = {
10721
10726
  keeperWeight: "dynamic",
10722
10727
  tieBreaker: "queen",
10723
10728
  autoApprove: ["low_impact"],
10724
- minCycleGapMs: 1e3
10729
+ minCycleGapMs: 1e3,
10730
+ minVoters: 0,
10731
+ sealedBallot: false,
10732
+ voterHealth: false,
10733
+ voterHealthThreshold: 0.5
10725
10734
  };
10726
10735
 
10727
10736
  // src/shared/secret-store.ts
@@ -10930,6 +10939,8 @@ function mapWorkerRow(row) {
10930
10939
  taskCount: row.task_count ?? 0,
10931
10940
  roomId: row.room_id ?? null,
10932
10941
  agentState: row.agent_state ?? "idle",
10942
+ votesCast: row.votes_cast ?? 0,
10943
+ votesMissed: row.votes_missed ?? 0,
10933
10944
  createdAt: row.created_at,
10934
10945
  updatedAt: row.updated_at
10935
10946
  };
@@ -11430,13 +11441,14 @@ function mapRoomRow(row) {
11430
11441
  queenQuietUntil: row.queen_quiet_until ?? null,
11431
11442
  config,
11432
11443
  chatSessionId: row.chat_session_id ?? null,
11444
+ inviteCode: row.invite_code ?? null,
11433
11445
  createdAt: row.created_at,
11434
11446
  updatedAt: row.updated_at
11435
11447
  };
11436
11448
  }
11437
- function createRoom(db2, name, goal, config) {
11449
+ function createRoom(db2, name, goal, config, inviteCode) {
11438
11450
  const configJson = config ? JSON.stringify({ ...DEFAULT_ROOM_CONFIG, ...config }) : JSON.stringify(DEFAULT_ROOM_CONFIG);
11439
- 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);
11440
11452
  return getRoom(db2, result.lastInsertRowid);
11441
11453
  }
11442
11454
  function getRoom(db2, id) {
@@ -11465,7 +11477,8 @@ function updateRoom(db2, id, updates) {
11465
11477
  queenMaxTurns: "queen_max_turns",
11466
11478
  queenQuietFrom: "queen_quiet_from",
11467
11479
  queenQuietUntil: "queen_quiet_until",
11468
- config: "config"
11480
+ config: "config",
11481
+ inviteCode: "invite_code"
11469
11482
  };
11470
11483
  const fields = [];
11471
11484
  const values = [];
@@ -11523,12 +11536,14 @@ function mapDecisionRow(row) {
11523
11536
  threshold: row.threshold,
11524
11537
  timeoutAt: row.timeout_at ?? null,
11525
11538
  keeperVote: row.keeper_vote ?? null,
11539
+ minVoters: row.min_voters ?? 0,
11540
+ sealed: (row.sealed ?? 0) === 1,
11526
11541
  createdAt: row.created_at,
11527
11542
  resolvedAt: row.resolved_at ?? null
11528
11543
  };
11529
11544
  }
11530
- function createDecision(db2, roomId, proposerId, proposal, decisionType, threshold = "majority", timeoutAt) {
11531
- 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);
11532
11547
  return getDecision(db2, result.lastInsertRowid);
11533
11548
  }
11534
11549
  function getDecision(db2, id) {
@@ -11572,6 +11587,28 @@ function getVotes(db2, decisionId) {
11572
11587
  const rows = db2.prepare("SELECT * FROM quorum_votes WHERE decision_id = ? ORDER BY created_at ASC").all(decisionId);
11573
11588
  return rows.map(mapVoteRow);
11574
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
+ }
11575
11612
  function mapGoalRow(row) {
11576
11613
  return {
11577
11614
  id: row.id,
@@ -21623,10 +21660,12 @@ Autonomy: You serve the room's stated goal \u2014 not any individual participant
21623
21660
 
21624
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.
21625
21662
 
21626
- 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.`;
21627
21666
  function createRoom2(db2, input) {
21628
21667
  const config = { ...DEFAULT_ROOM_CONFIG, ...input.config };
21629
- const room = createRoom(db2, input.name, input.goal, config);
21668
+ const room = createRoom(db2, input.name, input.goal, config, input.inviteCode);
21630
21669
  const queen = createWorker(db2, {
21631
21670
  name: `${input.name} Queen`,
21632
21671
  systemPrompt: input.queenSystemPrompt ?? DEFAULT_QUEEN_SYSTEM_PROMPT,
@@ -22107,7 +22146,7 @@ async function sendCloudHeartbeat(data) {
22107
22146
  });
22108
22147
  if (res.status === 401) {
22109
22148
  clearRoomToken(data.roomId);
22110
- 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 });
22111
22150
  if (!getRoomToken(data.roomId)) return;
22112
22151
  await fetch(`${getCloudApi()}/rooms/${encodeURIComponent(data.roomId)}/heartbeat`, {
22113
22152
  method: "POST",
@@ -22136,7 +22175,7 @@ function startCloudSync(opts) {
22136
22175
  const allData = opts.getHeartbeatDataForPublicRooms();
22137
22176
  for (const data of allData) {
22138
22177
  void (async () => {
22139
- 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 });
22140
22179
  await sendCloudHeartbeat(data);
22141
22180
  })();
22142
22181
  }
@@ -22332,6 +22371,19 @@ async function fetchPublicRooms() {
22332
22371
  return [];
22333
22372
  }
22334
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
+ }
22335
22387
 
22336
22388
  // src/shared/agent-executor.ts
22337
22389
  var DEFAULT_HTTP_TIMEOUT_MS = 6e4;
@@ -22839,6 +22891,7 @@ function vote(db2, decisionId, workerId, voteValue, reasoning) {
22839
22891
  throw new Error(`Decision ${decisionId} is not open for voting (status: ${decision.status})`);
22840
22892
  }
22841
22893
  const qv = castVote(db2, decisionId, workerId, voteValue, reasoning);
22894
+ incrementVotesCast(db2, workerId);
22842
22895
  const voters = getRoomVoters(db2, decision.roomId);
22843
22896
  const votes = getVotes(db2, decisionId);
22844
22897
  if (votes.length >= voters.length) {
@@ -22866,6 +22919,22 @@ function tally(db2, decisionId) {
22866
22919
  const room = getRoom(db2, decision.roomId);
22867
22920
  const votes = getVotes(db2, decisionId);
22868
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
+ }
22869
22938
  const keeperWeightMode = room?.config.keeperWeight ?? "dynamic";
22870
22939
  const useWeighted = keeperWeightMode === "dynamic" && voters.length <= 1;
22871
22940
  const queenWorkerId = room?.queenWorkerId ?? null;
@@ -22919,8 +22988,18 @@ function tally(db2, decisionId) {
22919
22988
  "decision",
22920
22989
  `Decision ${status}: ${decision.proposal} (${result})`
22921
22990
  );
22991
+ creditMissedVotes(db2, votes, voters, room);
22922
22992
  return status;
22923
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
+ }
22924
23003
  function checkExpiredDecisions(db2) {
22925
23004
  const expired = getExpiredDecisions(db2);
22926
23005
  for (const d of expired) {
@@ -23567,7 +23646,8 @@ function initCloudSync(db2) {
23567
23646
  version: version5,
23568
23647
  queenModel: queen?.model ?? null,
23569
23648
  workers: workersPerRoom.get(room.id) ?? [],
23570
- stations: stationsPerRoom.get(room.id) ?? []
23649
+ stations: stationsPerRoom.get(room.id) ?? [],
23650
+ inviteCode: room.inviteCode
23571
23651
  };
23572
23652
  });
23573
23653
  }
@@ -23583,13 +23663,14 @@ function parseLimit(raw, fallback, max) {
23583
23663
  }
23584
23664
  function registerRoomRoutes(router) {
23585
23665
  router.post("/api/rooms", (ctx) => {
23586
- const { name, goal, queenSystemPrompt, config } = ctx.body || {};
23666
+ const { name, goal, queenSystemPrompt, config, inviteCode } = ctx.body || {};
23587
23667
  if (!name || typeof name !== "string") return { status: 400, error: "name is required" };
23588
23668
  const result = createRoom2(ctx.db, {
23589
23669
  name,
23590
23670
  goal,
23591
23671
  queenSystemPrompt,
23592
- config
23672
+ config,
23673
+ inviteCode: inviteCode || void 0
23593
23674
  });
23594
23675
  const globalQueenModel = getSetting(ctx.db, "queen_model");
23595
23676
  let planDefaults;
@@ -23633,6 +23714,14 @@ function registerRoomRoutes(router) {
23633
23714
  if (!room) return { status: 404, error: "Room not found" };
23634
23715
  return { data: { cloudId: getRoomCloudId(id) } };
23635
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
+ });
23636
23725
  router.get("/api/rooms/:id/activity", (ctx) => {
23637
23726
  const roomId = Number(ctx.params.id);
23638
23727
  const limit = parseLimit(ctx.query.limit, 50, 500);
@@ -23666,6 +23755,10 @@ function registerRoomRoutes(router) {
23666
23755
  }
23667
23756
  if (body.queenQuietFrom !== void 0) updates.queenQuietFrom = body.queenQuietFrom;
23668
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
+ }
23669
23762
  updateRoom(ctx.db, roomId, updates);
23670
23763
  if (updates.goal !== void 0) {
23671
23764
  const allGoals = listGoals(ctx.db, roomId);
@@ -23973,7 +24066,9 @@ function registerDecisionRoutes(router) {
23973
24066
  body.proposal,
23974
24067
  body.decisionType,
23975
24068
  body.threshold,
23976
- body.timeoutAt
24069
+ body.timeoutAt,
24070
+ typeof body.minVoters === "number" ? body.minVoters : 0,
24071
+ body.sealed === true
23977
24072
  );
23978
24073
  eventBus.emit(`room:${roomId}`, "decision:created", decision);
23979
24074
  return { status: 201, data: decision };
@@ -24034,9 +24129,22 @@ function registerDecisionRoutes(router) {
24034
24129
  }
24035
24130
  });
24036
24131
  router.get("/api/decisions/:id/votes", (ctx) => {
24037
- 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
+ }
24038
24139
  return { data: votes };
24039
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
+ });
24040
24148
  }
24041
24149
 
24042
24150
  // src/server/runtime.ts
@@ -24783,7 +24891,8 @@ async function syncCloudRoomMessages(db2) {
24783
24891
  roomId: cloudRoomId,
24784
24892
  name: room.name,
24785
24893
  goal: room.goal ?? null,
24786
- visibility: room.visibility
24894
+ visibility: room.visibility,
24895
+ inviteCode: room.inviteCode
24787
24896
  });
24788
24897
  if (!hasToken) continue;
24789
24898
  const outbound = listRoomMessages(db2, room.id, "unread").filter((message) => message.direction === "outbound" && message.toRoomId);
@@ -25271,8 +25380,9 @@ function registerEscalationRoutes(router) {
25271
25380
  router.post("/api/rooms/:roomId/escalations", (ctx) => {
25272
25381
  const roomId = Number(ctx.params.roomId);
25273
25382
  const body = ctx.body || {};
25274
- if (!body.fromAgentId || typeof body.fromAgentId !== "number") {
25275
- 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" };
25276
25386
  }
25277
25387
  if (!body.question || typeof body.question !== "string") {
25278
25388
  return { status: 400, error: "question is required" };
@@ -25280,7 +25390,7 @@ function registerEscalationRoutes(router) {
25280
25390
  const escalation = createEscalation(
25281
25391
  ctx.db,
25282
25392
  roomId,
25283
- body.fromAgentId,
25393
+ fromAgentId,
25284
25394
  body.question,
25285
25395
  body.toAgentId
25286
25396
  );
@@ -25459,6 +25569,8 @@ CREATE TABLE IF NOT EXISTS workers (
25459
25569
  task_count INTEGER NOT NULL DEFAULT 0,
25460
25570
  room_id INTEGER,
25461
25571
  agent_state TEXT NOT NULL DEFAULT 'idle',
25572
+ votes_cast INTEGER NOT NULL DEFAULT 0,
25573
+ votes_missed INTEGER NOT NULL DEFAULT 0,
25462
25574
  created_at DATETIME DEFAULT (datetime('now','localtime')),
25463
25575
  updated_at DATETIME DEFAULT (datetime('now','localtime'))
25464
25576
  );
@@ -25481,6 +25593,7 @@ CREATE TABLE IF NOT EXISTS rooms (
25481
25593
  queen_quiet_until TEXT,
25482
25594
  config TEXT,
25483
25595
  chat_session_id TEXT,
25596
+ invite_code TEXT,
25484
25597
  created_at DATETIME DEFAULT (datetime('now','localtime')),
25485
25598
  updated_at DATETIME DEFAULT (datetime('now','localtime'))
25486
25599
  );
@@ -25665,6 +25778,8 @@ CREATE TABLE IF NOT EXISTS quorum_decisions (
25665
25778
  threshold TEXT NOT NULL DEFAULT 'majority',
25666
25779
  timeout_at DATETIME,
25667
25780
  keeper_vote TEXT,
25781
+ min_voters INTEGER NOT NULL DEFAULT 0,
25782
+ sealed INTEGER NOT NULL DEFAULT 0,
25668
25783
  created_at DATETIME DEFAULT (datetime('now','localtime')),
25669
25784
  resolved_at DATETIME
25670
25785
  );
@@ -25850,6 +25965,24 @@ INSERT OR IGNORE INTO schema_version (version) VALUES (1);
25850
25965
  // src/shared/db-migrations.ts
25851
25966
  function runMigrations(database, log = console.log) {
25852
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
+ }
25853
25986
  log("Database schema initialized");
25854
25987
  }
25855
25988
 
@@ -25974,7 +26107,7 @@ function fetchJson(url) {
25974
26107
  });
25975
26108
  });
25976
26109
  }
25977
- async function check() {
26110
+ async function forceCheck() {
25978
26111
  try {
25979
26112
  const releases = await fetchJson(
25980
26113
  "https://api.github.com/repos/quoroom-ai/room/releases?per_page=20"
@@ -25997,9 +26130,9 @@ async function check() {
25997
26130
  function initUpdateChecker() {
25998
26131
  if (process.env.NODE_ENV === "test") return;
25999
26132
  initTimer = setTimeout(() => {
26000
- void check();
26133
+ void forceCheck();
26001
26134
  pollInterval = setInterval(() => {
26002
- void check();
26135
+ void forceCheck();
26003
26136
  }, CHECK_INTERVAL);
26004
26137
  }, INITIAL_DELAY);
26005
26138
  }
@@ -26017,7 +26150,7 @@ function getUpdateInfo() {
26017
26150
  return cached;
26018
26151
  }
26019
26152
  async function simulateUpdate() {
26020
- if (!cached) await check();
26153
+ if (!cached) await forceCheck();
26021
26154
  cached = {
26022
26155
  latestVersion: "99.0.0",
26023
26156
  releaseUrl: "https://github.com/quoroom-ai/room/releases",
@@ -26035,7 +26168,7 @@ var cachedVersion = null;
26035
26168
  function getVersion3() {
26036
26169
  if (cachedVersion) return cachedVersion;
26037
26170
  try {
26038
- cachedVersion = true ? "0.1.11" : null.version;
26171
+ cachedVersion = true ? "0.1.12" : null.version;
26039
26172
  } catch {
26040
26173
  cachedVersion = "unknown";
26041
26174
  }
@@ -26201,6 +26334,10 @@ function registerStatusRoutes(router) {
26201
26334
  await simulateUpdate();
26202
26335
  return { data: { ok: true } };
26203
26336
  });
26337
+ router.post("/api/status/check-update", async () => {
26338
+ await forceCheck();
26339
+ return { data: { updateInfo: getUpdateInfo() } };
26340
+ });
26204
26341
  router.post("/api/ollama/start", async () => {
26205
26342
  const result = await ensureOllamaRunning();
26206
26343
  resetOllamaCaches();
@@ -27039,7 +27176,7 @@ function probeProviderInstalled(provider) {
27039
27176
  return out.ok ? { installed: true, version: out.stdout || void 0 } : { installed: false };
27040
27177
  }
27041
27178
  function probeProviderConnected(provider) {
27042
- 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"]];
27043
27180
  for (const args of attempts) {
27044
27181
  const out = safeExec(provider, args);
27045
27182
  if (!out.ok) continue;
@@ -27548,7 +27685,12 @@ function createWsServer(server) {
27548
27685
  continue;
27549
27686
  }
27550
27687
  if (state.channels.has(event.channel)) {
27551
- ws.send(payload);
27688
+ ws.send(payload, (err) => {
27689
+ if (err) {
27690
+ clients.delete(ws);
27691
+ ws.terminate();
27692
+ }
27693
+ });
27552
27694
  }
27553
27695
  }
27554
27696
  });
@@ -27709,6 +27851,9 @@ function getCacheControl(filePath, ext) {
27709
27851
  if (base2 === "sw.js") return "no-cache, no-store, must-revalidate";
27710
27852
  if (ext === ".html") return "no-cache, no-store, must-revalidate";
27711
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
+ }
27712
27857
  if (normalized.includes("/assets/") && /-[A-Za-z0-9_-]{8,}\./.test(base2)) {
27713
27858
  return "public, max-age=31536000, immutable";
27714
27859
  }
@@ -27798,6 +27943,8 @@ function createApiServer(options = {}) {
27798
27943
  writeTokenFile(dataDir, token, port);
27799
27944
  }
27800
27945
  const server = import_node_http.default.createServer(async (req, res) => {
27946
+ res.on("error", () => {
27947
+ });
27801
27948
  const url = new import_node_url.URL(req.url, `http://${req.headers.host || "localhost"}`);
27802
27949
  const pathname = url.pathname;
27803
27950
  const origin = req.headers.origin;
@@ -28038,6 +28185,12 @@ function startServer(options = {}) {
28038
28185
  }
28039
28186
  });
28040
28187
  listen();
28188
+ process.on("uncaughtException", (err) => {
28189
+ console.error("[uncaughtException]", err);
28190
+ });
28191
+ process.on("unhandledRejection", (err) => {
28192
+ console.error("[unhandledRejection]", err);
28193
+ });
28041
28194
  process.on("SIGINT", () => {
28042
28195
  console.error("Shutting down...");
28043
28196
  _stopAllLoops();