@owloops/browserbird 1.2.6 → 1.2.7
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-C9ESgb0d.mjs → db-Da2zSpkY.mjs} +4 -9
- package/dist/index.mjs +111 -60
- package/package.json +1 -1
|
@@ -209,7 +209,6 @@ const MIGRATIONS = [{
|
|
|
209
209
|
target_channel_id TEXT,
|
|
210
210
|
active_hours_start TEXT,
|
|
211
211
|
active_hours_end TEXT,
|
|
212
|
-
timezone TEXT DEFAULT 'UTC',
|
|
213
212
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
214
213
|
failure_count INTEGER NOT NULL DEFAULT 0,
|
|
215
214
|
last_run TEXT,
|
|
@@ -536,11 +535,11 @@ function listCronJobs(page = 1, perPage = DEFAULT_PER_PAGE, includeSystem = fals
|
|
|
536
535
|
function getEnabledCronJobs() {
|
|
537
536
|
return getDb().prepare("SELECT * FROM cron_jobs WHERE enabled = 1 ORDER BY created_at").all();
|
|
538
537
|
}
|
|
539
|
-
function createCronJob(name, schedule, prompt, targetChannelId, agentId,
|
|
538
|
+
function createCronJob(name, schedule, prompt, targetChannelId, agentId, activeHoursStart, activeHoursEnd) {
|
|
540
539
|
const uid = generateUid(UID_PREFIX.bird);
|
|
541
|
-
return getDb().prepare(`INSERT INTO cron_jobs (uid, name, schedule, prompt, target_channel_id, agent_id,
|
|
542
|
-
VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
543
|
-
RETURNING *`).get(uid, name, schedule, prompt, targetChannelId ?? null, agentId ?? "default",
|
|
540
|
+
return getDb().prepare(`INSERT INTO cron_jobs (uid, name, schedule, prompt, target_channel_id, agent_id, active_hours_start, active_hours_end)
|
|
541
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
542
|
+
RETURNING *`).get(uid, name, schedule, prompt, targetChannelId ?? null, agentId ?? "default", activeHoursStart ?? null, activeHoursEnd ?? null);
|
|
544
543
|
}
|
|
545
544
|
function updateCronJobStatus(jobUid, status, failureCount) {
|
|
546
545
|
getDb().prepare(`UPDATE cron_jobs SET last_run = datetime('now'), last_status = ?, failure_count = ? WHERE uid = ?`).run(status, failureCount, jobUid);
|
|
@@ -575,10 +574,6 @@ function updateCronJob(jobUid, fields) {
|
|
|
575
574
|
sets.push("agent_id = ?");
|
|
576
575
|
params.push(fields.agentId);
|
|
577
576
|
}
|
|
578
|
-
if (fields.timezone !== void 0) {
|
|
579
|
-
sets.push("timezone = ?");
|
|
580
|
-
params.push(fields.timezone);
|
|
581
|
-
}
|
|
582
577
|
if (fields.activeHoursStart !== void 0) {
|
|
583
578
|
sets.push("active_hours_start = ?");
|
|
584
579
|
params.push(fields.activeHoursStart);
|
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-
|
|
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-Da2zSpkY.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: ${"febb9bb9f678ef12c57880b663dcc875288f1950".substring(0, 7)}`);
|
|
126
|
+
buildInfo.push(`built: 2026-03-06T10:34:46+04:00`);
|
|
127
127
|
const buildString = buildInfo.length > 0 ? ` (${buildInfo.join(", ")})` : "";
|
|
128
128
|
const VERSION = `browserbird ${pkg.version}${buildString}`;
|
|
129
129
|
const BIRD = [
|
|
@@ -374,6 +374,37 @@ function loadDotEnv(envPath) {
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/browser/lock.ts
|
|
379
|
+
/** @fileoverview SQLite-based browser lock for persistent browser mode. */
|
|
380
|
+
/**
|
|
381
|
+
* Attempts to acquire the browser lock for a given holder.
|
|
382
|
+
* Uses a single conditional UPSERT: inserts if absent, overwrites if stale.
|
|
383
|
+
* Returns false when an active (non-stale) lock is held by someone else.
|
|
384
|
+
*/
|
|
385
|
+
function acquireBrowserLock(holder, timeoutMs) {
|
|
386
|
+
const timeoutSeconds = Math.ceil(timeoutMs / 1e3);
|
|
387
|
+
const result = getDb().prepare(`INSERT INTO browser_lock (id, holder, acquired_at)
|
|
388
|
+
VALUES (1, ?, datetime('now'))
|
|
389
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
390
|
+
holder = excluded.holder,
|
|
391
|
+
acquired_at = excluded.acquired_at
|
|
392
|
+
WHERE acquired_at <= datetime('now', '-' || ? || ' seconds')`).run(holder, timeoutSeconds);
|
|
393
|
+
return Number(result.changes) > 0;
|
|
394
|
+
}
|
|
395
|
+
/** Refreshes the lock timestamp to prevent staleness during long operations. */
|
|
396
|
+
function refreshBrowserLock(holder) {
|
|
397
|
+
getDb().prepare(`UPDATE browser_lock SET acquired_at = datetime('now') WHERE id = 1 AND holder = ?`).run(holder);
|
|
398
|
+
}
|
|
399
|
+
/** Releases the browser lock only if the caller is the current holder. */
|
|
400
|
+
function releaseBrowserLock(holder) {
|
|
401
|
+
getDb().prepare("DELETE FROM browser_lock WHERE id = 1 AND holder = ?").run(holder);
|
|
402
|
+
}
|
|
403
|
+
/** Clears any browser lock unconditionally. Called on startup before any sessions exist. */
|
|
404
|
+
function clearBrowserLock() {
|
|
405
|
+
getDb().prepare("DELETE FROM browser_lock WHERE id = 1").run();
|
|
406
|
+
}
|
|
407
|
+
|
|
377
408
|
//#endregion
|
|
378
409
|
//#region src/server/auth.ts
|
|
379
410
|
/** @fileoverview Password hashing, token signing, and verification using node:crypto. */
|
|
@@ -1353,7 +1384,7 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
|
|
|
1353
1384
|
for (const bird of getEnabledCronJobs()) {
|
|
1354
1385
|
if (bird.name.startsWith(SYSTEM_CRON_PREFIX)) continue;
|
|
1355
1386
|
try {
|
|
1356
|
-
const next = nextCronMatch(parseCron(bird.schedule), now,
|
|
1387
|
+
const next = nextCronMatch(parseCron(bird.schedule), now, getConfig().timezone);
|
|
1357
1388
|
if (next) upcoming.push({
|
|
1358
1389
|
uid: bird.uid,
|
|
1359
1390
|
name: bird.name,
|
|
@@ -1417,7 +1448,7 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
|
|
|
1417
1448
|
jsonError(res, "\"prompt\" is required", 400);
|
|
1418
1449
|
return;
|
|
1419
1450
|
}
|
|
1420
|
-
const job = createCronJob(deriveBirdName(body.prompt), body.schedule.trim(), body.prompt.trim(), body.channel?.trim() || void 0, body.agent?.trim() || void 0, body.
|
|
1451
|
+
const job = createCronJob(deriveBirdName(body.prompt), body.schedule.trim(), body.prompt.trim(), body.channel?.trim() || void 0, body.agent?.trim() || void 0, body.activeHoursStart?.trim() || void 0, body.activeHoursEnd?.trim() || void 0);
|
|
1421
1452
|
broadcastSSE("invalidate", { resource: "birds" });
|
|
1422
1453
|
json(res, job, 201);
|
|
1423
1454
|
}
|
|
@@ -1441,7 +1472,6 @@ function buildRoutes(getConfig, startedAt, getDeps, options) {
|
|
|
1441
1472
|
name: body.prompt ? deriveBirdName(body.prompt) : void 0,
|
|
1442
1473
|
targetChannelId: body.channel !== void 0 ? body.channel?.trim() || null : void 0,
|
|
1443
1474
|
agentId: body.agent?.trim() || void 0,
|
|
1444
|
-
timezone: body.timezone?.trim() || void 0,
|
|
1445
1475
|
activeHoursStart: body.activeHoursStart !== void 0 ? body.activeHoursStart?.trim() || null : void 0,
|
|
1446
1476
|
activeHoursEnd: body.activeHoursEnd !== void 0 ? body.activeHoursEnd?.trim() || null : void 0
|
|
1447
1477
|
});
|
|
@@ -2121,7 +2151,10 @@ function buildCommand$1(options) {
|
|
|
2121
2151
|
String(agent.maxTurns)
|
|
2122
2152
|
];
|
|
2123
2153
|
if (sessionId) args.push("--resume", sessionId);
|
|
2124
|
-
|
|
2154
|
+
const systemParts = [];
|
|
2155
|
+
if (agent.systemPrompt) systemParts.push(agent.systemPrompt);
|
|
2156
|
+
if (options.timezone) systemParts.push(`System timezone: ${options.timezone}. All cron expressions and scheduled times use this timezone.`);
|
|
2157
|
+
if (systemParts.length > 0) args.push("--append-system-prompt", systemParts.join(" "));
|
|
2125
2158
|
if (mcpConfigPath) args.push("--mcp-config", mcpConfigPath);
|
|
2126
2159
|
if (agent.fallbackModel) args.push("--fallback-model", agent.fallbackModel);
|
|
2127
2160
|
args.push("--dangerously-skip-permissions");
|
|
@@ -2210,6 +2243,10 @@ function parseAssistantContent(parsed) {
|
|
|
2210
2243
|
type: "text_delta",
|
|
2211
2244
|
delta: b["text"]
|
|
2212
2245
|
});
|
|
2246
|
+
else if (b["type"] === "tool_use" && typeof b["name"] === "string") events.push({
|
|
2247
|
+
type: "tool_use",
|
|
2248
|
+
toolName: b["name"]
|
|
2249
|
+
});
|
|
2213
2250
|
}
|
|
2214
2251
|
return events;
|
|
2215
2252
|
}
|
|
@@ -2315,7 +2352,10 @@ function ensureWorkspace(mcpConfigPath, systemPrompt) {
|
|
|
2315
2352
|
*/
|
|
2316
2353
|
function buildCommand(options) {
|
|
2317
2354
|
const { message, sessionId, agent, mcpConfigPath } = options;
|
|
2318
|
-
|
|
2355
|
+
const systemParts = [];
|
|
2356
|
+
if (agent.systemPrompt) systemParts.push(agent.systemPrompt);
|
|
2357
|
+
if (options.timezone) systemParts.push(`System timezone: ${options.timezone}. All cron expressions and scheduled times use this timezone.`);
|
|
2358
|
+
ensureWorkspace(mcpConfigPath, systemParts.join(" ") || void 0);
|
|
2319
2359
|
const args = [
|
|
2320
2360
|
"run",
|
|
2321
2361
|
"--format",
|
|
@@ -2428,6 +2468,14 @@ function parseStreamLine(line) {
|
|
|
2428
2468
|
accumulators.delete(sid);
|
|
2429
2469
|
return [completion];
|
|
2430
2470
|
}
|
|
2471
|
+
case "tool_use": {
|
|
2472
|
+
const toolName = typeof part?.["tool"] === "string" && part["tool"] || "";
|
|
2473
|
+
if (toolName) return [{
|
|
2474
|
+
type: "tool_use",
|
|
2475
|
+
toolName
|
|
2476
|
+
}];
|
|
2477
|
+
return [];
|
|
2478
|
+
}
|
|
2431
2479
|
case "error": {
|
|
2432
2480
|
const err = parsed["error"];
|
|
2433
2481
|
const data = err?.["data"];
|
|
@@ -2867,31 +2915,10 @@ function truncate(text, maxLength) {
|
|
|
2867
2915
|
return text.slice(0, maxLength) + "...";
|
|
2868
2916
|
}
|
|
2869
2917
|
|
|
2870
|
-
//#endregion
|
|
2871
|
-
//#region src/browser/lock.ts
|
|
2872
|
-
/** @fileoverview SQLite-based browser lock for persistent browser mode. */
|
|
2873
|
-
/**
|
|
2874
|
-
* Attempts to acquire the browser lock for a given holder.
|
|
2875
|
-
* Uses a single conditional UPSERT: inserts if absent, overwrites if stale.
|
|
2876
|
-
* Returns false when an active (non-stale) lock is held by someone else.
|
|
2877
|
-
*/
|
|
2878
|
-
function acquireBrowserLock(holder, timeoutMs) {
|
|
2879
|
-
const timeoutSeconds = Math.ceil(timeoutMs / 1e3);
|
|
2880
|
-
const result = getDb().prepare(`INSERT INTO browser_lock (id, holder, acquired_at)
|
|
2881
|
-
VALUES (1, ?, datetime('now'))
|
|
2882
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
2883
|
-
holder = excluded.holder,
|
|
2884
|
-
acquired_at = excluded.acquired_at
|
|
2885
|
-
WHERE acquired_at <= datetime('now', '-' || ? || ' seconds')`).run(holder, timeoutSeconds);
|
|
2886
|
-
return Number(result.changes) > 0;
|
|
2887
|
-
}
|
|
2888
|
-
/** Releases the browser lock only if the caller is the current holder. */
|
|
2889
|
-
function releaseBrowserLock(holder) {
|
|
2890
|
-
getDb().prepare("DELETE FROM browser_lock WHERE id = 1 AND holder = ?").run(holder);
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
2918
|
//#endregion
|
|
2894
2919
|
//#region src/cron/scheduler.ts
|
|
2920
|
+
const BROWSER_TOOL_PREFIX$1 = "mcp__playwright__";
|
|
2921
|
+
const LOCK_HEARTBEAT_MS$1 = 3e4;
|
|
2895
2922
|
const TICK_INTERVAL_MS = 6e4;
|
|
2896
2923
|
const MAX_SCHEDULE_ERRORS = 3;
|
|
2897
2924
|
const systemHandlers = /* @__PURE__ */ new Map();
|
|
@@ -2928,19 +2955,26 @@ function startScheduler(config, signal, deps) {
|
|
|
2928
2955
|
const agent = config.agents.find((a) => a.id === payload.agentId);
|
|
2929
2956
|
if (!agent) throw new Error(`agent "${payload.agentId}" not found`);
|
|
2930
2957
|
const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
}
|
|
2958
|
+
let browserLockAcquired = false;
|
|
2959
|
+
let heartbeatTimer;
|
|
2934
2960
|
try {
|
|
2935
2961
|
const { events } = spawnProvider(agent.provider, {
|
|
2936
2962
|
message: payload.prompt,
|
|
2937
2963
|
agent,
|
|
2938
|
-
mcpConfigPath: config.browser.mcpConfigPath
|
|
2964
|
+
mcpConfigPath: config.browser.mcpConfigPath,
|
|
2965
|
+
timezone: config.timezone
|
|
2939
2966
|
}, signal);
|
|
2940
2967
|
if (payload.channelId) logMessage(payload.channelId, null, agent.id, "in", payload.prompt);
|
|
2941
2968
|
let result = "";
|
|
2942
2969
|
let completion;
|
|
2943
|
-
for await (const event of events) if (event.type === "
|
|
2970
|
+
for await (const event of events) if (event.type === "tool_use") {
|
|
2971
|
+
if (needsBrowserLock && !browserLockAcquired && event.toolName.startsWith(BROWSER_TOOL_PREFIX$1)) {
|
|
2972
|
+
if (!acquireBrowserLock(payload.cronJobUid, config.sessions.processTimeoutMs)) throw new Error("browser is locked by another session");
|
|
2973
|
+
browserLockAcquired = true;
|
|
2974
|
+
heartbeatTimer = setInterval(() => refreshBrowserLock(payload.cronJobUid), LOCK_HEARTBEAT_MS$1);
|
|
2975
|
+
logger.info(`browser lock acquired lazily for bird ${shortUid(payload.cronJobUid)}`);
|
|
2976
|
+
}
|
|
2977
|
+
} else if (event.type === "text_delta") result += redact(event.delta);
|
|
2944
2978
|
else if (event.type === "completion") completion = event;
|
|
2945
2979
|
else if (event.type === "rate_limit") logger.debug(`bird ${shortUid(payload.cronJobUid)} rate limit window resets ${(/* @__PURE__ */ new Date(event.resetsAt * 1e3)).toISOString()}`);
|
|
2946
2980
|
else if (event.type === "error") {
|
|
@@ -2967,7 +3001,8 @@ function startScheduler(config, signal, deps) {
|
|
|
2967
3001
|
} else logger.info(`bird ${shortUid(payload.cronJobUid)} completed (${result.length} chars)`);
|
|
2968
3002
|
return result;
|
|
2969
3003
|
} finally {
|
|
2970
|
-
if (
|
|
3004
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
3005
|
+
if (browserLockAcquired) releaseBrowserLock(payload.cronJobUid);
|
|
2971
3006
|
}
|
|
2972
3007
|
});
|
|
2973
3008
|
registerHandler("system_cron_run", (raw) => {
|
|
@@ -3000,8 +3035,8 @@ function startScheduler(config, signal, deps) {
|
|
|
3000
3035
|
} else logger.warn(`bird ${shortUid(job.uid)}: invalid expression "${job.schedule}" (attempt ${count}/${MAX_SCHEDULE_ERRORS})`);
|
|
3001
3036
|
continue;
|
|
3002
3037
|
}
|
|
3003
|
-
if (!matchesCron(schedule, now,
|
|
3004
|
-
if (!isWithinActiveHours(job.active_hours_start, job.active_hours_end, now,
|
|
3038
|
+
if (!matchesCron(schedule, now, config.timezone)) continue;
|
|
3039
|
+
if (!isWithinActiveHours(job.active_hours_start, job.active_hours_end, now, config.timezone)) {
|
|
3005
3040
|
logger.debug(`bird ${shortUid(job.uid)} skipped: outside active hours`);
|
|
3006
3041
|
continue;
|
|
3007
3042
|
}
|
|
@@ -3101,6 +3136,8 @@ function createCoalescer(config, onDispatch) {
|
|
|
3101
3136
|
|
|
3102
3137
|
//#endregion
|
|
3103
3138
|
//#region src/channel/handler.ts
|
|
3139
|
+
const BROWSER_TOOL_PREFIX = "mcp__playwright__";
|
|
3140
|
+
const LOCK_HEARTBEAT_MS = 3e4;
|
|
3104
3141
|
function createHandler(client, config, signal, getTeamId) {
|
|
3105
3142
|
const locks = /* @__PURE__ */ new Map();
|
|
3106
3143
|
let activeSpawns = 0;
|
|
@@ -3148,6 +3185,9 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3148
3185
|
case "tool_images":
|
|
3149
3186
|
await uploadImages(event.images, channelId, threadTs);
|
|
3150
3187
|
break;
|
|
3188
|
+
case "tool_use":
|
|
3189
|
+
meta.onToolUse?.(event.toolName);
|
|
3190
|
+
break;
|
|
3151
3191
|
case "completion":
|
|
3152
3192
|
completion = event;
|
|
3153
3193
|
logger.info(`completion [${event.subtype}]: ${event.tokensIn}in/${event.tokensOut}out, $${event.costUsd.toFixed(4)}, ${event.numTurns} turns`);
|
|
@@ -3200,11 +3240,8 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3200
3240
|
return;
|
|
3201
3241
|
}
|
|
3202
3242
|
const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
lock.queue.push(dispatch);
|
|
3206
|
-
return;
|
|
3207
|
-
}
|
|
3243
|
+
let browserLockAcquired = false;
|
|
3244
|
+
let heartbeatTimer;
|
|
3208
3245
|
lock.processing = true;
|
|
3209
3246
|
activeSpawns++;
|
|
3210
3247
|
let sessionUid;
|
|
@@ -3227,7 +3264,8 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3227
3264
|
message: prompt,
|
|
3228
3265
|
sessionId: existingSessionId,
|
|
3229
3266
|
agent,
|
|
3230
|
-
mcpConfigPath: config.browser.mcpConfigPath
|
|
3267
|
+
mcpConfigPath: config.browser.mcpConfigPath,
|
|
3268
|
+
timezone: config.timezone
|
|
3231
3269
|
}, signal);
|
|
3232
3270
|
lock.killCurrent = kill;
|
|
3233
3271
|
client.setStatus?.(channelId, threadTs, "is thinking...").catch(() => {});
|
|
@@ -3235,7 +3273,22 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3235
3273
|
const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
|
|
3236
3274
|
client.setTitle?.(channelId, threadTs, title).catch(() => {});
|
|
3237
3275
|
}
|
|
3238
|
-
|
|
3276
|
+
const onToolUse = (toolName) => {
|
|
3277
|
+
if (!needsBrowserLock || browserLockAcquired) return;
|
|
3278
|
+
if (!toolName.startsWith(BROWSER_TOOL_PREFIX)) return;
|
|
3279
|
+
if (acquireBrowserLock(key, config.sessions.processTimeoutMs)) {
|
|
3280
|
+
browserLockAcquired = true;
|
|
3281
|
+
heartbeatTimer = setInterval(() => refreshBrowserLock(key), LOCK_HEARTBEAT_MS);
|
|
3282
|
+
logger.info(`browser lock acquired lazily for ${key} (tool: ${toolName})`);
|
|
3283
|
+
} else {
|
|
3284
|
+
logger.warn(`browser lock unavailable for ${key} (tool: ${toolName})`);
|
|
3285
|
+
client.postMessage(channelId, threadTs, "The browser is in use by another session.").catch(() => {});
|
|
3286
|
+
}
|
|
3287
|
+
};
|
|
3288
|
+
await streamToChannel(events, channelId, threadTs, session.uid, getTeamId(), userId, {
|
|
3289
|
+
birdName: agent.name,
|
|
3290
|
+
onToolUse
|
|
3291
|
+
});
|
|
3239
3292
|
} catch (err) {
|
|
3240
3293
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3241
3294
|
logger.error(`handler error: ${errMsg}`);
|
|
@@ -3245,7 +3298,8 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3245
3298
|
await client.postMessage(channelId, threadTs, `Something went wrong: ${errMsg}`, { blocks });
|
|
3246
3299
|
} catch {}
|
|
3247
3300
|
} finally {
|
|
3248
|
-
if (
|
|
3301
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
3302
|
+
if (browserLockAcquired) releaseBrowserLock(key);
|
|
3249
3303
|
activeSpawns--;
|
|
3250
3304
|
lock.processing = false;
|
|
3251
3305
|
lock.killCurrent = null;
|
|
@@ -3656,7 +3710,7 @@ function createSlackChannel(config, signal) {
|
|
|
3656
3710
|
const interactionType = body["type"];
|
|
3657
3711
|
if (interactionType === "view_submission") {
|
|
3658
3712
|
const view = body["view"];
|
|
3659
|
-
if (view?.["callback_id"] === "bird_create") await handleBirdCreateSubmission(view, webClient
|
|
3713
|
+
if (view?.["callback_id"] === "bird_create") await handleBirdCreateSubmission(view, webClient);
|
|
3660
3714
|
}
|
|
3661
3715
|
if (interactionType === "block_actions") {
|
|
3662
3716
|
const actionsArr = body["actions"];
|
|
@@ -3769,7 +3823,7 @@ function createSlackChannel(config, signal) {
|
|
|
3769
3823
|
postMessage
|
|
3770
3824
|
};
|
|
3771
3825
|
}
|
|
3772
|
-
async function handleBirdCreateSubmission(view, webClient
|
|
3826
|
+
async function handleBirdCreateSubmission(view, webClient) {
|
|
3773
3827
|
try {
|
|
3774
3828
|
const stateValues = view["state"]?.["values"] ?? {};
|
|
3775
3829
|
const name = stateValues["bird_name"]?.["name_input"]?.["value"] ?? "";
|
|
@@ -3781,8 +3835,8 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
|
|
|
3781
3835
|
logger.warn("bird_create submission missing required fields");
|
|
3782
3836
|
return;
|
|
3783
3837
|
}
|
|
3784
|
-
const { createCronJob, setCronJobEnabled } = await import("./db-
|
|
3785
|
-
const bird = createCronJob(name, schedule, prompt, channelId || void 0, "default"
|
|
3838
|
+
const { createCronJob, setCronJobEnabled } = await import("./db-Da2zSpkY.mjs").then((n) => n.t);
|
|
3839
|
+
const bird = createCronJob(name, schedule, prompt, channelId || void 0, "default");
|
|
3786
3840
|
if (enabledValue !== "enabled") setCronJobEnabled(bird.uid, false);
|
|
3787
3841
|
await webClient.chat.postMessage({
|
|
3788
3842
|
channel: channelId || "general",
|
|
@@ -3795,7 +3849,7 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
|
|
|
3795
3849
|
}
|
|
3796
3850
|
async function handleSessionRetry(sessionUid, channelId, userId, config, handler) {
|
|
3797
3851
|
try {
|
|
3798
|
-
const { getSession, getLastInboundMessage } = await import("./db-
|
|
3852
|
+
const { getSession, getLastInboundMessage } = await import("./db-Da2zSpkY.mjs").then((n) => n.t);
|
|
3799
3853
|
const session = getSession(sessionUid);
|
|
3800
3854
|
if (!session) {
|
|
3801
3855
|
logger.warn(`retry: session ${sessionUid} not found`);
|
|
@@ -3872,6 +3926,7 @@ async function startDaemon(options) {
|
|
|
3872
3926
|
const configDir = dirname(configPath);
|
|
3873
3927
|
const envPath = resolve(configDir, ".env");
|
|
3874
3928
|
openDatabase(resolveDbPath(options.flags.db));
|
|
3929
|
+
clearBrowserLock();
|
|
3875
3930
|
startWorker(controller.signal);
|
|
3876
3931
|
loadDotEnv(envPath);
|
|
3877
3932
|
let currentConfig = loadConfig(configPath);
|
|
@@ -4102,7 +4157,6 @@ ${c("dim", "options:")}
|
|
|
4102
4157
|
${c("yellow", "--agent")} <id> target agent id
|
|
4103
4158
|
${c("yellow", "--schedule")} <expr> cron schedule expression
|
|
4104
4159
|
${c("yellow", "--prompt")} <text> prompt text
|
|
4105
|
-
${c("yellow", "--timezone")} <tz> IANA timezone (default: UTC)
|
|
4106
4160
|
${c("yellow", "--active-hours")} <range> restrict runs to a time window (e.g. "09:00-17:00")
|
|
4107
4161
|
${c("yellow", "--limit")} <n> number of flights to show (default: 10)
|
|
4108
4162
|
${c("yellow", "--json")} output as JSON (with list, flights)
|
|
@@ -4150,7 +4204,6 @@ function handleBirds(argv) {
|
|
|
4150
4204
|
agent: { type: "string" },
|
|
4151
4205
|
schedule: { type: "string" },
|
|
4152
4206
|
prompt: { type: "string" },
|
|
4153
|
-
timezone: { type: "string" },
|
|
4154
4207
|
"active-hours": { type: "string" },
|
|
4155
4208
|
limit: { type: "string" },
|
|
4156
4209
|
json: {
|
|
@@ -4224,7 +4277,7 @@ function handleBirds(argv) {
|
|
|
4224
4277
|
activeStart = parsed.start;
|
|
4225
4278
|
activeEnd = parsed.end;
|
|
4226
4279
|
}
|
|
4227
|
-
const job = createCronJob(deriveBirdName(prompt), schedule, prompt, values.channel, values.agent,
|
|
4280
|
+
const job = createCronJob(deriveBirdName(prompt), schedule, prompt, values.channel, values.agent, activeStart, activeEnd);
|
|
4228
4281
|
logger.success(`bird ${shortUid(job.uid)} created: "${schedule}"`);
|
|
4229
4282
|
process.stderr.write(c("dim", ` hint: run 'browserbird birds fly ${shortUid(job.uid)}' to trigger it now`) + "\n");
|
|
4230
4283
|
break;
|
|
@@ -4232,7 +4285,7 @@ function handleBirds(argv) {
|
|
|
4232
4285
|
case "edit": {
|
|
4233
4286
|
const uidPrefix = positionals[0];
|
|
4234
4287
|
if (!uidPrefix) {
|
|
4235
|
-
logger.error("usage: browserbird birds edit <uid> [--schedule <expr>] [--prompt <text>] [--channel <id>] [--agent <id>] [--
|
|
4288
|
+
logger.error("usage: browserbird birds edit <uid> [--schedule <expr>] [--prompt <text>] [--channel <id>] [--agent <id>] [--active-hours <range>]");
|
|
4236
4289
|
process.exitCode = 1;
|
|
4237
4290
|
return;
|
|
4238
4291
|
}
|
|
@@ -4242,7 +4295,6 @@ function handleBirds(argv) {
|
|
|
4242
4295
|
const agent = values.agent;
|
|
4243
4296
|
const schedule = values.schedule;
|
|
4244
4297
|
const prompt = values.prompt;
|
|
4245
|
-
const timezone = values.timezone;
|
|
4246
4298
|
const editActiveHoursRaw = values["active-hours"];
|
|
4247
4299
|
let editActiveStart;
|
|
4248
4300
|
let editActiveEnd;
|
|
@@ -4259,8 +4311,8 @@ function handleBirds(argv) {
|
|
|
4259
4311
|
editActiveStart = parsed.start;
|
|
4260
4312
|
editActiveEnd = parsed.end;
|
|
4261
4313
|
}
|
|
4262
|
-
if (!schedule && !prompt && !channel && !agent &&
|
|
4263
|
-
logger.error("provide at least one of: --schedule, --prompt, --channel, --agent, --
|
|
4314
|
+
if (!schedule && !prompt && !channel && !agent && editActiveStart === void 0) {
|
|
4315
|
+
logger.error("provide at least one of: --schedule, --prompt, --channel, --agent, --active-hours");
|
|
4264
4316
|
process.exitCode = 1;
|
|
4265
4317
|
return;
|
|
4266
4318
|
}
|
|
@@ -4270,7 +4322,6 @@ function handleBirds(argv) {
|
|
|
4270
4322
|
name: prompt ? deriveBirdName(prompt) : void 0,
|
|
4271
4323
|
targetChannelId: channel !== void 0 ? channel || null : void 0,
|
|
4272
4324
|
agentId: agent,
|
|
4273
|
-
timezone,
|
|
4274
4325
|
activeHoursStart: editActiveStart,
|
|
4275
4326
|
activeHoursEnd: editActiveEnd
|
|
4276
4327
|
})) {
|