@owloops/browserbird 1.4.2 → 1.4.4

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/dist/index.mjs +63 -12
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -122,8 +122,8 @@ function unknownSubcommand(subcommand, command, validCommands) {
122
122
  /** @fileoverview ASCII banner displayed on daemon startup and in help text. */
123
123
  const pkg = createRequire(import.meta.url)("../package.json");
124
124
  const buildInfo = [];
125
- buildInfo.push(`commit: ${"f6a42fafa0c1579a02218396225be1f54c24a261".substring(0, 7)}`);
126
- buildInfo.push(`built: 2026-03-11T16:54:07+04:00`);
125
+ buildInfo.push(`commit: ${"084f5f5f8a7edbc9bc8cd7fd189abda396b27f99".substring(0, 7)}`);
126
+ buildInfo.push(`built: 2026-03-11T22:42:24+04:00`);
127
127
  const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
128
128
  const VERSION = `browserbird ${pkg.version}${buildString}`;
129
129
  const BIRD = [
@@ -2764,6 +2764,18 @@ function sessionTimeoutBlocks(timeoutMs, opts) {
2764
2764
  });
2765
2765
  return blocks;
2766
2766
  }
2767
+ function sessionStopBlocks(sessionKey) {
2768
+ return [{
2769
+ type: "actions",
2770
+ elements: [{
2771
+ type: "button",
2772
+ text: plain("Stop Session"),
2773
+ action_id: "session_stop",
2774
+ value: sessionKey,
2775
+ style: "danger"
2776
+ }]
2777
+ }];
2778
+ }
2767
2779
  function busyBlocks(activeCount, maxConcurrent) {
2768
2780
  return [section("*Too many active sessions*"), context(`${activeCount}/${maxConcurrent} slots in use. Try again shortly.`)];
2769
2781
  }
@@ -3166,11 +3178,36 @@ function createHandler(client, getConfig, signal, getTeamId) {
3166
3178
  teamId,
3167
3179
  userId
3168
3180
  });
3181
+ let streamDead = false;
3169
3182
  let fullText = "";
3170
3183
  let completion;
3171
3184
  let hasError = false;
3172
3185
  let timedOut = false;
3173
3186
  let timedOutMs = 0;
3187
+ function isStreamExpired(err) {
3188
+ const msg = err instanceof Error ? err.message : String(err);
3189
+ return msg.includes("not_in_streaming_state") || msg.includes("streaming");
3190
+ }
3191
+ async function safeAppend(content) {
3192
+ if (streamDead) return;
3193
+ try {
3194
+ await streamer.append(content);
3195
+ } catch (err) {
3196
+ if (isStreamExpired(err)) {
3197
+ streamDead = true;
3198
+ logger.warn("slack stream expired, falling back to regular messages");
3199
+ } else throw err;
3200
+ }
3201
+ }
3202
+ async function safeStop(opts) {
3203
+ if (streamDead) return;
3204
+ try {
3205
+ await streamer.stop(opts);
3206
+ } catch (err) {
3207
+ if (isStreamExpired(err)) streamDead = true;
3208
+ else throw err;
3209
+ }
3210
+ }
3174
3211
  for await (const event of events) {
3175
3212
  if (signal.aborted) break;
3176
3213
  logger.debug(`stream event: ${event.type}`);
@@ -3181,7 +3218,7 @@ function createHandler(client, getConfig, signal, getTeamId) {
3181
3218
  case "text_delta": {
3182
3219
  const safe = redact(event.delta);
3183
3220
  fullText += safe;
3184
- await streamer.append({ markdown_text: safe });
3221
+ await safeAppend({ markdown_text: safe });
3185
3222
  break;
3186
3223
  }
3187
3224
  case "tool_images":
@@ -3203,7 +3240,7 @@ function createHandler(client, getConfig, signal, getTeamId) {
3203
3240
  const safeError = redact(event.error);
3204
3241
  logger.error(`agent error: ${safeError}`);
3205
3242
  insertLog("error", "spawn", safeError, channelId);
3206
- await streamer.append({ markdown_text: `\n\nError: ${safeError}` });
3243
+ await safeAppend({ markdown_text: `\n\nError: ${safeError}` });
3207
3244
  break;
3208
3245
  }
3209
3246
  case "timeout":
@@ -3214,12 +3251,12 @@ function createHandler(client, getConfig, signal, getTeamId) {
3214
3251
  }
3215
3252
  }
3216
3253
  if (timedOut) {
3217
- await streamer.stop({});
3254
+ await safeStop({});
3218
3255
  const blocks = sessionTimeoutBlocks(timedOutMs, { sessionUid });
3219
3256
  await client.postMessage(channelId, threadTs, `Session timed out after ${Math.round(timedOutMs / 6e4)} minutes.`, { blocks });
3220
3257
  } else {
3221
3258
  const footerBlocks = completion ? completionFooterBlocks(completion, hasError, meta.birdName, userId) : void 0;
3222
- await streamer.stop(footerBlocks ? { blocks: footerBlocks } : {});
3259
+ await safeStop(footerBlocks ? { blocks: footerBlocks } : {});
3223
3260
  }
3224
3261
  }
