@owloops/browserbird 1.2.4 → 1.2.5
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/dist/{db-DAXqDqAz.mjs → db-C9ESgb0d.mjs} +12 -1
- package/dist/index.mjs +84 -43
- package/package.json +1 -1
|
@@ -290,6 +290,17 @@ const MIGRATIONS = [{
|
|
|
290
290
|
ON logs(level, source, created_at DESC);
|
|
291
291
|
`);
|
|
292
292
|
}
|
|
293
|
+
}, {
|
|
294
|
+
name: "browser lock",
|
|
295
|
+
up(d) {
|
|
296
|
+
d.exec(`
|
|
297
|
+
CREATE TABLE IF NOT EXISTS browser_lock (
|
|
298
|
+
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
299
|
+
holder TEXT NOT NULL,
|
|
300
|
+
acquired_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
301
|
+
);
|
|
302
|
+
`);
|
|
303
|
+
}
|
|
293
304
|
}];
|
|
294
305
|
let db = null;
|
|
295
306
|
function getSchemaVersion(d) {
|
|
@@ -1025,4 +1036,4 @@ var db_exports = /* @__PURE__ */ __exportAll({
|
|
|
1025
1036
|
});
|
|
1026
1037
|
|
|
1027
1038
|
//#endregion
|
|
1028
|
-
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,
|
|
1039
|
+
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 };
|
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
|
|
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-C9ESgb0d.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { parseArgs, styleText } from "node:util";
|
|
4
4
|
import { copyFileSync, existsSync, mkdirSync, 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: ${"
|
|
126
|
-
buildInfo.push(`built: 2026-03-
|
|
125
|
+
buildInfo.push(`commit: ${"6c46374ad2f83536a49768a46c02b8a043b1304a".substring(0, 7)}`);
|
|
126
|
+
buildInfo.push(`built: 2026-03-05T12:20:55+04:00`);
|
|
127
127
|
const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
|
|
128
128
|
const VERSION = `browserbird ${pkg.version}${buildString}`;
|
|
129
129
|
const BIRD = [
|
|
@@ -248,8 +248,7 @@ function validateConfig(config) {
|
|
|
248
248
|
if (!Array.isArray(agent.channels) || agent.channels.length === 0) throw new Error(`agent "${agent.id}": "channels" must be a non-empty array`);
|
|
249
249
|
if (agent.fallbackModel && agent.fallbackModel === agent.model) throw new Error(`agent "${agent.id}": fallbackModel cannot be the same as model ("${agent.model}")`);
|
|
250
250
|
}
|
|
251
|
-
|
|
252
|
-
if (config.browser.enabled && browserMode === "persistent" && config.sessions.maxConcurrent > 1) logger.warn("persistent browser mode with maxConcurrent > 1 will cause lock contention; use \"isolated\" or set maxConcurrent to 1");
|
|
251
|
+
if (config.browser.enabled && getBrowserMode() === "persistent" && config.sessions.maxConcurrent > 1) logger.warn("persistent browser mode with maxConcurrent > 1 will cause lock contention; use \"isolated\" or set maxConcurrent to 1");
|
|
253
252
|
}
|
|
254
253
|
/**
|
|
255
254
|
* Reads and merges JSON config with DEFAULTS but skips env: resolution.
|
|
@@ -313,6 +312,10 @@ function ensureMcpConfig(config, configDir) {
|
|
|
313
312
|
config.browser.mcpConfigPath = mcpPath;
|
|
314
313
|
logger.info(`generated mcp config at ${mcpPath} (host: ${host})`);
|
|
315
314
|
}
|
|
315
|
+
/** Returns the browser mode from BROWSER_MODE env var, defaulting to 'persistent'. */
|
|
316
|
+
function getBrowserMode() {
|
|
317
|
+
return process.env["BROWSER_MODE"] ?? "persistent";
|
|
318
|
+
}
|
|
316
319
|
/** Atomic write: writes to a .tmp file then renames over the target. */
|
|
317
320
|
function saveConfig(configPath, data) {
|
|
318
321
|
const tmp = configPath + ".tmp";
|
|
@@ -908,7 +911,7 @@ function sanitizeConfig(config) {
|
|
|
908
911
|
birds: config.birds,
|
|
909
912
|
browser: {
|
|
910
913
|
enabled: config.browser.enabled,
|
|
911
|
-
mode:
|
|
914
|
+
mode: getBrowserMode(),
|
|
912
915
|
novncHost: config.browser.novncHost,
|
|
913
916
|
vncPort: config.browser.vncPort,
|
|
914
917
|
novncPort: config.browser.novncPort
|
|
@@ -2861,6 +2864,29 @@ function truncate(text, maxLength) {
|
|
|
2861
2864
|
return text.slice(0, maxLength) + "...";
|
|
2862
2865
|
}
|
|
2863
2866
|
|
|
2867
|
+
//#endregion
|
|
2868
|
+
//#region src/browser/lock.ts
|
|
2869
|
+
/** @fileoverview SQLite-based browser lock for persistent browser mode. */
|
|
2870
|
+
/**
|
|
2871
|
+
* Attempts to acquire the browser lock for a given holder.
|
|
2872
|
+
* Uses a single conditional UPSERT: inserts if absent, overwrites if stale.
|
|
2873
|
+
* Returns false when an active (non-stale) lock is held by someone else.
|
|
2874
|
+
*/
|
|
2875
|
+
function acquireBrowserLock(holder, timeoutMs) {
|
|
2876
|
+
const timeoutSeconds = Math.ceil(timeoutMs / 1e3);
|
|
2877
|
+
const result = getDb().prepare(`INSERT INTO browser_lock (id, holder, acquired_at)
|
|
2878
|
+
VALUES (1, ?, datetime('now'))
|
|
2879
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2880
|
+
holder = excluded.holder,
|
|
2881
|
+
acquired_at = excluded.acquired_at
|
|
2882
|
+
WHERE acquired_at <= datetime('now', '-' || ? || ' seconds')`).run(holder, timeoutSeconds);
|
|
2883
|
+
return Number(result.changes) > 0;
|
|
2884
|
+
}
|
|
2885
|
+
/** Releases the browser lock only if the caller is the current holder. */
|
|
2886
|
+
function releaseBrowserLock(holder) {
|
|
2887
|
+
getDb().prepare("DELETE FROM browser_lock WHERE id = 1 AND holder = ?").run(holder);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2864
2890
|
//#endregion
|
|
2865
2891
|
//#region src/cron/scheduler.ts
|
|
2866
2892
|
const TICK_INTERVAL_MS = 6e4;
|
|
@@ -2898,40 +2924,48 @@ function startScheduler(config, signal, deps) {
|
|
|
2898
2924
|
const payload = raw;
|
|
2899
2925
|
const agent = config.agents.find((a) => a.id === payload.agentId);
|
|
2900
2926
|
if (!agent) throw new Error(`agent "${payload.agentId}" not found`);
|
|
2901
|
-
const
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
mcpConfigPath: config.browser.mcpConfigPath
|
|
2905
|
-
}, signal);
|
|
2906
|
-
if (payload.channelId) logMessage(payload.channelId, null, agent.id, "in", payload.prompt);
|
|
2907
|
-
let result = "";
|
|
2908
|
-
let completion;
|
|
2909
|
-
for await (const event of events) if (event.type === "text_delta") result += redact(event.delta);
|
|
2910
|
-
else if (event.type === "completion") completion = event;
|
|
2911
|
-
else if (event.type === "rate_limit") logger.debug(`bird ${shortUid(payload.cronJobUid)} rate limit window resets ${(/* @__PURE__ */ new Date(event.resetsAt * 1e3)).toISOString()}`);
|
|
2912
|
-
else if (event.type === "error") {
|
|
2913
|
-
const safeError = redact(event.error);
|
|
2914
|
-
if (payload.channelId && deps?.postToSlack) {
|
|
2915
|
-
const blocks = sessionErrorBlocks(safeError, { birdName: agent.name });
|
|
2916
|
-
await deps.postToSlack(payload.channelId, `Bird failed: ${safeError}`, { blocks });
|
|
2917
|
-
}
|
|
2918
|
-
throw new Error(safeError);
|
|
2927
|
+
const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
|
|
2928
|
+
if (needsBrowserLock) {
|
|
2929
|
+
if (!acquireBrowserLock(payload.cronJobUid, config.sessions.processTimeoutMs)) throw new Error("browser is locked by another session");
|
|
2919
2930
|
}
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
+
try {
|
|
2932
|
+
const { events } = spawnProvider(agent.provider, {
|
|
2933
|
+
message: payload.prompt,
|
|
2934
|
+
agent,
|
|
2935
|
+
mcpConfigPath: config.browser.mcpConfigPath
|
|
2936
|
+
}, signal);
|
|
2937
|
+
if (payload.channelId) logMessage(payload.channelId, null, agent.id, "in", payload.prompt);
|
|
2938
|
+
let result = "";
|
|
2939
|
+
let completion;
|
|
2940
|
+
for await (const event of events) if (event.type === "text_delta") result += redact(event.delta);
|
|
2941
|
+
else if (event.type === "completion") completion = event;
|
|
2942
|
+
else if (event.type === "rate_limit") logger.debug(`bird ${shortUid(payload.cronJobUid)} rate limit window resets ${(/* @__PURE__ */ new Date(event.resetsAt * 1e3)).toISOString()}`);
|
|
2943
|
+
else if (event.type === "error") {
|
|
2944
|
+
const safeError = redact(event.error);
|
|
2945
|
+
if (payload.channelId && deps?.postToSlack) {
|
|
2946
|
+
const blocks = sessionErrorBlocks(safeError, { birdName: agent.name });
|
|
2947
|
+
await deps.postToSlack(payload.channelId, `Bird failed: ${safeError}`, { blocks });
|
|
2948
|
+
}
|
|
2949
|
+
throw new Error(safeError);
|
|
2950
|
+
}
|
|
2951
|
+
if (completion && payload.channelId) logMessage(payload.channelId, null, agent.id, "out", result || void 0, completion.tokensIn, completion.tokensOut);
|
|
2952
|
+
if (!result) {
|
|
2953
|
+
logger.info(`bird ${shortUid(payload.cronJobUid)} completed (no output)`);
|
|
2954
|
+
return "completed (no output)";
|
|
2931
2955
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2956
|
+
if (payload.channelId && deps?.postToSlack) {
|
|
2957
|
+
await deps.postToSlack(payload.channelId, result);
|
|
2958
|
+
if (completion) {
|
|
2959
|
+
const blocks = sessionCompleteBlocks(completion, void 0, agent.name);
|
|
2960
|
+
const fallback = `Bird ${agent.name} completed: ${completion.numTurns} turns`;
|
|
2961
|
+
await deps.postToSlack(payload.channelId, fallback, { blocks });
|
|
2962
|
+
}
|
|
2963
|
+
logger.info(`bird ${shortUid(payload.cronJobUid)} result posted to ${payload.channelId}`);
|
|
2964
|
+
} else logger.info(`bird ${shortUid(payload.cronJobUid)} completed (${result.length} chars)`);
|
|
2965
|
+
return result;
|
|
2966
|
+
} finally {
|
|
2967
|
+
if (needsBrowserLock) releaseBrowserLock(payload.cronJobUid);
|
|
2968
|
+
}
|
|
2935
2969
|
});
|
|
2936
2970
|
registerHandler("system_cron_run", (raw) => {
|
|
2937
2971
|
const payload = raw;
|
|
@@ -3162,6 +3196,12 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3162
3196
|
logger.warn("max concurrent sessions reached");
|
|
3163
3197
|
return;
|
|
3164
3198
|
}
|
|
3199
|
+
const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
|
|
3200
|
+
if (needsBrowserLock && !acquireBrowserLock(key, config.sessions.processTimeoutMs)) {
|
|
3201
|
+
await client.postMessage(channelId, threadTs, "The browser is in use by another session. Your message will be processed when it finishes.");
|
|
3202
|
+
lock.queue.push(dispatch);
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3165
3205
|
lock.processing = true;
|
|
3166
3206
|
activeSpawns++;
|
|
3167
3207
|
let sessionUid;
|
|
@@ -3202,6 +3242,7 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3202
3242
|
await client.postMessage(channelId, threadTs, `Something went wrong: ${errMsg}`, { blocks });
|
|
3203
3243
|
} catch {}
|
|
3204
3244
|
} finally {
|
|
3245
|
+
if (needsBrowserLock) releaseBrowserLock(key);
|
|
3205
3246
|
activeSpawns--;
|
|
3206
3247
|
lock.processing = false;
|
|
3207
3248
|
lock.killCurrent = null;
|
|
@@ -3737,7 +3778,7 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
|
|
|
3737
3778
|
logger.warn("bird_create submission missing required fields");
|
|
3738
3779
|
return;
|
|
3739
3780
|
}
|
|
3740
|
-
const { createCronJob, setCronJobEnabled } = await import("./db-
|
|
3781
|
+
const { createCronJob, setCronJobEnabled } = await import("./db-C9ESgb0d.mjs").then((n) => n.t);
|
|
3741
3782
|
const bird = createCronJob(name, schedule, prompt, channelId || void 0, "default", defaultTimezone);
|
|
3742
3783
|
if (enabledValue !== "enabled") setCronJobEnabled(bird.uid, false);
|
|
3743
3784
|
await webClient.chat.postMessage({
|
|
@@ -3751,7 +3792,7 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
|
|
|
3751
3792
|
}
|
|
3752
3793
|
async function handleSessionRetry(sessionUid, channelId, userId, config, handler) {
|
|
3753
3794
|
try {
|
|
3754
|
-
const { getSession, getLastInboundMessage } = await import("./db-
|
|
3795
|
+
const { getSession, getLastInboundMessage } = await import("./db-C9ESgb0d.mjs").then((n) => n.t);
|
|
3755
3796
|
const session = getSession(sessionUid);
|
|
3756
3797
|
if (!session) {
|
|
3757
3798
|
logger.warn(`retry: session ${sessionUid} not found`);
|
|
@@ -3857,7 +3898,7 @@ async function startDaemon(options) {
|
|
|
3857
3898
|
logger.success("browserbird orchestrator started");
|
|
3858
3899
|
logger.info(`agents: ${config.agents.map((a) => a.id).join(", ")}`);
|
|
3859
3900
|
logger.info(`max concurrent sessions: ${config.sessions.maxConcurrent}`);
|
|
3860
|
-
if (config.browser.enabled) logger.info(`browser mode: ${
|
|
3901
|
+
if (config.browser.enabled) logger.info(`browser mode: ${getBrowserMode()}`);
|
|
3861
3902
|
};
|
|
3862
3903
|
const onLaunch = async () => {
|
|
3863
3904
|
loadDotEnv(envPath);
|
|
@@ -4622,7 +4663,7 @@ function handleConfig(argv) {
|
|
|
4622
4663
|
allowPositionals: false,
|
|
4623
4664
|
strict: false
|
|
4624
4665
|
});
|
|
4625
|
-
printConfig(values.config);
|
|
4666
|
+
printConfig(values.config ?? process.env["BROWSERBIRD_CONFIG"]);
|
|
4626
4667
|
}
|
|
4627
4668
|
function printConfig(configPath) {
|
|
4628
4669
|
const config = loadRawConfig(configPath);
|
|
@@ -4652,7 +4693,7 @@ function printConfig(configPath) {
|
|
|
4652
4693
|
console.log(`\n${c("cyan", "browser:")}`);
|
|
4653
4694
|
console.log(` ${c("dim", "enabled:")} ${config.browser.enabled ? "yes" : "no"}`);
|
|
4654
4695
|
if (config.browser.enabled) {
|
|
4655
|
-
console.log(` ${c("dim", "mode:")} ${
|
|
4696
|
+
console.log(` ${c("dim", "mode:")} ${getBrowserMode()}`);
|
|
4656
4697
|
console.log(` ${c("dim", "vnc port:")} ${config.browser.vncPort}`);
|
|
4657
4698
|
console.log(` ${c("dim", "novnc port:")} ${config.browser.novncPort}`);
|
|
4658
4699
|
}
|