@owloops/browserbird 1.0.1 → 1.0.3
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/bin/browserbird +7 -1
- package/dist/db-BsYEYsul.mjs +1011 -0
- package/dist/index.mjs +4748 -0
- package/package.json +6 -3
- package/src/channel/blocks.ts +0 -485
- package/src/channel/coalesce.ts +0 -79
- package/src/channel/commands.ts +0 -216
- package/src/channel/handler.ts +0 -272
- package/src/channel/slack.ts +0 -573
- package/src/channel/types.ts +0 -59
- package/src/cli/banner.ts +0 -10
- package/src/cli/birds.ts +0 -396
- package/src/cli/config.ts +0 -77
- package/src/cli/doctor.ts +0 -63
- package/src/cli/index.ts +0 -5
- package/src/cli/jobs.ts +0 -166
- package/src/cli/logs.ts +0 -67
- package/src/cli/run.ts +0 -148
- package/src/cli/sessions.ts +0 -158
- package/src/cli/style.ts +0 -19
- package/src/config.ts +0 -291
- package/src/core/logger.ts +0 -78
- package/src/core/redact.ts +0 -75
- package/src/core/types.ts +0 -83
- package/src/core/uid.ts +0 -26
- package/src/core/utils.ts +0 -137
- package/src/cron/parse.ts +0 -146
- package/src/cron/scheduler.ts +0 -242
- package/src/daemon.ts +0 -169
- package/src/db/auth.ts +0 -49
- package/src/db/birds.ts +0 -357
- package/src/db/core.ts +0 -377
- package/src/db/index.ts +0 -10
- package/src/db/jobs.ts +0 -289
- package/src/db/logs.ts +0 -64
- package/src/db/messages.ts +0 -79
- package/src/db/path.ts +0 -30
- package/src/db/sessions.ts +0 -165
- package/src/jobs.ts +0 -140
- package/src/provider/claude.test.ts +0 -95
- package/src/provider/claude.ts +0 -196
- package/src/provider/opencode.test.ts +0 -169
- package/src/provider/opencode.ts +0 -248
- package/src/provider/session.ts +0 -65
- package/src/provider/spawn.ts +0 -173
- package/src/provider/stream.ts +0 -67
- package/src/provider/types.ts +0 -24
- package/src/server/auth.ts +0 -135
- package/src/server/health.ts +0 -87
- package/src/server/http.ts +0 -132
- package/src/server/index.ts +0 -6
- package/src/server/lifecycle.ts +0 -135
- package/src/server/routes.ts +0 -1199
- package/src/server/sse.ts +0 -54
- package/src/server/static.ts +0 -45
- package/src/server/vnc-proxy.ts +0 -75
package/src/channel/commands.ts
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Slash command handler for `/bird` commands in Slack. */
|
|
2
|
-
|
|
3
|
-
import type { WebClient } from '@slack/web-api';
|
|
4
|
-
import type { Config } from '../core/types.ts';
|
|
5
|
-
import type { ChannelClient } from './types.ts';
|
|
6
|
-
|
|
7
|
-
import * as db from '../db/index.ts';
|
|
8
|
-
import { enqueue } from '../jobs.ts';
|
|
9
|
-
import { logger } from '../core/logger.ts';
|
|
10
|
-
import {
|
|
11
|
-
birdListBlocks,
|
|
12
|
-
birdFlyBlocks,
|
|
13
|
-
birdLogsBlocks,
|
|
14
|
-
birdCreateModal,
|
|
15
|
-
statusBlocks,
|
|
16
|
-
} from './blocks.ts';
|
|
17
|
-
|
|
18
|
-
export interface SlashCommandBody {
|
|
19
|
-
command: string;
|
|
20
|
-
text: string;
|
|
21
|
-
trigger_id: string;
|
|
22
|
-
user_id: string;
|
|
23
|
-
channel_id: string;
|
|
24
|
-
team_id: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface StatusProvider {
|
|
28
|
-
slackConnected: () => boolean;
|
|
29
|
-
activeCount: () => number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const startTime = Date.now();
|
|
33
|
-
|
|
34
|
-
function formatUptime(): string {
|
|
35
|
-
const ms = Date.now() - startTime;
|
|
36
|
-
const hours = Math.floor(ms / 3_600_000);
|
|
37
|
-
const minutes = Math.floor((ms % 3_600_000) / 60_000);
|
|
38
|
-
if (hours === 0) return `${minutes}m`;
|
|
39
|
-
return `${hours}h ${minutes}m`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function handleSlashCommand(
|
|
43
|
-
body: SlashCommandBody,
|
|
44
|
-
webClient: WebClient,
|
|
45
|
-
channelClient: ChannelClient,
|
|
46
|
-
config: Config,
|
|
47
|
-
status: StatusProvider,
|
|
48
|
-
): Promise<void> {
|
|
49
|
-
const parts = body.text.trim().split(/\s+/);
|
|
50
|
-
const subcommand = parts[0] ?? 'help';
|
|
51
|
-
|
|
52
|
-
async function say(message: { text: string; blocks?: unknown[] }): Promise<void> {
|
|
53
|
-
await webClient.chat.postMessage({
|
|
54
|
-
channel: body.channel_id,
|
|
55
|
-
text: message.text,
|
|
56
|
-
...(message.blocks ? { blocks: message.blocks } : {}),
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function findBird(nameOrUid: string): db.CronJobRow | undefined {
|
|
61
|
-
const byUid = db.resolveByUid<db.CronJobRow>('cron_jobs', nameOrUid);
|
|
62
|
-
if (byUid && 'row' in byUid) return byUid.row;
|
|
63
|
-
const result = db.listCronJobs(1, 100, false);
|
|
64
|
-
return result.items.find((b) => b.name === nameOrUid);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
switch (subcommand) {
|
|
68
|
-
case 'list': {
|
|
69
|
-
const result = db.listCronJobs(1, 20, false);
|
|
70
|
-
const birds = result.items.map((b) => ({
|
|
71
|
-
uid: b.uid,
|
|
72
|
-
name: b.name,
|
|
73
|
-
schedule: b.schedule,
|
|
74
|
-
enabled: b.enabled === 1,
|
|
75
|
-
lastStatus: b.last_status,
|
|
76
|
-
agentId: b.agent_id,
|
|
77
|
-
}));
|
|
78
|
-
const blocks = birdListBlocks(birds);
|
|
79
|
-
await say({
|
|
80
|
-
text: `${birds.length} bird${birds.length === 1 ? '' : 's'} configured`,
|
|
81
|
-
blocks,
|
|
82
|
-
});
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case 'fly': {
|
|
87
|
-
const birdName = parts.slice(1).join(' ');
|
|
88
|
-
if (!birdName) {
|
|
89
|
-
await say({ text: 'Usage: `/bird fly <name or id>`' });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const bird = findBird(birdName);
|
|
94
|
-
if (!bird) {
|
|
95
|
-
await say({ text: `Bird not found: \`${birdName}\`` });
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
enqueue(
|
|
100
|
-
'cron_run',
|
|
101
|
-
{
|
|
102
|
-
cronJobUid: bird.uid,
|
|
103
|
-
prompt: bird.prompt,
|
|
104
|
-
channelId: bird.target_channel_id,
|
|
105
|
-
agentId: bird.agent_id,
|
|
106
|
-
},
|
|
107
|
-
{ maxAttempts: config.birds.maxAttempts, timeout: 600, cronJobUid: bird.uid },
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const blocks = birdFlyBlocks(bird.name, body.user_id);
|
|
111
|
-
await say({ text: `${bird.name} is taking flight...`, blocks });
|
|
112
|
-
logger.info(`/bird fly: ${bird.name} triggered by ${body.user_id}`);
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
case 'enable':
|
|
117
|
-
case 'disable': {
|
|
118
|
-
const birdName = parts.slice(1).join(' ');
|
|
119
|
-
if (!birdName) {
|
|
120
|
-
await say({ text: `Usage: \`/bird ${subcommand} <name or id>\`` });
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const bird = findBird(birdName);
|
|
125
|
-
if (!bird) {
|
|
126
|
-
await say({ text: `Bird not found: \`${birdName}\`` });
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const enabling = subcommand === 'enable';
|
|
131
|
-
const alreadyInState = (bird.enabled === 1) === enabling;
|
|
132
|
-
if (alreadyInState) {
|
|
133
|
-
await say({ text: `*${bird.name}* is already ${subcommand}d.` });
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
db.setCronJobEnabled(bird.uid, enabling);
|
|
138
|
-
await say({ text: `*${bird.name}* ${subcommand}d.` });
|
|
139
|
-
logger.info(`/bird ${subcommand}: ${bird.name} by ${body.user_id}`);
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
case 'logs': {
|
|
144
|
-
const birdName = parts.slice(1).join(' ');
|
|
145
|
-
if (!birdName) {
|
|
146
|
-
await say({ text: 'Usage: `/bird logs <name or id>`' });
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const bird = findBird(birdName);
|
|
151
|
-
if (!bird) {
|
|
152
|
-
await say({ text: `Bird not found: \`${birdName}\`` });
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const flights = db.listFlights(1, 5, { birdUid: bird.uid });
|
|
157
|
-
const mapped = flights.items.map((f) => {
|
|
158
|
-
const durationMs =
|
|
159
|
-
f.finished_at && f.started_at
|
|
160
|
-
? new Date(f.finished_at).getTime() - new Date(f.started_at).getTime()
|
|
161
|
-
: undefined;
|
|
162
|
-
return {
|
|
163
|
-
uid: f.uid,
|
|
164
|
-
status: f.status,
|
|
165
|
-
startedAt: f.started_at,
|
|
166
|
-
durationMs,
|
|
167
|
-
error: f.error ?? undefined,
|
|
168
|
-
};
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const blocks = birdLogsBlocks(bird.name, mapped);
|
|
172
|
-
const text = `${flights.totalItems} flight${flights.totalItems === 1 ? '' : 's'} for ${bird.name}`;
|
|
173
|
-
await say({ text, blocks });
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
case 'create': {
|
|
178
|
-
if (!channelClient.openModal) {
|
|
179
|
-
await say({ text: 'Modals are not supported by this adapter.' });
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const modal = birdCreateModal();
|
|
184
|
-
await channelClient.openModal(body.trigger_id, modal);
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
case 'status': {
|
|
189
|
-
const cronJobs = db.listCronJobs(1, 1, false);
|
|
190
|
-
const blocks = statusBlocks({
|
|
191
|
-
slackConnected: status.slackConnected(),
|
|
192
|
-
activeCount: status.activeCount(),
|
|
193
|
-
maxConcurrent: config.sessions.maxConcurrent,
|
|
194
|
-
birdCount: cronJobs.totalItems,
|
|
195
|
-
uptime: formatUptime(),
|
|
196
|
-
});
|
|
197
|
-
await say({ text: 'BrowserBird status', blocks });
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
default:
|
|
202
|
-
await say({
|
|
203
|
-
text: [
|
|
204
|
-
'*Usage:* `/bird <command>`',
|
|
205
|
-
'',
|
|
206
|
-
'`/bird list` - Show all configured birds',
|
|
207
|
-
'`/bird fly <name>` - Trigger a bird immediately',
|
|
208
|
-
'`/bird logs <name>` - Show recent flights',
|
|
209
|
-
'`/bird enable <name>` - Enable a bird',
|
|
210
|
-
'`/bird disable <name>` - Disable a bird',
|
|
211
|
-
'`/bird create` - Create a new bird (opens form)',
|
|
212
|
-
'`/bird status` - Show daemon status',
|
|
213
|
-
].join('\n'),
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
package/src/channel/handler.ts
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/** @fileoverview Core orchestration: session resolution, spawn, stream-to-channel. */
|
|
2
|
-
|
|
3
|
-
import type { Config } from '../core/types.ts';
|
|
4
|
-
import type { CoalesceDispatch } from './coalesce.ts';
|
|
5
|
-
import type { StreamEvent, StreamEventCompletion, ToolImage } from '../provider/stream.ts';
|
|
6
|
-
import type { ChannelClient } from './types.ts';
|
|
7
|
-
|
|
8
|
-
import { resolveSession } from '../provider/session.ts';
|
|
9
|
-
import { spawnProvider } from '../provider/spawn.ts';
|
|
10
|
-
import * as db from '../db/index.ts';
|
|
11
|
-
import { logger } from '../core/logger.ts';
|
|
12
|
-
import { redact } from '../core/redact.ts';
|
|
13
|
-
import { broadcastSSE } from '../server/index.ts';
|
|
14
|
-
import { sessionErrorBlocks, busyBlocks, noAgentBlocks, completionFooterBlocks } from './blocks.ts';
|
|
15
|
-
|
|
16
|
-
interface SessionLock {
|
|
17
|
-
processing: boolean;
|
|
18
|
-
queue: CoalesceDispatch[];
|
|
19
|
-
killCurrent: (() => void) | null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface Handler {
|
|
23
|
-
handle(dispatch: CoalesceDispatch): Promise<void>;
|
|
24
|
-
activeCount(): number;
|
|
25
|
-
killAll(): void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createHandler(
|
|
29
|
-
client: ChannelClient,
|
|
30
|
-
config: Config,
|
|
31
|
-
signal: AbortSignal,
|
|
32
|
-
getTeamId: () => string,
|
|
33
|
-
): Handler {
|
|
34
|
-
const locks = new Map<string, SessionLock>();
|
|
35
|
-
let activeSpawns = 0;
|
|
36
|
-
|
|
37
|
-
function getLock(key: string): SessionLock {
|
|
38
|
-
let lock = locks.get(key);
|
|
39
|
-
if (!lock) {
|
|
40
|
-
lock = { processing: false, queue: [], killCurrent: null };
|
|
41
|
-
locks.set(key, lock);
|
|
42
|
-
}
|
|
43
|
-
return lock;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function formatPrompt(messages: CoalesceDispatch['messages']): string {
|
|
47
|
-
if (messages.length === 1) {
|
|
48
|
-
return messages[0]!.text;
|
|
49
|
-
}
|
|
50
|
-
return messages
|
|
51
|
-
.map((m) => {
|
|
52
|
-
const time = new Date(Number(m.timestamp) * 1000).toISOString().slice(11, 19);
|
|
53
|
-
return `[${time}] @${m.userId}: ${m.text}`;
|
|
54
|
-
})
|
|
55
|
-
.join('\n');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function streamToChannel(
|
|
59
|
-
events: AsyncIterable<StreamEvent>,
|
|
60
|
-
channelId: string,
|
|
61
|
-
threadTs: string,
|
|
62
|
-
sessionUid: string,
|
|
63
|
-
teamId: string,
|
|
64
|
-
userId: string,
|
|
65
|
-
meta: { birdName?: string },
|
|
66
|
-
): Promise<void> {
|
|
67
|
-
const streamer = client.startStream({ channelId, threadTs, teamId, userId });
|
|
68
|
-
let fullText = '';
|
|
69
|
-
let completion: StreamEventCompletion | undefined;
|
|
70
|
-
let hasError = false;
|
|
71
|
-
|
|
72
|
-
for await (const event of events) {
|
|
73
|
-
if (signal.aborted) break;
|
|
74
|
-
logger.debug(`stream event: ${event.type}`);
|
|
75
|
-
|
|
76
|
-
switch (event.type) {
|
|
77
|
-
case 'init':
|
|
78
|
-
db.updateSessionProviderId(sessionUid, event.sessionId);
|
|
79
|
-
break;
|
|
80
|
-
|
|
81
|
-
case 'text_delta': {
|
|
82
|
-
const safe = redact(event.delta);
|
|
83
|
-
fullText += safe;
|
|
84
|
-
await streamer.append({ markdown_text: safe });
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
case 'tool_images':
|
|
89
|
-
await uploadImages(event.images, channelId, threadTs);
|
|
90
|
-
break;
|
|
91
|
-
|
|
92
|
-
case 'completion':
|
|
93
|
-
completion = event;
|
|
94
|
-
logger.info(
|
|
95
|
-
`completion [${event.subtype}]: ${event.tokensIn}in/${event.tokensOut}out, $${event.costUsd.toFixed(4)}, ${event.numTurns} turns`,
|
|
96
|
-
);
|
|
97
|
-
db.logMessage(
|
|
98
|
-
channelId,
|
|
99
|
-
threadTs,
|
|
100
|
-
'bot',
|
|
101
|
-
'out',
|
|
102
|
-
fullText,
|
|
103
|
-
event.tokensIn,
|
|
104
|
-
event.tokensOut,
|
|
105
|
-
);
|
|
106
|
-
break;
|
|
107
|
-
|
|
108
|
-
case 'rate_limit':
|
|
109
|
-
logger.debug(`rate limit window resets ${new Date(event.resetsAt * 1000).toISOString()}`);
|
|
110
|
-
break;
|
|
111
|
-
|
|
112
|
-
case 'error': {
|
|
113
|
-
hasError = true;
|
|
114
|
-
const safeError = redact(event.error);
|
|
115
|
-
logger.error(`agent error: ${safeError}`);
|
|
116
|
-
db.insertLog('error', 'spawn', safeError, channelId);
|
|
117
|
-
await streamer.append({ markdown_text: `\n\nError: ${safeError}` });
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const footerBlocks = completion
|
|
124
|
-
? completionFooterBlocks(completion, hasError, meta.birdName, userId)
|
|
125
|
-
: undefined;
|
|
126
|
-
|
|
127
|
-
await streamer.stop(footerBlocks ? { blocks: footerBlocks } : {});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function uploadImages(
|
|
131
|
-
images: ToolImage[],
|
|
132
|
-
channelId: string,
|
|
133
|
-
threadTs: string,
|
|
134
|
-
): Promise<void> {
|
|
135
|
-
for (let i = 0; i < images.length; i++) {
|
|
136
|
-
const img = images[i]!;
|
|
137
|
-
const content = Buffer.from(img.data, 'base64');
|
|
138
|
-
const ext = img.mediaType === 'image/jpeg' ? 'jpg' : 'png';
|
|
139
|
-
const filename = `screenshot-${i + 1}.${ext}`;
|
|
140
|
-
try {
|
|
141
|
-
await client.uploadFile(channelId, threadTs, content, filename, `Screenshot ${i + 1}`);
|
|
142
|
-
} catch (err) {
|
|
143
|
-
logger.warn(`image upload failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function handle(dispatch: CoalesceDispatch): Promise<void> {
|
|
149
|
-
const { channelId, threadTs, messages } = dispatch;
|
|
150
|
-
const key = `${channelId}:${threadTs}`;
|
|
151
|
-
const lock = getLock(key);
|
|
152
|
-
|
|
153
|
-
if (lock.processing) {
|
|
154
|
-
lock.queue.push(dispatch);
|
|
155
|
-
try {
|
|
156
|
-
await client.postEphemeral(
|
|
157
|
-
channelId,
|
|
158
|
-
threadTs,
|
|
159
|
-
messages[messages.length - 1]!.userId,
|
|
160
|
-
"Got it, I'll get to this after my current response.",
|
|
161
|
-
);
|
|
162
|
-
} catch {
|
|
163
|
-
/* postEphemeral may fail if user left channel */
|
|
164
|
-
}
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (activeSpawns >= config.sessions.maxConcurrent) {
|
|
169
|
-
const blocks = busyBlocks(activeSpawns, config.sessions.maxConcurrent);
|
|
170
|
-
await client.postMessage(
|
|
171
|
-
channelId,
|
|
172
|
-
threadTs,
|
|
173
|
-
'Too many active sessions. Try again shortly.',
|
|
174
|
-
{ blocks },
|
|
175
|
-
);
|
|
176
|
-
logger.warn('max concurrent sessions reached');
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
lock.processing = true;
|
|
181
|
-
activeSpawns++;
|
|
182
|
-
|
|
183
|
-
let sessionUid: string | undefined;
|
|
184
|
-
try {
|
|
185
|
-
const resolved = resolveSession(channelId, threadTs, config);
|
|
186
|
-
if (!resolved) {
|
|
187
|
-
const blocks = noAgentBlocks(channelId);
|
|
188
|
-
await client.postMessage(channelId, threadTs, 'No agent configured for this channel.', {
|
|
189
|
-
blocks,
|
|
190
|
-
});
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const { session, agent, isNew } = resolved;
|
|
195
|
-
sessionUid = session.uid;
|
|
196
|
-
|
|
197
|
-
for (const msg of messages) {
|
|
198
|
-
db.logMessage(channelId, threadTs, msg.userId, 'in', msg.text);
|
|
199
|
-
}
|
|
200
|
-
db.touchSession(session.uid, messages.length + 1);
|
|
201
|
-
broadcastSSE('invalidate', { resource: 'sessions' });
|
|
202
|
-
|
|
203
|
-
const prompt = formatPrompt(messages);
|
|
204
|
-
const lastMessage = messages[messages.length - 1]!;
|
|
205
|
-
const userId = lastMessage.userId;
|
|
206
|
-
|
|
207
|
-
const existingSessionId = isNew ? undefined : session.provider_session_id || undefined;
|
|
208
|
-
const { events, kill } = spawnProvider(
|
|
209
|
-
agent.provider,
|
|
210
|
-
{
|
|
211
|
-
message: prompt,
|
|
212
|
-
sessionId: existingSessionId,
|
|
213
|
-
agent,
|
|
214
|
-
mcpConfigPath: config.browser.mcpConfigPath,
|
|
215
|
-
},
|
|
216
|
-
signal,
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
lock.killCurrent = kill;
|
|
220
|
-
|
|
221
|
-
client.setStatus?.(channelId, threadTs, 'is thinking...').catch(() => {});
|
|
222
|
-
|
|
223
|
-
if (isNew) {
|
|
224
|
-
const title = prompt.length > 60 ? prompt.slice(0, 57) + '...' : prompt;
|
|
225
|
-
client.setTitle?.(channelId, threadTs, title).catch(() => {});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
await streamToChannel(events, channelId, threadTs, session.uid, getTeamId(), userId, {
|
|
229
|
-
birdName: agent.name,
|
|
230
|
-
});
|
|
231
|
-
} catch (err) {
|
|
232
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
233
|
-
logger.error(`handler error: ${errMsg}`);
|
|
234
|
-
db.insertLog('error', 'handler', errMsg, channelId);
|
|
235
|
-
try {
|
|
236
|
-
const blocks = sessionErrorBlocks(errMsg, { sessionUid });
|
|
237
|
-
await client.postMessage(channelId, threadTs, `Something went wrong: ${errMsg}`, {
|
|
238
|
-
blocks,
|
|
239
|
-
});
|
|
240
|
-
} catch {
|
|
241
|
-
/* channel may no longer be accessible */
|
|
242
|
-
}
|
|
243
|
-
} finally {
|
|
244
|
-
activeSpawns--;
|
|
245
|
-
lock.processing = false;
|
|
246
|
-
lock.killCurrent = null;
|
|
247
|
-
|
|
248
|
-
const next = lock.queue.shift();
|
|
249
|
-
if (next) {
|
|
250
|
-
handle(next).catch((err: unknown) => {
|
|
251
|
-
logger.error(`dispatch error: ${err instanceof Error ? err.message : String(err)}`);
|
|
252
|
-
});
|
|
253
|
-
} else if (lock.queue.length === 0) {
|
|
254
|
-
locks.delete(key);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function activeCount(): number {
|
|
260
|
-
return activeSpawns;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function killAll(): void {
|
|
264
|
-
for (const lock of locks.values()) {
|
|
265
|
-
lock.killCurrent?.();
|
|
266
|
-
lock.queue.length = 0;
|
|
267
|
-
}
|
|
268
|
-
locks.clear();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return { handle, activeCount, killAll };
|
|
272
|
-
}
|