3225
3262
  async function uploadImages(images, channelId, threadTs) {
@@ -3284,6 +3321,7 @@ function createHandler(client, getConfig, signal, getTeamId) {
3284
3321
  }, signal);
3285
3322
  lock.killCurrent = kill;
3286
3323
  client.setStatus?.(channelId, threadTs, "is thinking...").catch(() => {});
3324
+ client.postEphemeral(channelId, threadTs, userId, "Session running.", { blocks: sessionStopBlocks(key) }).catch(() => {});
3287
3325
  if (isNew) {
3288
3326
  const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
3289
3327
  client.setTitle?.(channelId, threadTs, title).catch(() => {});
@@ -3328,6 +3366,13 @@ function createHandler(client, getConfig, signal, getTeamId) {
3328
3366
  function activeCount() {
3329
3367
  return activeSpawns;
3330
3368
  }
3369
+ function killSession(key) {
3370
+ const lock = locks.get(key);
3371
+ if (!lock?.killCurrent) return false;
3372
+ lock.killCurrent();
3373
+ lock.queue.length = 0;
3374
+ return true;
3375
+ }
3331
3376
  function killAll() {
3332
3377
  for (const lock of locks.values()) {
3333
3378
  lock.killCurrent?.();
@@ -3338,7 +3383,8 @@ function createHandler(client, getConfig, signal, getTeamId) {
3338
3383
  return {
3339
3384
  handle,
3340
3385
  activeCount,
3341
- killAll
3386
+ killAll,
3387
+ killSession
3342
3388
  };
3343
3389
  }
3344
3390
 
@@ -3736,14 +3782,19 @@ function createSlackChannel(getConfig, signal) {
3736
3782
  const user = body["user"]?.["id"];
3737
3783
  if (!actionsArr || !channel) return;
3738
3784
  for (const action of actionsArr) {
3739
- if (action["action_id"] !== "session_error_overflow") continue;
3740
- const selected = action["selected_option"]?.["value"];
3741
- if (!selected) continue;
3742
- if (selected.startsWith("retry:")) {
3743
- const sessionUid = selected.slice(6);
3785
+ const actionId = action["action_id"];
3786
+ if (actionId === "session_error_overflow" || actionId === "session_retry") {
3787
+ const raw = actionId === "session_error_overflow" ? action["selected_option"]?.["value"] : action["value"];
3788
+ if (!raw?.startsWith("retry:")) continue;
3789
+ const sessionUid = raw.slice(6);
3744
3790
  if (!sessionUid) continue;
3745
3791
  await handleSessionRetry(sessionUid, channel, user ?? "unknown", getConfig(), handler);
3746
3792
  }
3793
+ if (actionId === "session_stop") {
3794
+ const sessionKey = action["value"];
3795
+ if (!sessionKey) continue;
3796
+ if (handler.killSession(sessionKey)) logger.info(`session stopped by ${user}: ${sessionKey}`);
3797
+ }
3747
3798
  }
3748
3799
  }
3749
3800
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owloops/browserbird",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "AI agent orchestrator with a real browser, a cron scheduler, and a web dashboard",
5
5
  "type": "module",
6
6
  "bin": {