@tritard/waterbrother 0.16.78 → 0.16.80

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.78",
3
+ "version": "0.16.80",
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/cli.js CHANGED
@@ -3107,23 +3107,49 @@ async function clearTelegramBridgeHost() {
3107
3107
  async function dequeueTelegramBridgeRequest({ cwd }) {
3108
3108
  const bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3109
3109
  const activeHost = bridge.activeHost || {};
3110
- if (Number(activeHost.pid || 0) !== process.pid) {
3111
- return null;
3112
- }
3113
- if (activeHost.cwd && String(activeHost.cwd) !== String(cwd || "")) {
3114
- return null;
3115
- }
3110
+ const currentHost = Number(activeHost.pid || 0) === process.pid
3111
+ ? activeHost
3112
+ : (Array.isArray(bridge.hosts) ? bridge.hosts.find((host) => Number(host?.pid || 0) === process.pid) : null) || {};
3116
3113
  const pending = Array.isArray(bridge.pendingRequests) ? bridge.pendingRequests : [];
3117
3114
  if (!pending.length) {
3118
3115
  return null;
3119
3116
  }
3120
- const [next, ...rest] = pending;
3117
+ const nextIndex = pending.findIndex((request) => {
3118
+ const targetPid = Number(request?.targetPid || 0);
3119
+ const targetSessionId = String(request?.targetSessionId || "").trim();
3120
+ if (targetPid > 0 && targetPid !== process.pid) {
3121
+ return false;
3122
+ }
3123
+ if (targetSessionId && targetSessionId !== String(currentHost.sessionId || "").trim()) {
3124
+ return false;
3125
+ }
3126
+ if (currentHost.cwd && String(currentHost.cwd) !== String(cwd || "")) {
3127
+ return false;
3128
+ }
3129
+ return true;
3130
+ });
3131
+ if (nextIndex < 0) {
3132
+ return null;
3133
+ }
3134
+ const next = pending[nextIndex];
3135
+ const rest = pending.filter((_, index) => index !== nextIndex);
3121
3136
  bridge.pendingRequests = rest;
3122
- bridge.activeHost = {
3123
- ...activeHost,
3124
- updatedAt: new Date().toISOString()
3125
- };
3126
- bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3137
+ if (Number(currentHost.pid || 0) === process.pid) {
3138
+ const updatedHost = {
3139
+ ...currentHost,
3140
+ updatedAt: new Date().toISOString()
3141
+ };
3142
+ if (Number(activeHost.pid || 0) === process.pid) {
3143
+ bridge.activeHost = updatedHost;
3144
+ }
3145
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, updatedHost);
3146
+ } else if (Number(activeHost.pid || 0) === process.pid) {
3147
+ bridge.activeHost = {
3148
+ ...activeHost,
3149
+ updatedAt: new Date().toISOString()
3150
+ };
3151
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3152
+ }
3127
3153
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3128
3154
  return next;
3129
3155
  }
