@owloops/browserbird 1.2.5 → 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 +115 -61
- 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
|
});
|
|
@@ -2106,6 +2136,7 @@ function splitLines(buffer, chunk) {
|
|
|
2106
2136
|
|
|
2107
2137
|
//#endregion
|
|
2108
2138
|
//#region src/provider/claude.ts
|
|
2139
|
+
/** @fileoverview Claude Code CLI provider: arg building and stream-json parsing. */
|
|
2109
2140
|
function buildCommand$1(options) {
|
|
2110
2141
|
const { message, sessionId, agent, mcpConfigPath } = options;
|
|
2111
2142
|
const args = [
|
|
@@ -2120,16 +2151,21 @@ function buildCommand$1(options) {
|
|
|
2120
2151
|
String(agent.maxTurns)
|
|
2121
2152
|
];
|
|
2122
2153
|
if (sessionId) args.push("--resume", sessionId);
|
|
2123
|
-
|
|
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(" "));
|
|
2124
2158
|
if (mcpConfigPath) args.push("--mcp-config", mcpConfigPath);
|
|
2125
2159
|
if (agent.fallbackModel) args.push("--fallback-model", agent.fallbackModel);
|
|
2126
2160
|
args.push("--dangerously-skip-permissions");
|
|
2127
2161
|
const oauthToken = process.env["CLAUDE_CODE_OAUTH_TOKEN"];
|
|
2128
2162
|
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
2163
|
+
const env = oauthToken ? { CLAUDE_CODE_OAUTH_TOKEN: oauthToken } : apiKey ? { ANTHROPIC_API_KEY: apiKey } : {};
|
|
2164
|
+
env["CLAUDE_CONFIG_DIR"] = resolve(".browserbird", "claude");
|
|
2129
2165
|
return {
|
|
2130
2166
|
binary: "claude",
|
|
2131
2167
|
args,
|
|
2132
|
-
env
|
|
2168
|
+
env
|
|
2133
2169
|
};
|
|
2134
2170
|
}
|
|
2135
2171
|
/**
|
|
@@ -2207,6 +2243,10 @@ function parseAssistantContent(parsed) {
|
|
|
2207
2243
|
type: "text_delta",
|
|
2208
2244
|
delta: b["text"]
|
|
2209
2245
|
});
|
|
2246
|
+
else if (b["type"] === "tool_use" && typeof b["name"] === "string") events.push({
|
|
2247
|
+
type: "tool_use",
|
|
2248
|
+
toolName: b["name"]
|
|
2249
|
+
});
|
|
2210
2250
|
}
|
|
2211
2251
|
return events;
|
|
2212
2252
|
}
|
|
@@ -2312,7 +2352,10 @@ function ensureWorkspace(mcpConfigPath, systemPrompt) {
|
|
|
2312
2352
|
*/
|
|
2313
2353
|
function buildCommand(options) {
|
|
2314
2354
|
const { message, sessionId, agent, mcpConfigPath } = options;
|
|
2315
|
-
|
|
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);
|
|
2316
2359
|
const args = [
|
|
2317
2360
|
"run",
|
|
2318
2361
|
"--format",
|
|
@@ -2425,6 +2468,14 @@ function parseStreamLine(line) {
|
|
|
2425
2468
|
accumulators.delete(sid);
|
|
2426
2469
|
return [completion];
|
|
2427
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
|
+
}
|
|
2428
2479
|
case "error": {
|
|
2429
2480
|
const err = parsed["error"];
|
|
2430
2481
|
const data = err?.["data"];
|
|
@@ -2864,31 +2915,10 @@ function truncate(text, maxLength) {
|
|
|
2864
2915
|
return text.slice(0, maxLength) + "...";
|
|
2865
2916
|
}
|
|
2866
2917
|
|
|
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
|
-
|
|
2890
2918
|
//#endregion
|
|
2891
2919
|
//#region src/cron/scheduler.ts
|
|
2920
|
+
const BROWSER_TOOL_PREFIX$1 = "mcp__playwright__";
|
|
2921
|
+
const LOCK_HEARTBEAT_MS$1 = 3e4;
|
|
2892
2922
|
const TICK_INTERVAL_MS = 6e4;
|
|
2893
2923
|
const MAX_SCHEDULE_ERRORS = 3;
|
|
2894
2924
|
const systemHandlers = /* @__PURE__ */ new Map();
|
|
@@ -2925,19 +2955,26 @@ function startScheduler(config, signal, deps) {
|
|
|
2925
2955
|
const agent = config.agents.find((a) => a.id === payload.agentId);
|
|
2926
2956
|
if (!agent) throw new Error(`agent "${payload.agentId}" not found`);
|
|
2927
2957
|
const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
}
|
|
2958
|
+
let browserLockAcquired = false;
|
|
2959
|
+
let heartbeatTimer;
|
|
2931
2960
|
try {
|
|
2932
2961
|
const { events } = spawnProvider(agent.provider, {
|
|
2933
2962
|
message: payload.prompt,
|
|
2934
2963
|
agent,
|
|
2935
|
-
mcpConfigPath: config.browser.mcpConfigPath
|
|
2964
|
+
mcpConfigPath: config.browser.mcpConfigPath,
|
|
2965
|
+
timezone: config.timezone
|
|
2936
2966
|
}, signal);
|
|
2937
2967
|
if (payload.channelId) logMessage(payload.channelId, null, agent.id, "in", payload.prompt);
|
|
2938
2968
|
let result = "";
|
|
2939
2969
|
let completion;
|
|
2940
|
-
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);
|
|
2941
2978
|
else if (event.type === "completion") completion = event;
|
|
2942
2979
|
else if (event.type === "rate_limit") logger.debug(`bird ${shortUid(payload.cronJobUid)} rate limit window resets ${(/* @__PURE__ */ new Date(event.resetsAt * 1e3)).toISOString()}`);
|
|
2943
2980
|
else if (event.type === "error") {
|
|
@@ -2964,7 +3001,8 @@ function startScheduler(config, signal, deps) {
|
|
|
2964
3001
|
} else logger.info(`bird ${shortUid(payload.cronJobUid)} completed (${result.length} chars)`);
|
|
2965
3002
|
return result;
|
|
2966
3003
|
} finally {
|
|
2967
|
-
if (
|
|
3004
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
3005
|
+
if (browserLockAcquired) releaseBrowserLock(payload.cronJobUid);
|
|
2968
3006
|
}
|
|
2969
3007
|
});
|
|
2970
3008
|
registerHandler("system_cron_run", (raw) => {
|
|
@@ -2997,8 +3035,8 @@ function startScheduler(config, signal, deps) {
|
|
|
2997
3035
|
} else logger.warn(`bird ${shortUid(job.uid)}: invalid expression "${job.schedule}" (attempt ${count}/${MAX_SCHEDULE_ERRORS})`);
|
|
2998
3036
|
continue;
|
|
2999
3037
|
}
|
|
3000
|
-
if (!matchesCron(schedule, now,
|
|
3001
|
-
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)) {
|
|
3002
3040
|
logger.debug(`bird ${shortUid(job.uid)} skipped: outside active hours`);
|
|
3003
3041
|
continue;
|
|
3004
3042
|
}
|
|
@@ -3098,6 +3136,8 @@ function createCoalescer(config, onDispatch) {
|
|
|
3098
3136
|
|
|
3099
3137
|
//#endregion
|
|
3100
3138
|
//#region src/channel/handler.ts
|
|
3139
|
+
const BROWSER_TOOL_PREFIX = "mcp__playwright__";
|
|
3140
|
+
const LOCK_HEARTBEAT_MS = 3e4;
|
|
3101
3141
|
function createHandler(client, config, signal, getTeamId) {
|
|
3102
3142
|
const locks = /* @__PURE__ */ new Map();
|
|
3103
3143
|
let activeSpawns = 0;
|
|
@@ -3145,6 +3185,9 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3145
3185
|
case "tool_images":
|
|
3146
3186
|
await uploadImages(event.images, channelId, threadTs);
|
|
3147
3187
|
break;
|
|
3188
|
+
case "tool_use":
|
|
3189
|
+
meta.onToolUse?.(event.toolName);
|
|
3190
|
+
break;
|
|
3148
3191
|
case "completion":
|
|
3149
3192
|
completion = event;
|
|
3150
3193
|
logger.info(`completion [${event.subtype}]: ${event.tokensIn}in/${event.tokensOut}out, $${event.costUsd.toFixed(4)}, ${event.numTurns} turns`);
|
|
@@ -3197,11 +3240,8 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3197
3240
|
return;
|
|
3198
3241
|
}
|
|
3199
3242
|
const needsBrowserLock = config.browser.enabled && getBrowserMode() === "persistent";
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
lock.queue.push(dispatch);
|
|
3203
|
-
return;
|
|
3204
|
-
}
|
|
3243
|
+
let browserLockAcquired = false;
|
|
3244
|
+
let heartbeatTimer;
|
|
3205
3245
|
lock.processing = true;
|
|
3206
3246
|
activeSpawns++;
|
|
3207
3247
|
let sessionUid;
|
|
@@ -3224,7 +3264,8 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3224
3264
|
message: prompt,
|
|
3225
3265
|
sessionId: existingSessionId,
|
|
3226
3266
|
agent,
|
|
3227
|
-
mcpConfigPath: config.browser.mcpConfigPath
|
|
3267
|
+
mcpConfigPath: config.browser.mcpConfigPath,
|
|
3268
|
+
timezone: config.timezone
|
|
3228
3269
|
}, signal);
|
|
3229
3270
|
lock.killCurrent = kill;
|
|
3230
3271
|
client.setStatus?.(channelId, threadTs, "is thinking...").catch(() => {});
|
|
@@ -3232,7 +3273,22 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3232
3273
|
const title = prompt.length > 60 ? prompt.slice(0, 57) + "..." : prompt;
|
|
3233
3274
|
client.setTitle?.(channelId, threadTs, title).catch(() => {});
|
|
3234
3275
|
}
|
|
3235
|
-
|
|
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
|
+
});
|
|
3236
3292
|
} catch (err) {
|
|
3237
3293
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3238
3294
|
logger.error(`handler error: ${errMsg}`);
|
|
@@ -3242,7 +3298,8 @@ function createHandler(client, config, signal, getTeamId) {
|
|
|
3242
3298
|
await client.postMessage(channelId, threadTs, `Something went wrong: ${errMsg}`, { blocks });
|
|
3243
3299
|
} catch {}
|
|
3244
3300
|
} finally {
|
|
3245
|
-
if (
|
|
3301
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
3302
|
+
if (browserLockAcquired) releaseBrowserLock(key);
|
|
3246
3303
|
activeSpawns--;
|
|
3247
3304
|
lock.processing = false;
|
|
3248
3305
|
lock.killCurrent = null;
|
|
@@ -3653,7 +3710,7 @@ function createSlackChannel(config, signal) {
|
|
|
3653
3710
|
const interactionType = body["type"];
|
|
3654
3711
|
if (interactionType === "view_submission") {
|
|
3655
3712
|
const view = body["view"];
|
|
3656
|
-
if (view?.["callback_id"] === "bird_create") await handleBirdCreateSubmission(view, webClient
|
|
3713
|
+
if (view?.["callback_id"] === "bird_create") await handleBirdCreateSubmission(view, webClient);
|
|
3657
3714
|
}
|
|
3658
3715
|
if (interactionType === "block_actions") {
|
|
3659
3716
|
const actionsArr = body["actions"];
|
|
@@ -3766,7 +3823,7 @@ function createSlackChannel(config, signal) {
|
|
|
3766
3823
|
postMessage
|
|
3767
3824
|
};
|
|
3768
3825
|
}
|
|
3769
|
-
async function handleBirdCreateSubmission(view, webClient
|
|
3826
|
+
async function handleBirdCreateSubmission(view, webClient) {
|
|
3770
3827
|
try {
|
|
3771
3828
|
const stateValues = view["state"]?.["values"] ?? {};
|
|
3772
3829
|
const name = stateValues["bird_name"]?.["name_input"]?.["value"] ?? "";
|
|
@@ -3778,8 +3835,8 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
|
|
|
3778
3835
|
logger.warn("bird_create submission missing required fields");
|
|
3779
3836
|
return;
|
|
3780
3837
|
}
|
|
3781
|
-
const { createCronJob, setCronJobEnabled } = await import("./db-
|
|
3782
|
-
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");
|
|
3783
3840
|
if (enabledValue !== "enabled") setCronJobEnabled(bird.uid, false);
|
|
3784
3841
|
await webClient.chat.postMessage({
|
|
3785
3842
|
channel: channelId || "general",
|
|
@@ -3792,7 +3849,7 @@ async function handleBirdCreateSubmission(view, webClient, defaultTimezone) {
|
|
|
3792
3849
|
}
|
|
3793
3850
|
async function handleSessionRetry(sessionUid, channelId, userId, config, handler) {
|
|
3794
3851
|
try {
|
|
3795
|
-
const { getSession, getLastInboundMessage } = await import("./db-
|
|
3852
|
+
const { getSession, getLastInboundMessage } = await import("./db-Da2zSpkY.mjs").then((n) => n.t);
|
|
3796
3853
|
const session = getSession(sessionUid);
|
|
3797
3854
|
if (!session) {
|
|
3798
3855
|
logger.warn(`retry: session ${sessionUid} not found`);
|
|
@@ -3869,6 +3926,7 @@ async function startDaemon(options) {
|
|
|
3869
3926
|
const configDir = dirname(configPath);
|
|
3870
3927
|
const envPath = resolve(configDir, ".env");
|
|
3871
3928
|
openDatabase(resolveDbPath(options.flags.db));
|
|
3929
|
+
clearBrowserLock();
|
|
3872
3930
|
startWorker(controller.signal);
|
|
3873
3931
|
loadDotEnv(envPath);
|
|
3874
3932
|
let currentConfig = loadConfig(configPath);
|
|
@@ -4099,7 +4157,6 @@ ${c("dim", "options:")}
|
|
|
4099
4157
|
${c("yellow", "--agent")} <id> target agent id
|
|
4100
4158
|
${c("yellow", "--schedule")} <expr> cron schedule expression
|
|
4101
4159
|
${c("yellow", "--prompt")} <text> prompt text
|
|
4102
|
-
${c("yellow", "--timezone")} <tz> IANA timezone (default: UTC)
|
|
4103
4160
|
${c("yellow", "--active-hours")} <range> restrict runs to a time window (e.g. "09:00-17:00")
|
|
4104
4161
|
${c("yellow", "--limit")} <n> number of flights to show (default: 10)
|
|
4105
4162
|
${c("yellow", "--json")} output as JSON (with list, flights)
|
|
@@ -4147,7 +4204,6 @@ function handleBirds(argv) {
|
|
|
4147
4204
|
agent: { type: "string" },
|
|
4148
4205
|
schedule: { type: "string" },
|
|
4149
4206
|
prompt: { type: "string" },
|
|
4150
|
-
timezone: { type: "string" },
|
|
4151
4207
|
"active-hours": { type: "string" },
|
|
4152
4208
|
limit: { type: "string" },
|
|
4153
4209
|
json: {
|
|
@@ -4221,7 +4277,7 @@ function handleBirds(argv) {
|
|
|
4221
4277
|
activeStart = parsed.start;
|
|
4222
4278
|
activeEnd = parsed.end;
|
|
4223
4279
|
}
|
|
4224
|
-
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);
|
|
4225
4281
|
logger.success(`bird ${shortUid(job.uid)} created: "${schedule}"`);
|
|
4226
4282
|
process.stderr.write(c("dim", ` hint: run 'browserbird birds fly ${shortUid(job.uid)}' to trigger it now`) + "\n");
|
|
4227
4283
|
break;
|
|
@@ -4229,7 +4285,7 @@ function handleBirds(argv) {
|
|
|
4229
4285
|
case "edit": {
|
|
4230
4286
|
const uidPrefix = positionals[0];
|
|
4231
4287
|
if (!uidPrefix) {
|
|
4232
|
-
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>]");
|
|
4233
4289
|
process.exitCode = 1;
|
|
4234
4290
|
return;
|
|
4235
4291
|
}
|
|
@@ -4239,7 +4295,6 @@ function handleBirds(argv) {
|
|
|
4239
4295
|
const agent = values.agent;
|
|
4240
4296
|
const schedule = values.schedule;
|
|
4241
4297
|
const prompt = values.prompt;
|
|
4242
|
-
const timezone = values.timezone;
|
|
4243
4298
|
const editActiveHoursRaw = values["active-hours"];
|
|
4244
4299
|
let editActiveStart;
|
|
4245
4300
|
let editActiveEnd;
|
|
@@ -4256,8 +4311,8 @@ function handleBirds(argv) {
|
|
|
4256
4311
|
editActiveStart = parsed.start;
|
|
4257
4312
|
editActiveEnd = parsed.end;
|
|
4258
4313
|
}
|
|
4259
|
-
if (!schedule && !prompt && !channel && !agent &&
|
|
4260
|
-
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");
|
|
4261
4316
|
process.exitCode = 1;
|
|
4262
4317
|
return;
|
|
4263
4318
|
}
|
|
@@ -4267,7 +4322,6 @@ function handleBirds(argv) {
|
|
|
4267
4322
|
name: prompt ? deriveBirdName(prompt) : void 0,
|
|
4268
4323
|
targetChannelId: channel !== void 0 ? channel || null : void 0,
|
|
4269
4324
|
agentId: agent,
|
|
4270
|
-
timezone,
|
|
4271
4325
|
activeHoursStart: editActiveStart,
|
|
4272
4326
|
activeHoursEnd: editActiveEnd
|
|
4273
4327
|
})) {
|