Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions apps/api/src/browserbase/browserbase-session.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,26 @@ describe('BrowserbaseSessionService', () => {
expect(close).toHaveBeenCalledTimes(1);
});

it('runs Stagehand with the hosted API disabled', async () => {
const service = new BrowserbaseSessionService();
const init = jest.fn().mockResolvedValue(undefined);
const close = jest.fn().mockResolvedValue(undefined);
const StagehandCtor = mockStagehandClass({ init, close });
jest.spyOn(service, 'loadStagehand').mockResolvedValue(StagehandCtor);

await service.createStagehand('session_1');

// disableAPI:true skips Stagehand's hosted POST /sessions/start (the source
// of "Unknown error: 400"); the session still resumes over CDP.
expect(StagehandCtor).toHaveBeenCalledWith(
expect.objectContaining({
env: 'BROWSERBASE',
browserbaseSessionID: 'session_1',
disableAPI: true,
}),
);
});

it('navigateToUrl returns the friendly unavailable message when init keeps failing', async () => {
const service = new BrowserbaseSessionService();
jest
Expand Down
25 changes: 20 additions & 5 deletions apps/api/src/browserbase/browserbase-session.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,18 @@ export class BrowserbaseSessionService {
async createStagehand(sessionId: string): Promise<Stagehand> {
const Stagehand = await this.loadStagehand();

// Stagehand.init() resumes the session by calling the Browserbase API on
// its own internal SDK client (outside getBrowserbase()), so transient
// upstream failures there — e.g. "Premature close" — bypass the per-call
// retry that wraps our direct SDK calls. Retry init the same way, closing
// any half-initialized instance between attempts so we don't leak it.
// We create and own the Browserbase session ourselves, and this feature only
// needs CDP navigation plus local inference. Stagehand's default hosted-API
// mode adds a POST /sessions/start round-trip that is unnecessary here and is
// the source of opaque "Unknown error: <status>" failures. disableAPI:true
// skips it: the session still resumes over CDP and extract/act/agent run
// locally against ANTHROPIC_API_KEY.
//
// init() still performs a Browserbase API round-trip to resume the session,
// whose transient failures — e.g. "Premature close" — bypass the retry that
// wraps our direct SDK calls, so retry init too, closing any half-initialized
// instance between attempts to avoid leaking it. Stagehand strips upstream
// error bodies from its throws, so forward its error logs into our logger.
return this.withBrowserbaseRetry({
operationName: 'stagehand initialization',
operation: async () => {
Expand All @@ -180,11 +187,19 @@ export class BrowserbaseSessionService {
apiKey: process.env.BROWSERBASE_API_KEY,
projectId: this.getProjectId(),
browserbaseSessionID: sessionId,
disableAPI: true,
model: {
modelName: STAGEHAND_MODEL,
apiKey: process.env.ANTHROPIC_API_KEY,
},
verbose: 1,
logger: (line) => {
if ((line.level ?? 1) === 0) {
this.logger.error(
`Stagehand[${line.category ?? 'log'}]: ${line.message}`,
);
}
},
});

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ async function sendTaskStatusChangeEmails(params: {
*/
export const runBrowserAutomation = task({
id: 'run-browser-automation',
maxDuration: 1000 * 60 * 10, // 10 minutes per automation
maxDuration: 60 * 10, // 10 minutes per automation — Trigger.dev maxDuration is in SECONDS
queue: {
concurrencyLimit: browserAutomationConcurrencyLimit(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function limitAutomationBatch<
export const browserAutomationsSchedule = schedules.task({
id: 'browser-automations-schedule',
cron: '0 5 * * *', // Daily at 5:00 AM UTC
maxDuration: 1000 * 60 * 30, // 30 minutes
maxDuration: 60 * 30, // 30 minutes — Trigger.dev maxDuration is in SECONDS
run: async (payload) => {
logger.info('Starting daily browser automations orchestrator', {
scheduledAt: payload.timestamp,
Expand Down
Loading