@tritard/waterbrother 0.16.76 → 0.16.77

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/gateway.js +114 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.76",
3
+ "version": "0.16.77",
4
4
  "description": "Waterbrother: bring-your-own-model coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
package/src/gateway.js CHANGED
@@ -419,6 +419,53 @@ function chooseExecutorAgent(project, fallbackExecutor = {}) {
419
419
  return null;
420
420
  }
421
421
 
422
+ function getAgentRuntimeKey(agent = {}) {
423
+ const provider = String(agent?.provider || "").trim();
424
+ const model = String(agent?.model || "").trim();
425
+ return provider && model ? `${provider}/${model}` : "";
426
+ }
427
+
428
+ function listRuntimeGroups(project, fallbackExecutor = {}) {
429
+ const groups = new Map();
430
+ for (const agent of listProjectAgents(project)) {
431
+ const runtime = getAgentRuntimeKey(agent);
432
+ if (!runtime) continue;
433
+ if (!groups.has(runtime)) {
434
+ groups.set(runtime, []);
435
+ }
436
+ groups.get(runtime).push(agent);
437
+ }
438
+ if (!groups.size && fallbackExecutor?.provider && fallbackExecutor?.model) {
439
+ const runtime = `${fallbackExecutor.provider}/${fallbackExecutor.model}`;
440
+ groups.set(runtime, [{
441
+ id: "",
442
+ ownerId: String(project?.activeOperator?.id || "").trim(),
443
+ ownerName: String(project?.activeOperator?.name || project?.activeOperator?.id || "").trim(),
444
+ label: String(project?.activeOperator?.name || project?.activeOperator?.id || "active terminal").trim(),
445
+ surface: String(fallbackExecutor.surface || "").trim(),
446
+ role: "executor",
447
+ provider: String(fallbackExecutor.provider || "").trim(),
448
+ model: String(fallbackExecutor.model || "").trim(),
449
+ runtimeProfile: String(fallbackExecutor.runtimeProfile || "").trim(),
450
+ sessionId: String(fallbackExecutor.hostSessionId || "").trim()
451
+ }]);
452
+ }
453
+ return [...groups.entries()].map(([runtime, agents]) => ({ runtime, agents }));
454
+ }
455
+
456
+ function summarizeRuntimeConflict(project, fallbackExecutor = {}) {
457
+ const groups = listRuntimeGroups(project, fallbackExecutor).sort((left, right) => right.agents.length - left.agents.length);
458
+ const uniqueRuntimes = groups.filter((group) => String(group.runtime || "").trim());
459
+ if (!uniqueRuntimes.length) {
460
+ return { hasConflict: false, runtimeCount: 0, groups: [] };
461
+ }
462
+ return {
463
+ hasConflict: uniqueRuntimes.length > 1,
464
+ runtimeCount: uniqueRuntimes.length,
465
+ groups: uniqueRuntimes
466
+ };
467
+ }
468
+
422
469
  function getLatestBlockingReviewPolicy(project) {
423
470
  const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
424
471
  const ordered = events
@@ -486,6 +533,7 @@ function formatTelegramSummaryMarkup({ cwd, project, chatId = "", title = "", ex
486
533
  if (project?.enabled) {
487
534
  const participants = listProjectParticipants(project);
488
535
  const agents = listProjectAgents(project);
536
+ const runtimeConflict = summarizeRuntimeConflict(project, executor);
489
537
  lines.push(`room mode: <code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`);
490
538
  lines.push(`bound chat: <code>${escapeTelegramHtml(roomLabel)}</code>`);
491
539
  lines.push(`active operator: <code>${escapeTelegramHtml(activeOperator)}</code>`);
@@ -493,6 +541,9 @@ function formatTelegramSummaryMarkup({ cwd, project, chatId = "", title = "", ex
493
541
  if (agents.length) {
494
542
  lines.push(`agents: <code>${escapeTelegramHtml(String(agents.length))}</code>`);
495
543
  }
544
+ if (runtimeConflict.runtimeCount) {
545
+ lines.push(`runtime split: <code>${escapeTelegramHtml(runtimeConflict.hasConflict ? `yes (${runtimeConflict.runtimeCount})` : "no")}</code>`);
546
+ }
496
547
  if (executor?.surface) lines.push(`executor: <code>${escapeTelegramHtml(executor.surface)}</code>`);
497
548
  if (executor?.provider && executor?.model) {
498
549
  lines.push(`runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>`);
@@ -679,9 +730,16 @@ function parseTelegramStateIntent(text = "") {
679
730
  if (/\bwho are the bots\b/.test(lower) || /\bwho are the agents\b/.test(lower) || /\bwhat bots are here\b/.test(lower) || /\bwhat agents are here\b/.test(lower)) {
680
731
  return { action: "agent-list" };
681
732
  }
733
+ if (/\bmodel conflict\b/.test(lower) || /\bruntime conflict\b/.test(lower) || /\bmodel split\b/.test(lower) || /\bruntime split\b/.test(lower) || /\bcompare models\b/.test(lower) || /\bcompare bots\b/.test(lower)) {
734
+ return { action: "model-conflict" };
735
+ }
682
736
  if (/\bwhich (?:bot|agent|terminal) should take this\b/.test(lower) || /\bwho should take this\b/.test(lower) || /\bwho should handle this\b/.test(lower)) {
683
737
  return { action: "executor-recommendation" };
684
738
  }
739
+ const agentPerspectiveMatch = value.match(/^(?:what does|how does)\s+(.+?)['’]s\s+model\s+think\??$/i);
740
+ if (agentPerspectiveMatch?.[1]) {
741
+ return { action: "agent-perspective", target: agentPerspectiveMatch[1].trim() };
742
+ }
685
743
  const agentModelMatch = value.match(/^(?:what model is|which model is|what runtime is)\s+(.+?)['’]s\s+(?:bot|terminal)(?:\s+on)?\??$/i);
686
744
  if (agentModelMatch?.[1]) {
687
745
  return { action: "agent-model", target: agentModelMatch[1].trim() };
@@ -795,6 +853,7 @@ function formatTelegramRoomMarkup(project, options = {}) {
795
853
  const agentLines = agents.length
796
854
  ? agents.map((agent) => `• ${escapeTelegramHtml(formatAgentLabel(agent) || agent.id || "agent")} <code>${escapeTelegramHtml(agent.surface || "unknown")}</code>`)
797
855
  : ["• none"];
856
+ const runtimeConflict = summarizeRuntimeConflict(project, executor);
798
857
  const executorBits = [
799
858
  `surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
800
859
  `provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
@@ -819,6 +878,16 @@ function formatTelegramRoomMarkup(project, options = {}) {
819
878
  `pending invites: <code>${pendingInviteCount}</code>`,
820
879
  "<b>Executor</b>",
821
880
  ...executorBits,
881
+ "<b>Runtime Split</b>",
882
+ runtimeConflict.runtimeCount
883
+ ? `status: <code>${escapeTelegramHtml(runtimeConflict.hasConflict ? `split (${runtimeConflict.runtimeCount})` : "aligned")}</code>`
884
+ : "status: <code>unknown</code>",
885
+ ...runtimeConflict.groups.map((group) => {
886
+ const owners = group.agents
887
+ .map((agent) => String(agent.ownerName || agent.label || agent.ownerId || agent.id || "").trim())
888
+ .filter(Boolean);
889
+ return `• <code>${escapeTelegramHtml(group.runtime)}</code>${owners.length ? ` — ${escapeTelegramHtml(owners.join(", "))}` : ""}`;
890
+ }),
822
891
  "<b>Task Summary</b>",
823
892
  `total: <code>${tasks.length}</code>`,
824
893
  ...[...byState.entries()].map(([state, count]) => `${escapeTelegramHtml(state)}: <code>${count}</code>`),
@@ -1998,6 +2067,51 @@ class TelegramGateway {
1998
2067
  ].join("\n");
1999
2068
  }
2000
2069
 
2070
+ if (intent.action === "model-conflict") {
2071
+ if (!project?.enabled) {
2072
+ return "This project is not shared.";
2073
+ }
2074
+ const summary = summarizeRuntimeConflict(project, executor);
2075
+ if (!summary.runtimeCount) {
2076
+ return "<b>Model conflict check</b>\nNo registered terminal runtime is known yet.";
2077
+ }
2078
+ const lines = [
2079
+ "<b>Model conflict check</b>",
2080
+ summary.hasConflict
2081
+ ? `This room has a runtime split across <code>${escapeTelegramHtml(String(summary.runtimeCount))}</code> models.`
2082
+ : "This room is currently aligned on one runtime."
2083
+ ];
2084
+ for (const group of summary.groups) {
2085
+ const owners = group.agents
2086
+ .map((agent) => String(agent.ownerName || agent.label || agent.ownerId || agent.id || "").trim())
2087
+ .filter(Boolean);
2088
+ lines.push(`• <code>${escapeTelegramHtml(group.runtime)}</code>${owners.length ? ` — ${escapeTelegramHtml(owners.join(", "))}` : ""}`);
2089
+ }
2090
+ if (summary.hasConflict) {
2091
+ lines.push("If you want a second opinion, assign a reviewer or ask whose bot should handle the work.");
2092
+ }
2093
+ return lines.join("\n");
2094
+ }
2095
+
2096
+ if (intent.action === "agent-perspective") {
2097
+ if (!project?.enabled) {
2098
+ return "This project is not shared.";
2099
+ }
2100
+ const agent = resolveProjectAgent(project, intent.target);
2101
+ if (!agent) {
2102
+ return `No terminal found for ${escapeTelegramHtml(intent.target || "that person")} in this room yet.`;
2103
+ }
2104
+ const runtime = agent.provider && agent.model ? `${agent.provider}/${agent.model}` : "unknown";
2105
+ return [
2106
+ "<b>Model perspective</b>",
2107
+ `person: <code>${escapeTelegramHtml(agent.ownerName || agent.ownerId || agent.label || agent.id || "-")}</code>`,
2108
+ `terminal: <code>${escapeTelegramHtml(agent.label || agent.id || "unknown")}</code>`,
2109
+ `runtime: <code>${escapeTelegramHtml(runtime)}</code>`,
2110
+ `role: <code>${escapeTelegramHtml(agent.role || "standby")}</code>`,
2111
+ "This shows which model would speak for that terminal. To get its perspective in the room, assign it as reviewer or executor."
2112
+ ].join("\n");
2113
+ }
2114
+
2001
2115
  if (intent.action === "agent-model") {
2002
2116
  if (!project?.enabled) {
2003
2117
  return "This project is not shared.";