@@ -54,6 +54,9 @@ function normalizeBridgeRequest(parsed = {}) {
54
54
  sessionId: String(parsed?.sessionId || "").trim(),
55
55
  text: String(parsed?.text || "").trim(),
56
56
  explicitExecution: parsed?.explicitExecution === true,
57
+ targetPid: Number.isFinite(Number(parsed?.targetPid)) ? Math.floor(Number(parsed.targetPid)) : 0,
58
+ targetSessionId: String(parsed?.targetSessionId || "").trim(),
59
+ targetOwnerId: String(parsed?.targetOwnerId || "").trim(),
57
60
  runtimeProfile: String(parsed?.runtimeProfile || "").trim(),
58
61
  replyToMessageId: Number.isFinite(Number(parsed?.replyToMessageId)) ? Math.floor(Number(parsed.replyToMessageId)) : 0,
59
62
  requestedAt: String(parsed?.requestedAt || "").trim(),
package/src/gateway.js CHANGED
@@ -473,6 +473,16 @@ function formatBridgeHostLabel(host = {}) {
473
473
  return [owner || label, label && label !== owner ? `(${label})` : "", runtime ? `[${runtime}]` : ""].filter(Boolean).join(" ").trim();
474
474
  }
475
475
 
476
+ function findLiveHostForAgent(hosts = [], agent = {}) {
477
+ const ownerId = String(agent?.ownerId || "").trim();
478
+ const sessionId = String(agent?.sessionId || "").trim();
479
+ return (Array.isArray(hosts) ? hosts : []).find((host) => {
480
+ const hostOwnerId = String(host?.ownerId || "").trim();
481
+ const hostSessionId = String(host?.sessionId || "").trim();
482
+ return (ownerId && hostOwnerId === ownerId) || (sessionId && hostSessionId === sessionId);
483
+ }) || null;
484
+ }
485
+
476
486
  function getLatestBlockingReviewPolicy(project) {
477
487
  const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
478
488
  const ordered = events
@@ -1890,6 +1900,44 @@ class TelegramGateway {
1890
1900
  const actorId = String(message?.from?.id || "").trim();
1891
1901
  const actorName = peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || actorId;
1892
1902
  const session = await loadSession(sessionId);
1903
+ if (/\bsend to reviewer\b/.test(reply) || /\breviewer\b/.test(reply) || /\bsend review\b/.test(reply)) {
1904
+ const targetHost = continuation.context?.reviewerHost && typeof continuation.context.reviewerHost === "object"
1905
+ ? continuation.context.reviewerHost
1906
+ : null;
1907
+ if (!targetHost?.pid) {
1908
+ return {
1909
+ markup: "The assigned reviewer is not live right now. Reply with <code>override</code> or <code>reviewed</code> instead.",
1910
+ remember: {
1911
+ text: String(continuation.lastPrompt || "").trim() || "Reply with override or reviewed.",
1912
+ kind: continuation.kind,
1913
+ source: continuation.source,
1914
+ context: continuation.context || {},
1915
+ force: true
1916
+ }
1917
+ };
1918
+ }
1919
+ const reviewPrompt = String(continuation.context?.reviewPrompt || "").trim();
1920
+ await addSharedRoomNote(session.cwd || this.cwd, `${continuation.context?.ownerName || "Assigned"} blocking review dispatched to reviewer terminal`, {
1921
+ actorId,
1922
+ actorName,
1923
+ type: "review-policy-dispatched",
1924
+ meta: {
1925
+ agentId: String(continuation.context?.agentId || "").trim(),
1926
+ reviewerPid: Number(targetHost.pid || 0)
1927
+ }
1928
+ });
1929
+ const content = await this.runPromptViaBridge(message, sessionId, reviewPrompt, {
1930
+ explicitExecution: true,
1931
+ targetHost
1932
+ });
1933
+ return {
1934
+ markup: [
1935
+ "<b>Reviewer response</b>",
1936
+ `reviewer: <code>${escapeTelegramHtml(continuation.context?.ownerName || "unknown")}</code>`,
1937
+ renderTelegramChunks(content || "(no content)").join("\n\n")
1938
+ ].join("\n\n")
1939
+ };
1940
+ }
1893
1941
  if (/\boverride\b/.test(reply)) {
1894
1942
  await addSharedRoomNote(session.cwd || this.cwd, `${continuation.context?.ownerName || "Assigned"} blocking review overridden for now`, {
1895
1943
  actorId,
@@ -1925,7 +1973,7 @@ class TelegramGateway {
1925
1973
  };
1926
1974
  }
1927
1975
  return {
1928
- markup: "Reply with <code>override</code> to proceed anyway, or <code>reviewed</code> after the blocking review is complete.",
1976
+ markup: "Reply with <code>send to reviewer</code>, <code>override</code>, or <code>reviewed</code> after the blocking review is complete.",
1929
1977
  remember: {
1930
1978
  text: String(continuation.lastPrompt || "").trim() || "Reply with override or reviewed.",
1931
1979
  kind: continuation.kind,
@@ -2631,7 +2679,7 @@ class TelegramGateway {
2631
2679
  }
2632
2680
 
2633
2681
  async runPromptViaBridge(message, sessionId, promptText, options = {}) {
2634
- const host = await this.getLiveBridgeHost();
2682
+ const host = options.targetHost || await this.getLiveBridgeHost();
2635
2683
  if (!host) {
2636
2684
  return null;
2637
2685
  }
@@ -2650,6 +2698,9 @@ class TelegramGateway {
2650
2698
  sessionId: String(sessionId || "").trim(),
2651
2699
  text: String(promptText || "").trim(),
2652
2700
  explicitExecution: options.explicitExecution === true,
2701
+ targetPid: Number(host?.pid || 0),
2702
+ targetSessionId: String(host?.sessionId || "").trim(),
2703
+ targetOwnerId: String(host?.ownerId || "").trim(),
2653
2704
  runtimeProfile: String(project?.runtimeProfile || "").trim(),
2654
2705
  replyToMessageId: message.message_id,
2655
2706
  requestedAt: new Date().toISOString(),
@@ -2678,7 +2729,11 @@ class TelegramGateway {
2678
2729
  }
2679
2730
 
2680
2731
  const activeHost = nextBridge.activeHost || {};
2681
- if (Number(activeHost.pid || 0) <= 0) {
2732
+ const knownHosts = Array.isArray(nextBridge.hosts) ? nextBridge.hosts : [];
2733
+ if (Number(host?.pid || 0) > 0 && !knownHosts.some((item) => Number(item?.pid || 0) === Number(host.pid || 0))) {
2734
+ break;
2735
+ }
2736
+ if (Number(activeHost.pid || 0) <= 0 && !(Number(host?.pid || 0) > 0)) {
2682
2737
  break;
2683
2738
  }
2684
2739
  }
@@ -3797,12 +3852,26 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
3797
3852
  if (blockingReview) {
3798
3853
  const meta = blockingReview.meta && typeof blockingReview.meta === "object" ? blockingReview.meta : {};
3799
3854
  const reviewerName = String(meta.ownerName || meta.agentId || "assigned reviewer").trim();
3800
- const followUp = `Blocking review is assigned to ${reviewerName}. Reply with override to proceed anyway, or reviewed when the review is done.`;
3855
+ const reviewerAgent = listProjectAgents(operatorGate.project).find((agent) => String(agent?.id || "").trim() === String(meta.agentId || "").trim()) || null;
3856
+ const liveHosts = await this.getLiveBridgeHosts({ cwd: operatorGate.session?.cwd || this.cwd });
3857
+ const reviewerHost = reviewerAgent ? findLiveHostForAgent(liveHosts, reviewerAgent) : null;
3858
+ const reviewPrompt = [
3859
+ `Review this pending Telegram Roundtable request before execution.`,
3860
+ `Requester: ${this.describeTelegramUser(message?.from || {}).displayName || userId}`,
3861
+ `Project: ${operatorGate.project.projectName || path.basename(operatorGate.session?.cwd || this.cwd)}`,
3862
+ "",
3863
+ `Pending request:`,
3864
+ promptText
3865
+ ].join("\n");
3866
+ const followUp = reviewerHost
3867
+ ? `Blocking review is assigned to ${reviewerName}. Reply with send to reviewer to route it there, override to proceed anyway, or reviewed when the review is done.`
3868
+ : `Blocking review is assigned to ${reviewerName}. Reply with override to proceed anyway, or reviewed when the review is done.`;
3801
3869
  await this.sendMarkup(
3802
3870
  message.chat.id,
3803
3871
  [
3804
3872
  "<b>Blocking review in effect</b>",
3805
3873
  `reviewer: <code>${escapeTelegramHtml(reviewerName)}</code>`,
3874
+ reviewerHost ? `reviewer terminal: <code>${escapeTelegramHtml(formatBridgeHostLabel(reviewerHost) || reviewerHost.sessionId || "live terminal")}</code>` : "",
3806
3875
  "Execution is paused until the blocking review is cleared or overridden.",
3807
3876
  followUp
3808
3877
  ].join("\n"),
@@ -3813,12 +3882,27 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
3813
3882
  source: "review-policy-gate",
3814
3883
  context: {
3815
3884
  agentId: String(meta.agentId || "").trim(),
3816
- ownerName: reviewerName
3885
+ ownerName: reviewerName,
3886
+ reviewPrompt,
3887
+ reviewerHost: reviewerHost
3888
+ ? {
3889
+ pid: Number(reviewerHost.pid || 0),
3890
+ sessionId: String(reviewerHost.sessionId || "").trim(),
3891
+ ownerId: String(reviewerHost.ownerId || "").trim(),
3892
+ ownerName: String(reviewerHost.ownerName || "").trim(),
3893
+ label: String(reviewerHost.label || "").trim(),
3894
+ surface: String(reviewerHost.surface || "").trim(),
3895
+ provider: String(reviewerHost.provider || "").trim(),
3896
+ model: String(reviewerHost.model || "").trim(),
3897
+ runtimeProfile: String(reviewerHost.runtimeProfile || "").trim()
3898
+ }
3899
+ : null
3817
3900
  },
3818
3901
  force: true
3819
3902
  });
3820
3903
  return;
3821
3904
  }
3905
+ const liveHosts = await this.getLiveBridgeHosts({ cwd: operatorGate.session?.cwd || this.cwd });
3822
3906
  const host = await this.getLiveBridgeHost();
3823
3907
  const activeExecutor = {
3824
3908
  surface: host?.surface || (host ? "live-tui" : "telegram-fallback"),
@@ -3831,10 +3915,12 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
3831
3915
  label: host?.label || ""
3832
3916
  };
3833
3917
  const selectedExecutor = chooseExecutorAgent(operatorGate.project, activeExecutor);
3918
+ const selectedLiveHost = selectedExecutor ? findLiveHostForAgent(liveHosts, selectedExecutor) : null;
3834
3919
  if (
3835
3920
  selectedExecutor?.ownerId
3836
3921
  && activeExecutor.ownerId
3837
3922
  && String(selectedExecutor.ownerId).trim() !== String(activeExecutor.ownerId).trim()
3923
+ && !selectedLiveHost
3838
3924
  ) {
3839
3925
  const selectedName = String(selectedExecutor.ownerName || selectedExecutor.label || selectedExecutor.ownerId || "selected executor").trim();
3840
3926
  const currentName = String(activeExecutor.ownerName || activeExecutor.label || activeExecutor.ownerId || "current terminal").trim();
@@ -3871,7 +3957,10 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
3871
3957
  return;
3872
3958
  }
3873
3959
  previewMessage = await this.sendProgressMessage(message.chat.id, message.message_id);
3874
- const content = (await this.runPromptViaBridge(message, sessionId, promptText, { explicitExecution: shouldExecutePrompt }))
3960
+ const content = (await this.runPromptViaBridge(message, sessionId, promptText, {
3961
+ explicitExecution: shouldExecutePrompt,
3962
+ targetHost: selectedLiveHost || host || null
3963
+ }))
3875
3964
  ?? (await this.runPromptFallback(sessionId, promptText));
3876
3965
  await this.deliverPromptResult(message.chat.id, message.message_id, previewMessage, content);
3877
3966
  await this.rememberContinuation(message, content);