@owloops/browserbird 1.4.12 → 1.4.13

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 +31 -24
  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: ${"ce64ae50bb22317ece3c9a004cf5acbe44af2f01".substring(0, 7)}`);
126
- buildInfo.push(`built: 2026-03-12T10:01:12+04:00`);
125
+ buildInfo.push(`commit: ${"378d6983064be8a4ec44ad5e090f8f80f3d7d7b3".substring(0, 7)}`);
126
+ buildInfo.push(`built: 2026-03-12T13:37:10+04:00`);
127
127
  const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
128
128
  const VERSION = `browserbird ${pkg.version}${buildString}`;
129
129
  const BIRD = [
@@ -375,6 +375,19 @@ function refreshBrowserLock(holder) {
375
375
  function releaseBrowserLock(holder) {
376
376
  getDb().prepare("DELETE FROM browser_lock WHERE id = 1 AND holder = ?").run(holder);
377
377
  }
378
+ const LOCK_HEARTBEAT_MS = 3e4;
379
+ /**
380
+ * Acquires the browser lock and starts a heartbeat interval.
381
+ * Returns a handle with a `release()` method, or null if the lock is unavailable.
382
+ */
383
+ function acquireBrowserLockWithHeartbeat(holder, timeoutMs) {
384
+ if (!acquireBrowserLock(holder, timeoutMs)) return null;
385
+ const timer = setInterval(() => refreshBrowserLock(holder), LOCK_HEARTBEAT_MS);
386
+ return { release() {
387
+ clearInterval(timer);
388
+ releaseBrowserLock(holder);
389
+ } };
390
+ }
378
391
  /** Clears any browser lock unconditionally. Called on startup before any sessions exist. */
379
392
  function clearBrowserLock() {
380
393
  getDb().prepare("DELETE FROM browser_lock WHERE id = 1").run();
@@ -2914,7 +2927,6 @@ function truncate(text, maxLength) {
2914
2927
  //#endregion
2915
2928
  //#region src/cron/scheduler.ts
2916
2929
  const BROWSER_TOOL_PREFIX$1 = "mcp__playwright__";
2917
- const LOCK_HEARTBEAT_MS$1 = 3e4;
2918
2930
  const TICK_INTERVAL_MS = 6e4;
2919
2931
  const MAX_SCHEDULE_ERRORS = 3;
2920
2932
  const systemHandlers = /* @__PURE__ */ new Map();
@@ -2953,8 +2965,7 @@ function startScheduler(getConfig, signal, deps) {
2953
2965
  const agent = config.agents.find((a) => a.id === payload.agentId);
2954
2966
  if (!agent) throw new Error(`agent "${payload.agentId}" not found`);
2955
2967
  const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
2956
- let browserLockAcquired = false;
2957
- let heartbeatTimer;
2968
+ let browserLock = null;
2958
2969
  try {
2959
2970
  const { events } = spawnProvider(agent.provider, {
2960
2971
  message: payload.prompt,
@@ -2967,10 +2978,9 @@ function startScheduler(getConfig, signal, deps) {
2967
2978
  let result = "";
2968
2979
  let completion;
2969
2980
  for await (const event of events) if (event.type === "tool_use") {
2970
- if (needsBrowserLock && !browserLockAcquired && event.toolName.startsWith(BROWSER_TOOL_PREFIX$1)) {
2971
- if (!acquireBrowserLock(payload.cronJobUid, config.sessions.processTimeoutMs)) throw new Error("browser is locked by another session");
2972
- browserLockAcquired = true;
2973
- heartbeatTimer = setInterval(() => refreshBrowserLock(payload.cronJobUid), LOCK_HEARTBEAT_MS$1);
2981
+ if (needsBrowserLock && !browserLock && event.toolName.startsWith(BROWSER_TOOL_PREFIX$1)) {
2982
+ browserLock = acquireBrowserLockWithHeartbeat(payload.cronJobUid, config.sessions.processTimeoutMs);
2983
+ if (!browserLock) throw new Error("browser is locked by another session");
2974
2984
  logger.info(`browser lock acquired lazily for bird ${shortUid(payload.cronJobUid)}`);
2975
2985
  }
2976
2986
  } else if (event.type === "text_delta") result += redact(event.delta);
@@ -3000,8 +3010,7 @@ function startScheduler(getConfig, signal, deps) {
3000
3010
  } else logger.info(`bird ${shortUid(payload.cronJobUid)} completed (${result.length} chars)`);
3001
3011
  return result;
3002
3012
  } finally {
3003
- if (heartbeatTimer) clearInterval(heartbeatTimer);
3004
- if (browserLockAcquired) releaseBrowserLock(payload.cronJobUid);
3013
+ browserLock?.release();
3005
3014
  }
3006
3015
  });
3007
3016
  registerHandler("system_cron_run", (raw) => {
@@ -3137,7 +3146,6 @@ function createCoalescer(config, onDispatch) {
3137
3146
  //#endregion
3138
3147
  //#region src/channel/handler.ts
3139
3148
  const BROWSER_TOOL_PREFIX = "mcp__playwright__";
3140
- const LOCK_HEARTBEAT_MS = 3e4;
3141
3149
  function createHandler(client, getConfig, signal, getTeamId) {
3142
3150
  const locks = /* @__PURE__ */ new Map();
3143
3151
  let activeSpawns = 0;
@@ -3279,8 +3287,7 @@ function createHandler(client, getConfig, signal, getTeamId) {
3279
3287
  return;
3280
3288
  }
3281
3289
  const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
3282
- let browserLockAcquired = false;
3283
- let heartbeatTimer;
3290
+ const browser = { lock: null };
3284
3291
  lock.processing = true;
3285
3292
  activeSpawns++;
3286
3293
  let sessionUid;
@@ -3314,13 +3321,11 @@ function createHandler(client, getConfig, signal, getTeamId) {
3314
3321
  client.setTitle?.(channelId, threadTs, title).catch(() => {});
3315
3322
  }
3316
3323
  const onToolUse = (toolName) => {
3317
- if (!needsBrowserLock || browserLockAcquired) return;
3324
+ if (!needsBrowserLock || browser.lock) return;
3318
3325
  if (!toolName.startsWith(BROWSER_TOOL_PREFIX)) return;
3319
- if (acquireBrowserLock(key, config.sessions.processTimeoutMs)) {
3320
- browserLockAcquired = true;
3321
- heartbeatTimer = setInterval(() => refreshBrowserLock(key), LOCK_HEARTBEAT_MS);
3322
- logger.info(`browser lock acquired lazily for ${key} (tool: ${toolName})`);
3323
- } else {
3326
+ browser.lock = acquireBrowserLockWithHeartbeat(key, config.sessions.processTimeoutMs);
3327
+ if (browser.lock) logger.info(`browser lock acquired lazily for ${key} (tool: ${toolName})`);
3328
+ else {
3324
3329
  logger.warn(`browser lock unavailable for ${key} (tool: ${toolName})`);
3325
3330
  client.postMessage(channelId, threadTs, "The browser is in use by another session.").catch(() => {});
3326
3331
  }
@@ -3338,8 +3343,7 @@ function createHandler(client, getConfig, signal, getTeamId) {
3338
3343
  await client.postMessage(channelId, threadTs, `Something went wrong: ${errMsg}`, { blocks });
3339
3344
  } catch {}
3340
3345
  } finally {
3341
- if (heartbeatTimer) clearInterval(heartbeatTimer);
3342
- if (browserLockAcquired) releaseBrowserLock(key);
3346
+ browser.lock?.release();
3343
3347
  activeSpawns--;
3344
3348
  lock.processing = false;
3345
3349
  lock.killCurrent = null;
@@ -4002,13 +4006,16 @@ async function startDaemon(options) {
4002
4006
  startHealthChecks(getConfig, controller.signal);
4003
4007
  healthStarted = true;
4004
4008
  }
4005
- if (!slackStarted && config.slack.botToken && config.slack.appToken) {
4009
+ const hasSlackTokens = typeof config.slack.botToken === "string" && config.slack.botToken.startsWith("xoxb-") && typeof config.slack.appToken === "string" && config.slack.appToken.startsWith("xapp-");
4010
+ if (!slackStarted && hasSlackTokens) {
4006
4011
  logger.info("connecting to slack...");
4007
4012
  slackHandle = createSlackChannel(getConfig, controller.signal);
4013
+ slackStarted = true;
4008
4014
  slackHandle.start().catch((err) => {
4009
4015
  logger.error(`slack failed to start: ${err instanceof Error ? err.message : String(err)}`);
4016
+ slackStarted = false;
4017
+ slackHandle = null;
4010
4018
  });
4011
- slackStarted = true;
4012
4019
  }
4013
4020
  if (!activated) {
4014
4021
  logger.success("browserbird orchestrator started");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owloops/browserbird",
3
- "version": "1.4.12",
3
+ "version": "1.4.13",
4
4
  "description": "AI agent orchestrator with a real browser, a cron scheduler, and a web dashboard",
5
5
  "type": "module",
6
6
  "bin": {