bosun 0.35.0 β†’ 0.35.2

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/telegram-bot.mjs CHANGED
@@ -935,6 +935,80 @@ function getBrowserUiUrl() {
935
935
  return appendTokenToUrl(base, token) || base;
936
936
  }
937
937
 
938
+ function isTelegramInlineButtonUrlAllowed(inputUrl) {
939
+ try {
940
+ const parsed = new URL(String(inputUrl || "").trim());
941
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
942
+ return false;
943
+ }
944
+ const host = String(parsed.hostname || "").toLowerCase();
945
+ if (!host) return false;
946
+ // Telegram rejects localhost/loopback URLs for inline keyboard URL buttons.
947
+ if (
948
+ host === "localhost" ||
949
+ host === "127.0.0.1" ||
950
+ host === "::1" ||
951
+ host === "[::1]"
952
+ ) {
953
+ return false;
954
+ }
955
+ return true;
956
+ } catch {
957
+ return false;
958
+ }
959
+ }
960
+
961
+ function getBrowserUiUrlOptions({ forTelegramButtons = true } = {}) {
962
+ const base = String(telegramUiUrl || "").trim();
963
+ if (!base) return [];
964
+
965
+ const token = getSessionToken();
966
+ const options = [];
967
+ const seen = new Set();
968
+ const add = (label, inputUrl) => {
969
+ const raw = String(inputUrl || "").trim();
970
+ if (!raw) return;
971
+ const url = appendTokenToUrl(raw, token) || raw;
972
+ if (!url) return;
973
+ if (seen.has(url)) return;
974
+ seen.add(url);
975
+ if (forTelegramButtons && !isTelegramInlineButtonUrlAllowed(url)) {
976
+ return;
977
+ }
978
+ options.push({ label, url });
979
+ };
980
+
981
+ let parsed = null;
982
+ try {
983
+ parsed = new URL(base);
984
+ } catch {
985
+ parsed = null;
986
+ }
987
+
988
+ if (parsed) {
989
+ const localhostUrl = `${parsed.protocol}//localhost${parsed.port ? `:${parsed.port}` : ""}`;
990
+ add("πŸ–₯️ Localhost", localhostUrl);
991
+ }
992
+
993
+ if (parsed) {
994
+ const lanIp = getLocalLanIp?.();
995
+ if (lanIp && parsed.port) {
996
+ const lanUrl = `${parsed.protocol}//${lanIp}:${parsed.port}`;
997
+ add("πŸ“Ά LAN", lanUrl);
998
+ }
999
+ }
1000
+
1001
+ const tunnelUrl = getTunnelUrl();
1002
+ if (tunnelUrl) {
1003
+ add("☁️ Cloudflare", tunnelUrl);
1004
+ }
1005
+
1006
+ if (options.length === 0) {
1007
+ add("🌐 Browser URL", base);
1008
+ }
1009
+ return options;
1010
+ }
1011
+
938
1012
  function syncUiUrlsFromServer() {
939
1013
  const currentUiUrl = getTelegramUiUrl?.() || null;
940
1014
  telegramUiUrl = currentUiUrl;
@@ -947,9 +1021,10 @@ function syncUiUrlsFromServer() {
947
1021
 
948
1022
  // ── Agent session state (for follow-up steering & bottom-pinning) ────────────
949
1023
 
950
- let activeAgentSession = null; // { chatId, messageId, taskPreview, abortController, followUpQueue, ... }
951
- let agentMessageId = null; // current agent streaming message ID
952
- let agentChatId = null; // chat where agent is running
1024
+ const activeAgentSessions = new Map(); // chatId -> session
1025
+ let activeAgentSession = null; // legacy pointer to the latest active session
1026
+ let agentMessageId = null; // latest agent streaming message ID
1027
+ let agentChatId = null; // latest chat where an agent is running
953
1028
 
954
1029
  // ── Sticky UI menu state (keep /menu accessible at bottom) ─────────────────
955
1030
  const stickyMenuState = new Map();
@@ -960,7 +1035,7 @@ const STICKY_MENU_BUMP_MS = 600;
960
1035
 
961
1036
  let fastCommandQueue = Promise.resolve();
962
1037
  let commandQueue = Promise.resolve();
963
- let agentQueue = Promise.resolve();
1038
+ const agentQueues = new Map();
964
1039
 
965
1040
  function enqueueFastCommand(task) {
966
1041
  fastCommandQueue = fastCommandQueue.then(task).catch((err) => {
@@ -974,10 +1049,69 @@ function enqueueCommand(task) {
974
1049
  });
975
1050
  }
976
1051
 
977
- function enqueueAgentTask(task) {
978
- agentQueue = agentQueue.then(task).catch((err) => {
979
- console.error(`[telegram-bot] agent error: ${err.message || err}`);
980
- });
1052
+ function enqueueAgentTask(task, key = "global") {
1053
+ const queueKey = String(key || "global");
1054
+ const prev = agentQueues.get(queueKey) || Promise.resolve();
1055
+ const next = prev
1056
+ .then(task)
1057
+ .catch((err) => {
1058
+ console.error(
1059
+ `[telegram-bot] agent error (${queueKey}): ${err.message || err}`,
1060
+ );
1061
+ })
1062
+ .finally(() => {
1063
+ if (agentQueues.get(queueKey) === next) {
1064
+ agentQueues.delete(queueKey);
1065
+ }
1066
+ });
1067
+ agentQueues.set(queueKey, next);
1068
+ }
1069
+
1070
+ function getActiveAgentSession(chatId = null) {
1071
+ if (chatId != null) {
1072
+ return activeAgentSessions.get(String(chatId)) || null;
1073
+ }
1074
+ if (
1075
+ activeAgentSession &&
1076
+ activeAgentSessions.has(String(activeAgentSession.chatId || ""))
1077
+ ) {
1078
+ return activeAgentSession;
1079
+ }
1080
+ const first = activeAgentSessions.values().next().value || null;
1081
+ if (first) activeAgentSession = first;
1082
+ return first;
1083
+ }
1084
+
1085
+ function setActiveAgentSession(chatId, session) {
1086
+ const key = String(chatId || "");
1087
+ if (!key || !session) return;
1088
+ activeAgentSessions.set(key, session);
1089
+ activeAgentSession = session;
1090
+ if (session.messageId) {
1091
+ agentMessageId = session.messageId;
1092
+ agentChatId = key;
1093
+ }
1094
+ }
1095
+
1096
+ function clearActiveAgentSession(chatId, expectedSession = null) {
1097
+ const key = String(chatId || "");
1098
+ if (!key) return;
1099
+ const current = activeAgentSessions.get(key);
1100
+ if (expectedSession && current && current !== expectedSession) return;
1101
+ activeAgentSessions.delete(key);
1102
+ if (agentChatId === key) {
1103
+ agentChatId = null;
1104
+ agentMessageId = null;
1105
+ }
1106
+ if (
1107
+ activeAgentSession &&
1108
+ String(activeAgentSession.chatId || "") === key
1109
+ ) {
1110
+ activeAgentSession = null;
1111
+ }
1112
+ if (!activeAgentSession && activeAgentSessions.size > 0) {
1113
+ activeAgentSession = Array.from(activeAgentSessions.values()).pop() || null;
1114
+ }
981
1115
  }
982
1116
 
983
1117
  async function getWorkspaceRegistryCached() {
@@ -1102,28 +1236,38 @@ export function injectMonitorFunctions({
1102
1236
  * Re-sends the agent message so it stays at the bottom of the chat.
1103
1237
  */
1104
1238
  export async function bumpAgentMessage() {
1105
- if (!activeAgentSession || activeAgentSession.background) return;
1106
- if (!agentMessageId || !agentChatId) return;
1107
- try {
1108
- // Delete the old message
1109
- await deleteDirect(agentChatId, agentMessageId);
1110
- } catch {
1111
- /* best effort */
1239
+ const candidates = [];
1240
+ if (agentChatId) {
1241
+ const pinned = getActiveAgentSession(agentChatId);
1242
+ if (pinned) candidates.push(pinned);
1112
1243
  }
1113
- // Re-send at bottom
1114
- const session = activeAgentSession;
1115
- const msg = buildStreamMessage({
1116
- taskPreview: session.taskPreview,
1117
- actionLog: session.actionLog,
1118
- currentThought: session.currentThought,
1119
- totalActions: session.totalActions,
1120
- phase: session.phase,
1121
- finalResponse: null,
1122
- });
1123
- const newId = await sendDirect(agentChatId, msg);
1124
- if (newId) {
1125
- agentMessageId = newId;
1126
- session.messageId = newId;
1244
+ if (candidates.length === 0) {
1245
+ for (const session of activeAgentSessions.values()) {
1246
+ if (!session?.background) candidates.push(session);
1247
+ }
1248
+ }
1249
+ for (const session of candidates) {
1250
+ if (!session || session.background) continue;
1251
+ if (!session.messageId || !session.chatId) continue;
1252
+ try {
1253
+ await deleteDirect(session.chatId, session.messageId);
1254
+ } catch {
1255
+ /* best effort */
1256
+ }
1257
+ const msg = buildStreamMessage({
1258
+ taskPreview: session.taskPreview,
1259
+ actionLog: session.actionLog,
1260
+ currentThought: session.currentThought,
1261
+ totalActions: session.totalActions,
1262
+ phase: session.phase,
1263
+ finalResponse: null,
1264
+ });
1265
+ const newId = await sendDirect(session.chatId, msg);
1266
+ if (newId) {
1267
+ session.messageId = newId;
1268
+ agentMessageId = newId;
1269
+ agentChatId = String(session.chatId);
1270
+ }
1127
1271
  }
1128
1272
  }
1129
1273
 
@@ -1131,7 +1275,7 @@ export async function bumpAgentMessage() {
1131
1275
  * Check if agent is active (for external callers like monitor.mjs).
1132
1276
  */
1133
1277
  export function isAgentActive() {
1134
- return !!activeAgentSession;
1278
+ return activeAgentSessions.size > 0;
1135
1279
  }
1136
1280
 
1137
1281
  function setStickyMenuState(chatId, patch) {
@@ -2381,13 +2525,14 @@ async function handleUpdate(update) {
2381
2525
  return;
2382
2526
  }
2383
2527
 
2384
- // Free-text agent task runs in a separate queue so polling isn't blocked.
2385
- // If agent is already busy, handle immediately so follow-ups can be queued.
2386
- if (isPrimaryBusy()) {
2528
+ // Free-text agent task runs in a separate per-chat queue so one chat does not
2529
+ // block another. If this same chat already has an active run, handle
2530
+ // immediately so follow-ups can be queued into that run.
2531
+ if (isPrimaryBusy() && getActiveAgentSession(chatId)) {
2387
2532
  safeDetach("free-text", () => handleFreeText(text, chatId));
2388
2533
  return;
2389
2534
  }
2390
- enqueueAgentTask(() => handleFreeText(text, chatId));
2535
+ enqueueAgentTask(() => handleFreeText(text, chatId), chatId);
2391
2536
  }
2392
2537
 
2393
2538
  // ── Command Router ────────────────────────────────────────────────────────────
@@ -4320,14 +4465,12 @@ Object.assign(UI_SCREENS, {
4320
4465
  },
4321
4466
  uiButton("❌", "cb:close_menu"),
4322
4467
  ]);
4323
- if (telegramUiUrl) {
4324
- rows.unshift([
4325
- { text: "🌐 Open in Browser", url: getBrowserUiUrl() || telegramUiUrl },
4326
- ]);
4468
+ if (getBrowserUiUrlOptions().length > 0) {
4469
+ rows.unshift([uiButton("🌐 Open in Browser", uiGoAction("browser_urls"))]);
4327
4470
  }
4328
4471
  } else if (telegramUiUrl) {
4329
4472
  rows.unshift([
4330
- { text: "🌐 Open Control Center", url: getBrowserUiUrl() || telegramUiUrl },
4473
+ uiButton("🌐 Open in Browser", uiGoAction("browser_urls")),
4331
4474
  uiButton("❌", "cb:close_menu"),
4332
4475
  ]);
4333
4476
  } else {
@@ -4336,6 +4479,27 @@ Object.assign(UI_SCREENS, {
4336
4479
  return buildKeyboard(rows);
4337
4480
  },
4338
4481
  },
4482
+ browser_urls: {
4483
+ title: "Browser URLs",
4484
+ parent: "home",
4485
+ body: () => {
4486
+ const options = getBrowserUiUrlOptions();
4487
+ if (options.length === 0) {
4488
+ return "Mini App URL is not available yet.";
4489
+ }
4490
+ const lines = ["Choose a browser URL. Session token is pre-attached."];
4491
+ for (const option of options) {
4492
+ lines.push(`β€’ ${option.label}`);
4493
+ }
4494
+ return lines.join("\n");
4495
+ },
4496
+ keyboard: () => {
4497
+ const options = getBrowserUiUrlOptions();
4498
+ const rows = options.map((option) => [{ text: option.label, url: option.url }]);
4499
+ rows.push(uiNavRow("home"));
4500
+ return buildKeyboard(rows);
4501
+ },
4502
+ },
4339
4503
  overview: {
4340
4504
  title: "Dashboard",
4341
4505
  parent: "home",
@@ -6168,10 +6332,16 @@ async function cmdApp(chatId) {
6168
6332
  );
6169
6333
  return;
6170
6334
  }
6171
- const rows = [[{ text: "🌐 Open in Browser", url: getBrowserUiUrl() || uiUrl }]];
6335
+ const browserOptions = getBrowserUiUrlOptions();
6336
+ const rows = [];
6172
6337
  if (webAppUrl) {
6173
6338
  rows.unshift([{ text: "πŸ“± Open Control Center", web_app: { url: webAppUrl } }]);
6174
6339
  }
6340
+ if (browserOptions.length > 0) {
6341
+ rows.push(...browserOptions.map((option) => [{ text: option.label, url: option.url }]));
6342
+ } else {
6343
+ rows.push([{ text: "🌐 Open in Browser", url: getBrowserUiUrl() || uiUrl }]);
6344
+ }
6175
6345
  const keyboard = { inline_keyboard: rows };
6176
6346
 
6177
6347
  await sendDirect(
@@ -6399,7 +6569,7 @@ async function cmdAsk(chatId, args) {
6399
6569
  await sendReply(chatId, "Usage: /ask <prompt>");
6400
6570
  return;
6401
6571
  }
6402
- enqueueAgentTask(() => handleFreeText(prompt, chatId));
6572
+ enqueueAgentTask(() => handleFreeText(prompt, chatId), chatId);
6403
6573
  }
6404
6574
 
6405
6575
  async function cmdStatus(chatId) {
@@ -9220,7 +9390,8 @@ async function cmdBackground(chatId, args) {
9220
9390
  return;
9221
9391
  }
9222
9392
 
9223
- if (!activeAgentSession) {
9393
+ const session = getActiveAgentSession(chatId);
9394
+ if (!session) {
9224
9395
  await sendReply(
9225
9396
  chatId,
9226
9397
  "No active agent. Usage:\n/background <task>\n(background current agent with /background)",
@@ -9228,19 +9399,20 @@ async function cmdBackground(chatId, args) {
9228
9399
  return;
9229
9400
  }
9230
9401
 
9231
- activeAgentSession.background = true;
9232
- activeAgentSession.suppressEdits = true;
9402
+ session.background = true;
9403
+ session.suppressEdits = true;
9233
9404
 
9234
- if (agentMessageId && agentChatId) {
9405
+ if (session.messageId && session.chatId) {
9235
9406
  try {
9236
- await deleteDirect(agentChatId, agentMessageId);
9407
+ await deleteDirect(session.chatId, session.messageId);
9237
9408
  } catch {
9238
9409
  /* best effort */
9239
9410
  }
9240
9411
  }
9241
- agentMessageId = null;
9242
- if (activeAgentSession) {
9243
- activeAgentSession.messageId = null;
9412
+ session.messageId = null;
9413
+ if (agentChatId === String(chatId)) {
9414
+ agentMessageId = null;
9415
+ agentChatId = null;
9244
9416
  }
9245
9417
 
9246
9418
  await sendReply(
@@ -9261,25 +9433,26 @@ async function cmdStop(chatId, args) {
9261
9433
  );
9262
9434
  return;
9263
9435
  }
9264
- if (!activeAgentSession) {
9436
+ const session = getActiveAgentSession(chatId);
9437
+ if (!session) {
9265
9438
  await sendReply(chatId, "No agent is currently running.");
9266
9439
  return;
9267
9440
  }
9268
- activeAgentSession.aborted = true;
9269
- if (activeAgentSession.abortController) {
9441
+ session.aborted = true;
9442
+ if (session.abortController) {
9270
9443
  try {
9271
- activeAgentSession.abortController.abort("user_stop");
9444
+ session.abortController.abort("user_stop");
9272
9445
  } catch {
9273
9446
  /* best effort */
9274
9447
  }
9275
9448
  }
9276
- if (activeAgentSession.actionLog) {
9277
- activeAgentSession.actionLog.push({
9449
+ if (session.actionLog) {
9450
+ session.actionLog.push({
9278
9451
  icon: "πŸ›‘",
9279
9452
  text: "Stop requested by user (will halt after current step)",
9280
9453
  });
9281
- if (activeAgentSession.scheduleEdit) {
9282
- activeAgentSession.scheduleEdit();
9454
+ if (session.scheduleEdit) {
9455
+ session.scheduleEdit();
9283
9456
  }
9284
9457
  }
9285
9458
  await sendReply(chatId, "πŸ›‘ Stop signal sent. Agent will halt and wait.");
@@ -9294,7 +9467,8 @@ async function cmdSteer(chatId, steerArgs) {
9294
9467
  }
9295
9468
  const message = steerArgs.trim();
9296
9469
 
9297
- if (!activeAgentSession || !isPrimaryBusy()) {
9470
+ const session = getActiveAgentSession(chatId);
9471
+ if (!session) {
9298
9472
  await sendReply(chatId, "No active agent. Sending as a new task.");
9299
9473
  await handleFreeText(message, chatId);
9300
9474
  return;
@@ -9302,34 +9476,34 @@ async function cmdSteer(chatId, steerArgs) {
9302
9476
 
9303
9477
  const result = await steerPrimaryPrompt(message);
9304
9478
  if (result.ok) {
9305
- if (activeAgentSession.actionLog) {
9306
- activeAgentSession.actionLog.push({
9479
+ if (session.actionLog) {
9480
+ session.actionLog.push({
9307
9481
  icon: "🧭",
9308
9482
  text: `Steering update delivered (${result.mode})`,
9309
9483
  });
9310
- if (activeAgentSession.scheduleEdit) {
9311
- activeAgentSession.scheduleEdit();
9484
+ if (session.scheduleEdit) {
9485
+ session.scheduleEdit();
9312
9486
  }
9313
9487
  }
9314
9488
  await sendReply(chatId, `🧭 Steering sent (${result.mode}).`);
9315
9489
  return;
9316
9490
  }
9317
9491
 
9318
- if (!activeAgentSession.followUpQueue) {
9319
- activeAgentSession.followUpQueue = [];
9492
+ if (!session.followUpQueue) {
9493
+ session.followUpQueue = [];
9320
9494
  }
9321
- activeAgentSession.followUpQueue.push(message);
9322
- const qLen = activeAgentSession.followUpQueue.length;
9323
- if (activeAgentSession.actionLog) {
9495
+ session.followUpQueue.push(message);
9496
+ const qLen = session.followUpQueue.length;
9497
+ if (session.actionLog) {
9324
9498
  const steerStatus = result.reason || "failed";
9325
- activeAgentSession.actionLog.push({
9499
+ session.actionLog.push({
9326
9500
  icon: "🧭",
9327
9501
  text: `Steering queued (#${qLen}; steer failed: ${steerStatus})`,
9328
9502
  kind: "followup_queued",
9329
9503
  steerStatus,
9330
9504
  });
9331
- if (activeAgentSession.scheduleEdit) {
9332
- activeAgentSession.scheduleEdit();
9505
+ if (session.scheduleEdit) {
9506
+ session.scheduleEdit();
9333
9507
  }
9334
9508
  }
9335
9509
  await sendReply(chatId, `🧭 Steering queued (#${qLen}).`);
@@ -9454,13 +9628,14 @@ function buildStreamMessage({
9454
9628
  async function handleFreeText(text, chatId, options = {}) {
9455
9629
  const backgroundMode = !!options.background;
9456
9630
  const isolatedMode = !!options.isolated;
9631
+ const chatSession = getActiveAgentSession(chatId);
9457
9632
  // ── Follow-up steering: if agent is busy, queue message as follow-up ──
9458
- if (!isolatedMode && isPrimaryBusy() && activeAgentSession) {
9459
- if (!activeAgentSession.followUpQueue) {
9460
- activeAgentSession.followUpQueue = [];
9633
+ if (!isolatedMode && chatSession) {
9634
+ if (!chatSession.followUpQueue) {
9635
+ chatSession.followUpQueue = [];
9461
9636
  }
9462
- activeAgentSession.followUpQueue.push(text);
9463
- const qLen = activeAgentSession.followUpQueue.length;
9637
+ chatSession.followUpQueue.push(text);
9638
+ const qLen = chatSession.followUpQueue.length;
9464
9639
 
9465
9640
  // Try immediate steering so the in-flight run can adapt ASAP.
9466
9641
  const steerResult = await steerPrimaryPrompt(text);
@@ -9476,30 +9651,21 @@ async function handleFreeText(text, chatId, options = {}) {
9476
9651
  );
9477
9652
 
9478
9653
  // Add follow-up indicator to the streaming message
9479
- if (activeAgentSession.actionLog) {
9480
- activeAgentSession.actionLog.push({
9654
+ if (chatSession.actionLog) {
9655
+ chatSession.actionLog.push({
9481
9656
  icon: "πŸ“Œ",
9482
9657
  text: `Follow-up: "${text.length > 60 ? text.slice(0, 60) + "…" : text}" (${steerNote})`,
9483
9658
  kind: "followup_queued",
9484
9659
  steerStatus,
9485
9660
  });
9486
9661
  // Trigger an edit to show the follow-up in the streaming message
9487
- if (activeAgentSession.scheduleEdit) {
9488
- activeAgentSession.scheduleEdit();
9662
+ if (chatSession.scheduleEdit) {
9663
+ chatSession.scheduleEdit();
9489
9664
  }
9490
9665
  }
9491
9666
  return;
9492
9667
  }
9493
9668
 
9494
- // ── Block if agent is busy but no session (shouldn't happen normally) ──
9495
- if (!isolatedMode && isPrimaryBusy()) {
9496
- await sendReply(
9497
- chatId,
9498
- "⏳ Agent is executing a task. Please wait for it to finish...",
9499
- );
9500
- return;
9501
- }
9502
-
9503
9669
  const taskPreview = text.length > 60 ? text.slice(0, 60) + "…" : text;
9504
9670
 
9505
9671
  // Send the initial message and capture its ID for editing (unless background)
@@ -9548,7 +9714,7 @@ async function handleFreeText(text, chatId, options = {}) {
9548
9714
  let hadError = false;
9549
9715
 
9550
9716
  const doEdit = async () => {
9551
- if (backgroundMode || activeAgentSession?.background) return;
9717
+ if (backgroundMode || sessionState.background) return;
9552
9718
  editPending = false;
9553
9719
  const msg = buildStreamMessage({
9554
9720
  taskPreview,
@@ -9569,7 +9735,7 @@ async function handleFreeText(text, chatId, options = {}) {
9569
9735
  };
9570
9736
 
9571
9737
  const scheduleEdit = () => {
9572
- if (backgroundMode || activeAgentSession?.background) return;
9738
+ if (backgroundMode || sessionState.background) return;
9573
9739
  if (editPending) return;
9574
9740
  const now = Date.now();
9575
9741
  const elapsed = now - lastEditAt;
@@ -9585,7 +9751,7 @@ async function handleFreeText(text, chatId, options = {}) {
9585
9751
 
9586
9752
  // ── Set up agent session (enables follow-up steering & bottom-pinning) ──
9587
9753
  const abortController = new AbortController();
9588
- activeAgentSession = {
9754
+ const sessionState = {
9589
9755
  chatId,
9590
9756
  messageId,
9591
9757
  taskPreview,
@@ -9600,6 +9766,7 @@ async function handleFreeText(text, chatId, options = {}) {
9600
9766
  background: backgroundMode,
9601
9767
  suppressEdits: backgroundMode,
9602
9768
  };
9769
+ setActiveAgentSession(chatId, sessionState);
9603
9770
  agentMessageId = messageId;
9604
9771
  agentChatId = chatId;
9605
9772
 
@@ -9692,17 +9859,17 @@ async function handleFreeText(text, chatId, options = {}) {
9692
9859
 
9693
9860
  if (action.phase === "thinking") {
9694
9861
  currentThought = action.text;
9695
- if (activeAgentSession) activeAgentSession.currentThought = action.text;
9862
+ sessionState.currentThought = action.text;
9696
9863
  } else {
9697
9864
  if (action.phase === "done" || action.phase === "running") {
9698
9865
  totalActions++;
9699
- if (activeAgentSession) activeAgentSession.totalActions = totalActions;
9866
+ sessionState.totalActions = totalActions;
9700
9867
  }
9701
9868
  actionLog.push(action);
9702
9869
  // Keep thought visible while actions proceed (only clear on new non-thinking action)
9703
9870
  if (action.phase !== "thinking") {
9704
9871
  currentThought = null;
9705
- if (activeAgentSession) activeAgentSession.currentThought = null;
9872
+ sessionState.currentThought = null;
9706
9873
  }
9707
9874
  }
9708
9875
 
@@ -9714,7 +9881,7 @@ async function handleFreeText(text, chatId, options = {}) {
9714
9881
  } else {
9715
9882
  phase = "working…";
9716
9883
  }
9717
- if (activeAgentSession) activeAgentSession.phase = phase;
9884
+ sessionState.phase = phase;
9718
9885
 
9719
9886
  scheduleEdit();
9720
9887
  };
@@ -9736,8 +9903,8 @@ async function handleFreeText(text, chatId, options = {}) {
9736
9903
 
9737
9904
  // ── Process follow-up queue ───────────────────────────────────
9738
9905
  // If user sent follow-up messages while agent was working, process them now
9739
- const followUps = activeAgentSession?.followUpQueue || [];
9740
- if (followUps.length > 0 && !activeAgentSession?.aborted) {
9906
+ const followUps = sessionState.followUpQueue || [];
9907
+ if (followUps.length > 0 && !sessionState.aborted) {
9741
9908
  for (const followUp of followUps) {
9742
9909
  actionLog.push({
9743
9910
  icon: "πŸ“Œ",
@@ -9807,7 +9974,7 @@ async function handleFreeText(text, chatId, options = {}) {
9807
9974
  searchesDone: searchCount,
9808
9975
  statusIcon,
9809
9976
  });
9810
- if (backgroundMode || activeAgentSession?.background) {
9977
+ if (backgroundMode || sessionState.background) {
9811
9978
  await sendReply(chatId, finalMsg);
9812
9979
  } else {
9813
9980
  const finalMessageId = await editDirect(chatId, messageId, finalMsg);
@@ -9829,7 +9996,7 @@ async function handleFreeText(text, chatId, options = {}) {
9829
9996
  searchesDone: searchCount,
9830
9997
  statusIcon: "❌",
9831
9998
  });
9832
- if (backgroundMode || activeAgentSession?.background) {
9999
+ if (backgroundMode || sessionState.background) {
9833
10000
  await sendReply(chatId, finalMsg);
9834
10001
  } else {
9835
10002
  const finalMessageId = await editDirect(chatId, messageId, finalMsg);
@@ -9839,9 +10006,7 @@ async function handleFreeText(text, chatId, options = {}) {
9839
10006
  }
9840
10007
  } finally {
9841
10008
  // ── Clean up agent session ────────────────────────────────────
9842
- activeAgentSession = null;
9843
- agentMessageId = null;
9844
- agentChatId = null;
10009
+ clearActiveAgentSession(chatId, sessionState);
9845
10010
  }
9846
10011
  }
9847
10012
 
package/ui/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
6
6
  <meta name="color-scheme" content="dark light" />
7
7
  <title>Bosun β€” Task Orchestrator</title>
8
- <link rel="icon" href="favicon.png" type="image/png" />
8
+ <link rel="icon" href="/favicon.png" type="image/png" />
9
9
  <script>
10
10
  (function() {
11
11
  var shim = document.createElement("script");
@@ -31,9 +31,9 @@
31
31
  <link rel="preconnect" href="https://fonts.googleapis.com" />
32
32
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
33
33
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Sora:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
34
- <link rel="stylesheet" href="styles.css" />
35
- <link rel="stylesheet" href="styles/kanban.css" />
36
- <link rel="stylesheet" href="styles/sessions.css" />
34
+ <link rel="stylesheet" href="/styles.css" />
35
+ <link rel="stylesheet" href="/styles/kanban.css" />
36
+ <link rel="stylesheet" href="/styles/sessions.css" />
37
37
  <!-- Apply stored colour theme before paint to prevent flash -->
38
38
  <script>
39
39
  try {
@@ -135,7 +135,7 @@
135
135
  tick();
136
136
  });
137
137
  const loadApp = async (bust = false) => {
138
- const base = new URL("app.js", import.meta.url).toString();
138
+ const base = new URL("/app.js", window.location.origin).toString();
139
139
  const url = bust ? `${base}?v=${Date.now()}` : base;
140
140
  if (window.importShim) {
141
141
  return window.importShim(url);