@owloops/browserbird 1.4.21 → 1.5.0

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.
@@ -496,8 +496,8 @@ function getSessionTokenStats(channelId, threadId) {
496
496
  function getSessionCount() {
497
497
  return getDb().prepare("SELECT COUNT(*) as count FROM sessions").get().count;
498
498
  }
499
- function deleteStaleSessions(ttlHours) {
500
- const result = getDb().prepare(`DELETE FROM sessions WHERE last_active < datetime('now', ? || ' hours')`).run(`-${ttlHours}`);
499
+ function deleteOldSessions(retentionDays) {
500
+ const result = getDb().prepare(`DELETE FROM sessions WHERE last_active < datetime('now', ? || ' days')`).run(`-${retentionDays}`);
501
501
  return Number(result.changes);
502
502
  }
503
503
  function updateSessionProviderId(uid, providerSessionId) {
@@ -731,6 +731,9 @@ function claimNextJob() {
731
731
  };
732
732
  });
733
733
  }
734
+ function getJobStatus(jobId) {
735
+ return getDb().prepare("SELECT status FROM jobs WHERE id = ?").get(jobId)?.status;
736
+ }
734
737
  function completeJob(jobId, result) {
735
738
  getDb().prepare(`UPDATE jobs SET status = 'completed', completed_at = datetime('now'), result = ?
736
739
  WHERE id = ?`).run(result ?? null, jobId);
@@ -984,7 +987,7 @@ var db_exports = /* @__PURE__ */ __exportAll({
984
987
  deleteOldJobs: () => deleteOldJobs,
985
988
  deleteOldLogs: () => deleteOldLogs,
986
989
  deleteOldMessages: () => deleteOldMessages,
987
- deleteStaleSessions: () => deleteStaleSessions,
990
+ deleteOldSessions: () => deleteOldSessions,
988
991
  ensureSystemCronJob: () => ensureSystemCronJob,
989
992
  failJob: () => failJob,
990
993
  failStaleJobs: () => failStaleJobs,
@@ -994,6 +997,7 @@ var db_exports = /* @__PURE__ */ __exportAll({
994
997
  getEnabledCronJobs: () => getEnabledCronJobs,
995
998
  getFlightStats: () => getFlightStats,
996
999
  getJobStats: () => getJobStats,
1000
+ getJobStatus: () => getJobStatus,
997
1001
  getLastInboundMessage: () => getLastInboundMessage,
998
1002
  getMessageStats: () => getMessageStats,
999
1003
  getRecentLogs: () => getRecentLogs,
@@ -1031,4 +1035,4 @@ var db_exports = /* @__PURE__ */ __exportAll({
1031
1035
  });
1032
1036
 
1033
1037
  //#endregion
1034
- export { updateSessionProviderId as $, completeCronRun as A, listFlights as B, failStaleJobs as C, retryAllFailedJobs as D, listJobs as E, ensureSystemCronJob as F, deleteStaleSessions as G, updateCronJob as H, getCronJob as I, getSessionCount as J, findSession as K, getEnabledCronJobs as L, createCronRun as M, deleteCronJob as N, retryJob as O, deleteOldCronRuns as P, touchSession as Q, getFlightStats as R, failJob as S, hasPendingCronJob as T, updateCronJobStatus as U, setCronJobEnabled as V, createSession as W, getSessionTokenStats as X, getSessionMessages as Y, listSessions as Z, clearJobs as _, getSetting as a, resolveByUid as at, deleteJob as b, getUserCount as c, getRecentLogs as d, shortUid as et, insertLog as f, claimNextJob as g, logMessage as h, createUser as i, optimizeDatabase as it, createCronJob as j, SYSTEM_CRON_PREFIX as k, setSetting as l, getMessageStats as m, resolveDbPath as n, getDb as nt, getUserByEmail as o, logger as ot, deleteOldMessages as p, getSession as q, resolveDbPathFromArgv as r, openDatabase as rt, getUserById as s, db_exports as t, closeDatabase as tt, deleteOldLogs as u, completeJob as v, getJobStats as w, deleteOldJobs as x, createJob as y, listCronJobs as z };
1038
+ export { touchSession as $, SYSTEM_CRON_PREFIX as A, listCronJobs as B, failStaleJobs as C, listJobs as D, hasPendingCronJob as E, deleteOldCronRuns as F, createSession as G, setCronJobEnabled as H, ensureSystemCronJob as I, getSession as J, deleteOldSessions as K, getCronJob as L, createCronJob as M, createCronRun as N, retryAllFailedJobs as O, deleteCronJob as P, listSessions as Q, getEnabledCronJobs as R, failJob as S, getJobStatus as T, updateCronJob as U, listFlights as V, updateCronJobStatus as W, getSessionMessages as X, getSessionCount as Y, getSessionTokenStats as Z, clearJobs as _, getSetting as a, optimizeDatabase as at, deleteJob as b, getUserCount as c, getRecentLogs as d, updateSessionProviderId as et, insertLog as f, claimNextJob as g, logMessage as h, createUser as i, openDatabase as it, completeCronRun as j, retryJob as k, setSetting as l, getMessageStats as m, resolveDbPath as n, closeDatabase as nt, getUserByEmail as o, resolveByUid as ot, deleteOldMessages as p, findSession as q, resolveDbPathFromArgv as r, getDb as rt, getUserById as s, logger as st, db_exports as t, shortUid as tt, deleteOldLogs as u, completeJob as v, getJobStats as w, deleteOldJobs as x, createJob as y, getFlightStats as z };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as updateSessionProviderId, A as completeCronRun, B as listFlights, C as failStaleJobs, D as retryAllFailedJobs, E as listJobs, F as ensureSystemCronJob, G as deleteStaleSessions, H as updateCronJob, I as getCronJob, J as getSessionCount, K as findSession, L as getEnabledCronJobs, M as createCronRun, N as deleteCronJob, O as retryJob, P as deleteOldCronRuns, Q as touchSession, R as getFlightStats, S as failJob, T as hasPendingCronJob, U as updateCronJobStatus, V as setCronJobEnabled, W as createSession, X as getSessionTokenStats, Y as getSessionMessages, Z as listSessions, _ as clearJobs, a as getSetting, at as resolveByUid, b as deleteJob, c as getUserCount, d as getRecentLogs, et as shortUid, f as insertLog, g as claimNextJob, h as logMessage, i as createUser, it as optimizeDatabase, j as createCronJob, k as SYSTEM_CRON_PREFIX, l as setSetting, m as getMessageStats, n as resolveDbPath, nt as getDb, o as getUserByEmail, ot as logger, p as deleteOldMessages, q as getSession, r as resolveDbPathFromArgv, rt as openDatabase, s as getUserById, tt as closeDatabase, u as deleteOldLogs, v as completeJob, w as getJobStats, x as deleteOldJobs, y as createJob, z as listCronJobs } from "./db-CyQcrilg.mjs";
1
+ import { $ as touchSession, A as SYSTEM_CRON_PREFIX, B as listCronJobs, C as failStaleJobs, D as listJobs, E as hasPendingCronJob, F as deleteOldCronRuns, G as createSession, H as setCronJobEnabled, I as ensureSystemCronJob, J as getSession, K as deleteOldSessions, L as getCronJob, M as createCronJob, N as createCronRun, O as retryAllFailedJobs, P as deleteCronJob, Q as listSessions, R as getEnabledCronJobs, S as failJob, T as getJobStatus, U as updateCronJob, V as listFlights, W as updateCronJobStatus, X as getSessionMessages, Y as getSessionCount, Z as getSessionTokenStats, _ as clearJobs, a as getSetting, at as optimizeDatabase, b as deleteJob, c as getUserCount, d as getRecentLogs, et as updateSessionProviderId, f as insertLog, g as claimNextJob, h as logMessage, i as createUser, it as openDatabase, j as completeCronRun, k as retryJob, l as setSetting, m as getMessageStats, n as resolveDbPath, nt as closeDatabase, o as getUserByEmail, ot as resolveByUid, p as deleteOldMessages, q as findSession, r as resolveDbPathFromArgv, rt as getDb, s as getUserById, st as logger, tt as shortUid, u as deleteOldLogs, v as completeJob, w as getJobStats, x as deleteOldJobs, y as createJob, z as getFlightStats } from "./db-BNF1vZIm.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { parseArgs, styleText } from "node:util";
4
4
  import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -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: ${"e1ba35fad0d7d361985d65e8ebbbb8401a37d866".substring(0, 7)}`);
126
- buildInfo.push(`built: 2026-03-16T19:06:04+04:00`);
125
+ buildInfo.push(`commit: ${"8a715575d2cfba34d6be3d948660af50a731ab0d".substring(0, 7)}`);
126
+ buildInfo.push(`built: 2026-03-17T23:32:28+04:00`);
127
127
  const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
128
128
  const VERSION = `browserbird ${pkg.version}${buildString}`;
129
129
  const BIRD = [
@@ -2009,10 +2009,12 @@ async function processJob(job) {
2009
2009
  } finally {
2010
2010
  if (cronRun != null) completeCronRun(cronRun.uid, finalStatus === "completed" ? "success" : "error", resultText, errorText);
2011
2011
  if (isCronRun && job.cron_job_uid != null) {
2012
- const cronJob = getCronJob(job.cron_job_uid);
2013
- if (cronJob != null) {
2014
- const newFailureCount = finalStatus === "failed" ? cronJob.failure_count + 1 : 0;
2015
- updateCronJobStatus(job.cron_job_uid, finalStatus, newFailureCount);
2012
+ if (!(getJobStatus(job.id) === "failed" && finalStatus === "failed")) {
2013
+ const cronJob = getCronJob(job.cron_job_uid);
2014
+ if (cronJob != null) {
2015
+ const newFailureCount = finalStatus === "failed" ? cronJob.failure_count + 1 : 0;
2016
+ updateCronJobStatus(job.cron_job_uid, finalStatus, newFailureCount);
2017
+ }
2016
2018
  }
2017
2019
  }
2018
2020
  const invalidatePayload = { resource: isCronRun ? "birds" : "sessions" };
@@ -2099,9 +2101,9 @@ function resolveSession(channelId, threadTs, config, nameToId) {
2099
2101
  isNew: true
2100
2102
  };
2101
2103
  }
2102
- function expireStaleSessions(ttlHours) {
2103
- const deleted = deleteStaleSessions(ttlHours);
2104
- if (deleted > 0) logger.info(`expired ${deleted} stale session(s)`);
2104
+ function deleteExpiredSessions(retentionDays) {
2105
+ const deleted = deleteOldSessions(retentionDays);
2106
+ if (deleted > 0) logger.info(`deleted ${deleted} expired session(s)`);
2105
2107
  return deleted;
2106
2108
  }
2107
2109
 
@@ -2694,7 +2696,9 @@ function birdFlyBlocks(birdName, userId) {
2694
2696
  }
2695
2697
  function statusBlocks(opts) {
2696
2698
  const slackStatus = opts.slackConnected ? "Connected" : "Disconnected";
2697
- return [header("BrowserBird Status"), fields(["Slack", slackStatus], ["Active Sessions", `${opts.activeCount}/${opts.maxConcurrent}`], ["Birds", String(opts.birdCount)], ["Uptime", opts.uptime])];
2699
+ const result = [header("BrowserBird Status"), fields(["Slack", slackStatus], ["Active Sessions", `${opts.activeCount}/${opts.maxConcurrent}`], ["Birds", String(opts.birdCount)], ["Uptime", opts.uptime])];
2700
+ if (opts.runningBirds && opts.runningBirds.length > 0) result.push(section(`*In flight:* ${opts.runningBirds.join(", ")}`));
2701
+ return result;
2698
2702
  }
2699
2703
  function truncate(text, maxLength) {
2700
2704
  if (text.length <= maxLength) return text;
@@ -2704,6 +2708,17 @@ function truncate(text, maxLength) {
2704
2708
  //#endregion
2705
2709
  //#region src/cron/scheduler.ts
2706
2710
  const BROWSER_TOOL_PREFIX$1 = "mcp__playwright__";
2711
+ const activeKills = /* @__PURE__ */ new Map();
2712
+ function killBird(cronJobUid) {
2713
+ const kill = activeKills.get(cronJobUid);
2714
+ if (!kill) return false;
2715
+ kill();
2716
+ activeKills.delete(cronJobUid);
2717
+ return true;
2718
+ }
2719
+ function getRunningBirdUids() {
2720
+ return [...activeKills.keys()];
2721
+ }
2707
2722
  const TICK_INTERVAL_MS = 6e4;
2708
2723
  const MAX_SCHEDULE_ERRORS = 3;
2709
2724
  const systemHandlers = /* @__PURE__ */ new Map();
@@ -2711,13 +2726,13 @@ function registerSystemCronJobs(config, retentionDays) {
2711
2726
  const cleanupName = `${SYSTEM_CRON_PREFIX}db_cleanup__`;
2712
2727
  const optimizeName = `${SYSTEM_CRON_PREFIX}db_optimize__`;
2713
2728
  systemHandlers.set(cleanupName, () => {
2714
- expireStaleSessions(config.sessions.ttlHours);
2729
+ const sessions = deleteExpiredSessions(retentionDays);
2715
2730
  const msgs = deleteOldMessages(retentionDays);
2716
2731
  const runs = deleteOldCronRuns(retentionDays);
2717
2732
  const jobs = deleteOldJobs(retentionDays);
2718
2733
  const logs = deleteOldLogs(retentionDays);
2719
- if (msgs > 0 || runs > 0 || jobs > 0 || logs > 0) {
2720
- const summary = `${msgs} messages, ${runs} flight logs, ${jobs} jobs, ${logs} logs older than ${retentionDays}d`;
2734
+ if (sessions > 0 || msgs > 0 || runs > 0 || jobs > 0 || logs > 0) {
2735
+ const summary = `${sessions} sessions, ${msgs} messages, ${runs} flight logs, ${jobs} jobs, ${logs} logs older than ${retentionDays}d`;
2721
2736
  logger.info(`system cleanup: ${summary}`);
2722
2737
  return summary;
2723
2738
  }
@@ -2744,13 +2759,14 @@ function startScheduler(getConfig, signal, deps) {
2744
2759
  const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
2745
2760
  let browserLock = null;
2746
2761
  try {
2747
- const { events } = spawnProvider({
2762
+ const { events, kill } = spawnProvider({
2748
2763
  message: payload.prompt,
2749
2764
  agent,
2750
2765
  mcpConfigPath: config.browser.mcpConfigPath,
2751
2766
  timezone: config.timezone,
2752
2767
  globalTimeoutMs: config.sessions.processTimeoutMs
2753
2768
  }, signal);
2769
+ activeKills.set(payload.cronJobUid, kill);
2754
2770
  if (payload.channelId) logMessage(payload.channelId, null, agent.id, "in", payload.prompt);
2755
2771
  let result = "";
2756
2772
  let completion;
@@ -2787,6 +2803,7 @@ function startScheduler(getConfig, signal, deps) {
2787
2803
  } else logger.info(`bird ${shortUid(payload.cronJobUid)} completed (${result.length} chars)`);
2788
2804
  return result;
2789
2805
  } finally {
2806
+ activeKills.delete(payload.cronJobUid);
2790
2807
  browserLock?.release();
2791
2808
  }
2792
2809
  });
@@ -2863,6 +2880,7 @@ function startScheduler(getConfig, signal, deps) {
2863
2880
  clearInterval(timer);
2864
2881
  scheduleCache.clear();
2865
2882
  scheduleErrors.clear();
2883
+ activeKills.clear();
2866
2884
  });
2867
2885
  logger.info("bird scheduler started (60s tick)");
2868
2886
  }
@@ -3027,9 +3045,10 @@ function createHandler(client, getConfig, signal, getTeamId, getChannelNameToId)
3027
3045
  await safeStop({});
3028
3046
  const blocks = sessionTimeoutBlocks(timedOutMs, { sessionUid });
3029
3047
  await client.postMessage(channelId, threadTs, `Session timed out after ${Math.round(timedOutMs / 6e4)} minutes.`, { blocks });
3030
- } else {
3031
- const footerBlocks = completion ? completionFooterBlocks(completion, hasError, meta.birdName, userId) : void 0;
3032
- await safeStop(footerBlocks ? { blocks: footerBlocks } : {});
3048
+ } else if (completion) await safeStop({ blocks: completionFooterBlocks(completion, hasError, meta.birdName, userId) });
3049
+ else {
3050
+ if (!fullText) await safeAppend({ markdown_text: "_Stopped._" });
3051
+ await safeStop({});
3033
3052
  }
3034
3053
  }
3035
3054
  async function uploadImages(images, channelId, threadTs) {
@@ -3078,7 +3097,7 @@ function createHandler(client, getConfig, signal, getTeamId, getChannelNameToId)
3078
3097
  const { session, agent, isNew } = resolved;
3079
3098
  sessionUid = session.uid;
3080
3099
  for (const msg of messages) logMessage(channelId, threadTs, msg.userId, "in", msg.text);
3081
- touchSession(session.uid, messages.length + 1);
3100
+ touchSession(session.uid, messages.length);
3082
3101
  broadcastSSE("invalidate", { resource: "sessions" });
3083
3102
  const prompt = formatPrompt(messages);
3084
3103
  const userId = messages[messages.length - 1].userId;
@@ -3288,6 +3307,9 @@ async function handleSlashCommand(body, webClient, channelClient, config, status
3288
3307
  }
3289
3308
  case "status": {
3290
3309
  const cronJobs = listCronJobs(1, 1, false);
3310
+ const runningBirds = getRunningBirdUids().map((uid) => {
3311
+ return getCronJob(uid)?.name ?? uid;
3312
+ });
3291
3313
  await say({
3292
3314
  text: "BrowserBird status",
3293
3315
  blocks: statusBlocks({
@@ -3295,16 +3317,35 @@ async function handleSlashCommand(body, webClient, channelClient, config, status
3295
3317
  activeCount: status.activeCount(),
3296
3318
  maxConcurrent: config.sessions.maxConcurrent,
3297
3319
  birdCount: cronJobs.totalItems,
3298
- uptime: formatUptime()
3320
+ uptime: formatUptime(),
3321
+ runningBirds
3299
3322
  })
3300
3323
  });
3301
3324
  break;
3302
3325
  }
3326
+ case "stop": {
3327
+ const birdName = parts.slice(1).join(" ");
3328
+ if (!birdName) {
3329
+ await say({ text: "Usage: `/bird stop <name or id>`" });
3330
+ return;
3331
+ }
3332
+ const bird = findBird(birdName);
3333
+ if (!bird) {
3334
+ await say({ text: `Bird not found: \`${birdName}\`` });
3335
+ return;
3336
+ }
3337
+ if (killBird(bird.uid)) {
3338
+ await say({ text: `Stopped *${bird.name}*.` });
3339
+ logger.info(`/bird stop: ${bird.name} killed by ${body.user_id}`);
3340
+ } else await say({ text: `*${bird.name}* is not currently in flight.` });
3341
+ break;
3342
+ }
3303
3343
  default: await say({ text: [
3304
3344
  "*Usage:* `/bird <command>`",
3305
3345
  "",
3306
3346
  "`/bird list` - Show all configured birds",
3307
3347
  "`/bird fly <name>` - Trigger a bird immediately",
3348
+ "`/bird stop [name]` - Stop a running bird",
3308
3349
  "`/bird logs <name>` - Show recent flights",
3309
3350
  "`/bird enable <name>` - Enable a bird",
3310
3351
  "`/bird disable <name>` - Disable a bird",
@@ -3468,6 +3509,15 @@ function createSlackChannel(getConfig, signal) {
3468
3509
  const messageTs = event["ts"];
3469
3510
  const cleanText = stripMention(text);
3470
3511
  if (!cleanText.trim()) return;
3512
+ if (cleanText.trim().toLowerCase() === "stop") {
3513
+ const key = `${channelId}:${threadTs}`;
3514
+ if (handler.killSession(key)) webClient.reactions.add({
3515
+ channel: channelId,
3516
+ timestamp: messageTs,
3517
+ name: "octagonal_sign"
3518
+ }).catch(() => {});
3519
+ return;
3520
+ }
3471
3521
  if (isDm && config.slack.coalesce.bypassDms) handler.handle({
3472
3522
  channelId,
3473
3523
  threadTs,
@@ -3493,6 +3543,15 @@ function createSlackChannel(getConfig, signal) {
3493
3543
  const userId = event["user"] ?? "unknown";
3494
3544
  const text = stripMention(event["text"] ?? "");
3495
3545
  if (!text.trim()) return;
3546
+ if (text.trim().toLowerCase() === "stop") {
3547
+ const key = `${channelId}:${threadTs}`;
3548
+ if (handler.killSession(key)) webClient.reactions.add({
3549
+ channel: channelId,
3550
+ timestamp: messageTs,
3551
+ name: "octagonal_sign"
3552
+ }).catch(() => {});
3553
+ return;
3554
+ }
3496
3555
  coalescer.push(channelId, threadTs, userId, text, messageTs);
3497
3556
  });
3498
3557
  socketClient.on("assistant_thread_started", async ({ ack, event }) => {
@@ -3661,7 +3720,7 @@ async function handleBirdCreateSubmission(view, webClient) {
3661
3720
  logger.warn("bird_create submission missing required fields");
3662
3721
  return;
3663
3722
  }
3664
- const { createCronJob, setCronJobEnabled } = await import("./db-CyQcrilg.mjs").then((n) => n.t);
3723
+ const { createCronJob, setCronJobEnabled } = await import("./db-BNF1vZIm.mjs").then((n) => n.t);
3665
3724
  const bird = createCronJob(name, schedule, prompt, channelId || void 0, "default");
3666
3725
  if (enabledValue !== "enabled") setCronJobEnabled(bird.uid, false);
3667
3726
  await webClient.chat.postMessage({
@@ -3675,7 +3734,7 @@ async function handleBirdCreateSubmission(view, webClient) {
3675
3734
  }
3676
3735
  async function handleSessionRetry(sessionUid, userId, handler) {
3677
3736
  try {
3678
- const { getSession, getLastInboundMessage } = await import("./db-CyQcrilg.mjs").then((n) => n.t);
3737
+ const { getSession, getLastInboundMessage } = await import("./db-BNF1vZIm.mjs").then((n) => n.t);
3679
3738
  const session = getSession(sessionUid);
3680
3739
  if (!session) {
3681
3740
  logger.warn(`retry: session ${sessionUid} not found`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owloops/browserbird",
3
- "version": "1.4.21",
3
+ "version": "1.5.0",
4
4
  "description": "AI agent orchestrator with a real browser, a cron scheduler, and a web dashboard",
5
5
  "type": "module",
6
6
  "bin": {