@controlflow-ai/daemon 0.1.3 → 0.1.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/bin/daemon.js +6 -1
- package/package.json +1 -1
- package/src/agent-runtime.ts +16 -2
- package/src/app.ts +47 -3
- package/src/cli.ts +20 -4
- package/src/client.ts +15 -0
- package/src/daemon-client.ts +5 -1
- package/src/daemon.ts +105 -21
- package/src/db.ts +38 -5
- package/src/local-api.ts +12 -1
- package/src/migrations/017_unified_room_delivery.ts +1 -1
- package/src/migrations/036_project_agent_defaults.ts +1 -1
- package/src/migrations/046_agent_run_interrupt.ts +78 -0
- package/src/migrations/047_available_receive_mode.ts +57 -0
- package/src/migrations.ts +12 -2
- package/src/types.ts +3 -3
package/bin/daemon.js
CHANGED
package/package.json
CHANGED
package/src/agent-runtime.ts
CHANGED
|
@@ -157,6 +157,19 @@ function formatMessageAttachmentSummary(message: Message): string {
|
|
|
157
157
|
return ` [attachments: ${items.join('; ')}]`;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
function formatMergedDeliveredContent(context: DeliveryContext | undefined, currentMessage: Message): string {
|
|
161
|
+
if (!context || context.messages.length === 0) return sanitizeProviderIds(currentMessage.content);
|
|
162
|
+
const lines = context.messages.map((message) => {
|
|
163
|
+
const marker = message.id === currentMessage.id ? ' current' : '';
|
|
164
|
+
const parent = message.parent_id === null ? '' : ` parent=${message.parent_id}`;
|
|
165
|
+
const recipient = message.recipient ? ` to=@${sanitizeProviderIds(message.recipient)}` : '';
|
|
166
|
+
const sender = sanitizeProviderIds(message.sender);
|
|
167
|
+
const content = sanitizeProviderIds(message.content).trim();
|
|
168
|
+
return `[${message.id}${marker}] @${sender}${recipient}${parent}:\n${content}`;
|
|
169
|
+
});
|
|
170
|
+
return `Merged delivered message content:\n${lines.join('\n\n')}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
160
173
|
function formatSavedRoomContext(savedMessages: RoomSavedMessage[] | undefined): string {
|
|
161
174
|
if (!savedMessages || savedMessages.length === 0) return '';
|
|
162
175
|
const lines = savedMessages.slice(0, 10).map((saved) => {
|
|
@@ -228,7 +241,8 @@ export function buildPalPrompt(input: AgentRuntimeRunInput): string {
|
|
|
228
241
|
const chatName = sanitizeProviderIds(input.message.chat_name);
|
|
229
242
|
const chatRef = input.message.chat_id;
|
|
230
243
|
const sender = sanitizeProviderIds(input.message.sender);
|
|
231
|
-
const
|
|
244
|
+
const currentContent = sanitizeProviderIds(input.message.content);
|
|
245
|
+
const content = formatMergedDeliveredContent(input.deliveryContext, input.message);
|
|
232
246
|
const freshnessFlags = `--base-message-id ${input.message.id} --hold-if-stale`;
|
|
233
247
|
const projectContext = input.projectContext
|
|
234
248
|
? input.projectContext.accessible
|
|
@@ -270,7 +284,7 @@ ${formatRoomModeAndSkillContext({ roomMode: input.roomMode, enabledSkills: input
|
|
|
270
284
|
${messageContext}
|
|
271
285
|
${savedRoomContext}
|
|
272
286
|
${runtimeAttachments}
|
|
273
|
-
${formatAllMentionOnlyHandoff(
|
|
287
|
+
${formatAllMentionOnlyHandoff(currentContent, messageContextLabel)}
|
|
274
288
|
Content:
|
|
275
289
|
${content}
|
|
276
290
|
|
package/src/app.ts
CHANGED
|
@@ -133,7 +133,7 @@ interface AgentRoomSubscriptionBody {
|
|
|
133
133
|
mode?: AgentRoomSubscriptionMode;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const agentRoomSubscriptionModes: AgentRoomSubscriptionMode[] = ['all', 'periodic', 'mentions', 'muted', 'off'];
|
|
136
|
+
const agentRoomSubscriptionModes: AgentRoomSubscriptionMode[] = ['all', 'available', 'periodic', 'mentions', 'muted', 'off'];
|
|
137
137
|
const agentActivityKinds: AgentActivityKind[] = ['lifecycle', 'working', 'thinking', 'output', 'tool', 'error'];
|
|
138
138
|
const roomTaskStatuses: RoomTaskStatus[] = ['todo', 'in_progress', 'in_review', 'done', 'closed'];
|
|
139
139
|
const roomReminderStatuses: RoomReminderStatus[] = ['scheduled', 'fired', 'canceled'];
|
|
@@ -144,7 +144,7 @@ const workflowRunStatuses: WorkflowRunStatus[] = ['running', 'completed', 'faile
|
|
|
144
144
|
|
|
145
145
|
function parseAgentRoomSubscriptionMode(value: unknown): AgentRoomSubscriptionMode {
|
|
146
146
|
if (typeof value !== 'string' || !agentRoomSubscriptionModes.includes(value as AgentRoomSubscriptionMode)) {
|
|
147
|
-
throw new HttpError(400, 'INVALID_SUBSCRIPTION_MODE', 'mode must be all, periodic, mentions, muted, or off');
|
|
147
|
+
throw new HttpError(400, 'INVALID_SUBSCRIPTION_MODE', 'mode must be all, available, periodic, mentions, muted, or off');
|
|
148
148
|
}
|
|
149
149
|
return value as AgentRoomSubscriptionMode;
|
|
150
150
|
}
|
|
@@ -2162,6 +2162,33 @@ export function route(store: MessageStore, request: Request, options: AppRouteOp
|
|
|
2162
2162
|
}
|
|
2163
2163
|
}
|
|
2164
2164
|
|
|
2165
|
+
const roomAgentInterruptMatch = pathname.match(/^\/api\/rooms\/([^/]+)\/agents\/([^/]+)\/interrupt$/);
|
|
2166
|
+
if (request.method === 'POST' && roomAgentInterruptMatch) {
|
|
2167
|
+
const room = store.resolveRoom(decodeURIComponent(roomAgentInterruptMatch[1]!));
|
|
2168
|
+
if (!room) throw new HttpError(404, 'ROOM_NOT_FOUND', 'room not found');
|
|
2169
|
+
const agent = decodeURIComponent(roomAgentInterruptMatch[2]!);
|
|
2170
|
+
if (!store.getAgent(agent)) throw new HttpError(404, 'AGENT_NOT_FOUND', 'agent not found');
|
|
2171
|
+
try {
|
|
2172
|
+
const run = store.requestRoomAgentRunAction({ roomId: room.id, agent, action: 'interrupt' });
|
|
2173
|
+
const pendingBacklogCount = store.countPendingDeliveriesAfterMessage({
|
|
2174
|
+
roomId: room.id,
|
|
2175
|
+
agent,
|
|
2176
|
+
afterMessageId: run.trigger_message_id ?? run.message_id,
|
|
2177
|
+
});
|
|
2178
|
+
return json({
|
|
2179
|
+
run,
|
|
2180
|
+
pending_backlog_count: pendingBacklogCount,
|
|
2181
|
+
message: `Stop requested for @${agent} current processing. Pending messages were not cleared. Already completed file changes, commands, and external side effects are not rolled back.`,
|
|
2182
|
+
});
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2185
|
+
if (message.includes('running run for agent') && message.includes('was not found')) {
|
|
2186
|
+
throw new HttpError(404, 'RUN_NOT_FOUND', 'no running runtime for this agent in this room');
|
|
2187
|
+
}
|
|
2188
|
+
throw error;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2165
2192
|
const roomAgentSubscriptionMatch = pathname.match(/^\/api\/rooms\/([^/]+)\/agents\/([^/]+)\/subscription$/);
|
|
2166
2193
|
if (request.method === 'PATCH' && roomAgentSubscriptionMatch) {
|
|
2167
2194
|
return readJson<AgentRoomSubscriptionBody>(request).then((body) => {
|
|
@@ -2653,6 +2680,23 @@ export function route(store: MessageStore, request: Request, options: AppRouteOp
|
|
|
2653
2680
|
});
|
|
2654
2681
|
}
|
|
2655
2682
|
|
|
2683
|
+
const deliveryCancelMatch = pathname.match(/^\/api\/deliveries\/([^/]+)\/cancel$/);
|
|
2684
|
+
if (request.method === 'POST' && deliveryCancelMatch) {
|
|
2685
|
+
return readJson<FinishDeliveryBody>(request).then((body) => {
|
|
2686
|
+
const connection = requireDaemonConnection(store, request);
|
|
2687
|
+
const ownerId = connection?.connectionId ?? body.connection_id ?? body.daemon_id;
|
|
2688
|
+
if (!ownerId?.trim()) throw new HttpError(400, 'MISSING_DAEMON_ID', 'daemon_id or connection auth is required');
|
|
2689
|
+
if (!body.claim_token?.trim()) throw new HttpError(400, 'MISSING_CLAIM_TOKEN', 'claim_token is required');
|
|
2690
|
+
const delivery = store.cancelClaimedDelivery(deliveryCancelMatch[1]!, {
|
|
2691
|
+
daemonId: ownerId,
|
|
2692
|
+
connectionId: connection?.connectionId ?? body.connection_id,
|
|
2693
|
+
claimToken: body.claim_token,
|
|
2694
|
+
runId: body.run_id,
|
|
2695
|
+
});
|
|
2696
|
+
return json({ delivery });
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2656
2700
|
if (request.method === 'POST' && pathname === '/api/messages') {
|
|
2657
2701
|
return readJson<SendBody>(request).then(async (body) => {
|
|
2658
2702
|
const connection = assertDaemonConnection(store, request);
|
|
@@ -2929,7 +2973,7 @@ export function route(store: MessageStore, request: Request, options: AppRouteOp
|
|
|
2929
2973
|
});
|
|
2930
2974
|
}
|
|
2931
2975
|
|
|
2932
|
-
const actionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/(kill|restart)$/);
|
|
2976
|
+
const actionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/(kill|restart|interrupt)$/);
|
|
2933
2977
|
if (request.method === 'POST' && actionMatch) {
|
|
2934
2978
|
const run = store.requestRunAction(actionMatch[1]!, actionMatch[2] as RunAction);
|
|
2935
2979
|
return json({ run });
|
package/src/cli.ts
CHANGED
|
@@ -23,6 +23,7 @@ Usage:
|
|
|
23
23
|
bun run src/cli.ts rooms handoff --from-room <room-id> --from <agent> --purpose <goal> [--source-message <id>] [--name <room>] [--team agent:mode ...] [--stdin]
|
|
24
24
|
bun run src/cli.ts rooms invite --room general --agent codex [--mode mentions|all|periodic|muted|off]
|
|
25
25
|
bun run src/cli.ts rooms restart-agent --room general --agent codex [--json]
|
|
26
|
+
bun run src/cli.ts rooms interrupt-agent --room general --agent codex [--json]
|
|
26
27
|
bun run src/cli.ts debate start --chat general --a palbeta --b clawed [--turns 6] <topic>
|
|
27
28
|
bun run src/cli.ts rooms list [--json]
|
|
28
29
|
bun run src/cli.ts rooms members --room <room-id-or-name> [--json]
|
|
@@ -139,15 +140,15 @@ function roomNameFromPurpose(purpose: string): string {
|
|
|
139
140
|
return `handoff-${slug || Date.now().toString(36)}`;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
function parseTeamFlags(values: string[]): Array<{ agent: string; mode?: 'all' | 'periodic' | 'mentions' | 'muted' | 'off' }> | undefined {
|
|
143
|
+
function parseTeamFlags(values: string[]): Array<{ agent: string; mode?: 'all' | 'available' | 'periodic' | 'mentions' | 'muted' | 'off' }> | undefined {
|
|
143
144
|
if (!values.length) return undefined;
|
|
144
145
|
return values.map((value) => {
|
|
145
146
|
const [agent, mode] = value.split(':');
|
|
146
147
|
if (!agent?.trim()) throw new Error('--team requires agent or agent:mode');
|
|
147
|
-
if (mode && !['all', 'periodic', 'mentions', 'muted', 'off'].includes(mode)) {
|
|
148
|
-
throw new Error('--team mode must be all, periodic, mentions, muted, or off');
|
|
148
|
+
if (mode && !['all', 'available', 'periodic', 'mentions', 'muted', 'off'].includes(mode)) {
|
|
149
|
+
throw new Error('--team mode must be all, available, periodic, mentions, muted, or off');
|
|
149
150
|
}
|
|
150
|
-
return { agent: agent.trim(), mode: mode as 'all' | 'periodic' | 'mentions' | 'muted' | 'off' | undefined };
|
|
151
|
+
return { agent: agent.trim(), mode: mode as 'all' | 'available' | 'periodic' | 'mentions' | 'muted' | 'off' | undefined };
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
|
|
@@ -614,6 +615,21 @@ async function main(): Promise<void> {
|
|
|
614
615
|
}
|
|
615
616
|
return;
|
|
616
617
|
}
|
|
618
|
+
if (sub === 'interrupt-agent' || sub === 'stop-agent') {
|
|
619
|
+
const roomRef = flag(args.flags, 'room') ?? flag(args.flags, 'chat-id') ?? args.values[1];
|
|
620
|
+
const agent = flag(args.flags, 'agent') ?? args.values[2];
|
|
621
|
+
if (!roomRef) throw new Error(`rooms ${sub} requires --room <room-id-or-name>`);
|
|
622
|
+
if (!agent) throw new Error(`rooms ${sub} requires --agent <agent-key>`);
|
|
623
|
+
const result = await daemonClient.interruptRoomAgent(roomRef, agent);
|
|
624
|
+
if (args.flags.json) {
|
|
625
|
+
printJson(result);
|
|
626
|
+
} else {
|
|
627
|
+
console.log(`interrupt requested for @${sanitizeProviderIds(agent)} in ${sanitizeProviderIds(roomRef)}: run=${result.run.id} status=${result.run.status} pending_backlog=${result.pending_backlog_count}`);
|
|
628
|
+
console.log('Pending messages were not cleared; they remain available for the next wakeup.');
|
|
629
|
+
console.log('Already completed file changes, commands, and external side effects are not rolled back.');
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
617
633
|
throw new Error(`unknown rooms subcommand: ${sub}`);
|
|
618
634
|
}
|
|
619
635
|
|
package/src/client.ts
CHANGED
|
@@ -134,6 +134,12 @@ export interface HeldDraftResolutionResult {
|
|
|
134
134
|
notify?: unknown;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
export interface InterruptRoomAgentResult {
|
|
138
|
+
run: AgentRun;
|
|
139
|
+
pending_backlog_count: number;
|
|
140
|
+
message: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
export interface ListHeldDraftsRequest {
|
|
138
144
|
agent?: string;
|
|
139
145
|
room_id?: string | null;
|
|
@@ -388,6 +394,10 @@ export class LockClient {
|
|
|
388
394
|
return data.run;
|
|
389
395
|
}
|
|
390
396
|
|
|
397
|
+
async interruptRoomAgent(room: string, agent: string): Promise<InterruptRoomAgentResult> {
|
|
398
|
+
return this.post<InterruptRoomAgentResult>(`/api/rooms/${encodeURIComponent(room)}/agents/${encodeURIComponent(agent)}/interrupt`, {});
|
|
399
|
+
}
|
|
400
|
+
|
|
391
401
|
async scheduleReminder(input: CreateRoomReminderRequest): Promise<RoomReminder> {
|
|
392
402
|
const data = await this.post<{ reminder: RoomReminder }>('/api/reminders', input);
|
|
393
403
|
return data.reminder;
|
|
@@ -667,6 +677,11 @@ export class LockClient {
|
|
|
667
677
|
return data.delivery;
|
|
668
678
|
}
|
|
669
679
|
|
|
680
|
+
async cancelClaimedDelivery(id: string, input: FinishDeliveryRequest): Promise<MessageDelivery> {
|
|
681
|
+
const data = await this.post<{ delivery: MessageDelivery }>(`/api/deliveries/${id}/cancel`, input);
|
|
682
|
+
return data.delivery;
|
|
683
|
+
}
|
|
684
|
+
|
|
670
685
|
async getOrCreateSession(input: GetOrCreateSessionRequest): Promise<AgentSession> {
|
|
671
686
|
const data = await this.post<{ session: AgentSession }>('/api/sessions', input);
|
|
672
687
|
return data.session;
|
package/src/daemon-client.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defaultDaemonToken, defaultDaemonUrl } from './config.js';
|
|
2
2
|
import { localHeaders } from './local-auth.js';
|
|
3
3
|
import type { AgentRoomSubscriptionMode, AgentRun, ApiResponse, Chat, HeldMessageDraft, Message, RoomChannel, RoomParticipant, RoomReminder, RoomSavedMessage, RoomTask, RoomTaskStatus, RunAction } from './types.js';
|
|
4
|
-
import type { ClaimRoomTaskRequest, CreateArtifactRequest, CreateHandoffRoomRequest, CreateRoomReminderRequest, CreateRoomSavedMessageRequest, CreateRoomTasksRequest, HeldDraftResolutionResult, ListHeldDraftsRequest, ListRoomRemindersRequest, ListRoomSavedMessagesRequest, SendMessageResult, SendRequest } from './client.js';
|
|
4
|
+
import type { ClaimRoomTaskRequest, CreateArtifactRequest, CreateHandoffRoomRequest, CreateRoomReminderRequest, CreateRoomSavedMessageRequest, CreateRoomTasksRequest, HeldDraftResolutionResult, InterruptRoomAgentResult, ListHeldDraftsRequest, ListRoomRemindersRequest, ListRoomSavedMessagesRequest, SendMessageResult, SendRequest } from './client.js';
|
|
5
5
|
|
|
6
6
|
export class DaemonClient {
|
|
7
7
|
readonly baseUrl: string;
|
|
@@ -115,6 +115,10 @@ export class DaemonClient {
|
|
|
115
115
|
return data.run;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
async interruptRoomAgent(room: string, agent: string): Promise<InterruptRoomAgentResult> {
|
|
119
|
+
return this.post<InterruptRoomAgentResult>(`/local/rooms/${encodeURIComponent(room)}/agents/${encodeURIComponent(agent)}/interrupt`, {});
|
|
120
|
+
}
|
|
121
|
+
|
|
118
122
|
async scheduleReminder(input: CreateRoomReminderRequest): Promise<RoomReminder> {
|
|
119
123
|
const data = await this.post<{ reminder: RoomReminder }>('/local/reminders', input);
|
|
120
124
|
return data.reminder;
|
package/src/daemon.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { formatMessage } from './format.js';
|
|
|
8
8
|
import { startLocalApi } from './local-api.js';
|
|
9
9
|
import type { LocalApiAgentPrincipal } from './local-auth.js';
|
|
10
10
|
import { loadOrCreateDaemonToken } from './token-file.js';
|
|
11
|
-
import type { ComputerAgentAssignment, Message, MessageDelivery } from './types.js';
|
|
11
|
+
import type { AgentRun, ComputerAgentAssignment, Message, MessageDelivery, RunAction } from './types.js';
|
|
12
12
|
import type { AgentRuntime } from './agent-runtime.js';
|
|
13
13
|
import { knownRuntimeNames, resolveRuntimeDriver } from './runtime-registry.js';
|
|
14
14
|
import { buildRuntimeLaunchContext, type AgentPermissionProfile } from './agent-permissions.js';
|
|
@@ -457,7 +457,7 @@ async function loadPermissionProfile(client: LockClient, agent: string): Promise
|
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
async function ackSteeredDeliveries(
|
|
460
|
-
client: LockClient,
|
|
460
|
+
client: Pick<LockClient, 'ackDelivery'>,
|
|
461
461
|
active: ActiveRunHandle,
|
|
462
462
|
options: { daemonId: string; connectionId?: string | null },
|
|
463
463
|
runId: string,
|
|
@@ -475,7 +475,7 @@ async function ackSteeredDeliveries(
|
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
async function failSteeredDeliveries(
|
|
478
|
-
client: LockClient,
|
|
478
|
+
client: Pick<LockClient, 'failDelivery'>,
|
|
479
479
|
active: ActiveRunHandle,
|
|
480
480
|
options: { daemonId: string; connectionId?: string | null },
|
|
481
481
|
runId: string,
|
|
@@ -494,11 +494,57 @@ async function failSteeredDeliveries(
|
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
export async function finishStoppedRunAction(
|
|
498
|
+
client: Pick<LockClient, 'finishRun' | 'ackDelivery' | 'cancelClaimedDelivery' | 'failDelivery'>,
|
|
499
|
+
input: {
|
|
500
|
+
action: 'kill' | 'restart' | 'interrupt';
|
|
501
|
+
runId: string;
|
|
502
|
+
exitCode: number | null;
|
|
503
|
+
output: string;
|
|
504
|
+
delivery: MessageDelivery;
|
|
505
|
+
active: ActiveRunHandle;
|
|
506
|
+
daemonId: string;
|
|
507
|
+
connectionId?: string | null;
|
|
508
|
+
},
|
|
509
|
+
): Promise<'done' | 'restart'> {
|
|
510
|
+
if (!input.delivery.claim_token) throw new Error(`delivery ${input.delivery.id} is not claimed`);
|
|
511
|
+
if (input.action === 'restart') {
|
|
512
|
+
await client.finishRun(input.runId, { status: 'restarted', exit_code: input.exitCode, output: input.output });
|
|
513
|
+
await failSteeredDeliveries(client, input.active, { daemonId: input.daemonId, connectionId: input.connectionId }, input.runId, 'active run restarted before completion');
|
|
514
|
+
return 'restart';
|
|
515
|
+
}
|
|
516
|
+
if (input.action === 'kill') {
|
|
517
|
+
await client.finishRun(input.runId, { status: 'killed', exit_code: input.exitCode, output: input.output });
|
|
518
|
+
await client.ackDelivery(input.delivery.id, { daemon_id: input.daemonId, connection_id: input.connectionId, claim_token: input.delivery.claim_token, run_id: input.runId });
|
|
519
|
+
await ackSteeredDeliveries(client, input.active, { daemonId: input.daemonId, connectionId: input.connectionId }, input.runId);
|
|
520
|
+
return 'done';
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
await client.finishRun(input.runId, { status: 'cancelled', exit_code: input.exitCode, output: input.output });
|
|
524
|
+
await client.cancelClaimedDelivery(input.delivery.id, { daemon_id: input.daemonId, connection_id: input.connectionId, claim_token: input.delivery.claim_token, run_id: input.runId });
|
|
525
|
+
await failSteeredDeliveries(client, input.active, { daemonId: input.daemonId, connectionId: input.connectionId }, input.runId, 'active run interrupted before completion');
|
|
526
|
+
return 'done';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export async function resolveStoppedRunActionAfterRuntime(
|
|
530
|
+
client: Pick<LockClient, 'getRun'>,
|
|
531
|
+
input: { runId: string; observedAction: RunAction | null },
|
|
532
|
+
): Promise<RunAction | null> {
|
|
533
|
+
if (input.observedAction) return input.observedAction;
|
|
534
|
+
const latest = await client.getRun(input.runId).catch((error) => {
|
|
535
|
+
console.warn(`[daemon] failed to re-check run action run=${input.runId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
536
|
+
return null as AgentRun | null;
|
|
537
|
+
});
|
|
538
|
+
if (latest?.status === 'running' && latest.action) return latest.action;
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
497
542
|
async function ackCoveredPendingDeliveries(
|
|
498
543
|
client: LockClient,
|
|
499
544
|
input: {
|
|
500
545
|
agent: string;
|
|
501
546
|
chatId: string;
|
|
547
|
+
startMessageId: number;
|
|
502
548
|
triggerMessageId: number;
|
|
503
549
|
maxCoveredMessageId: number;
|
|
504
550
|
daemonId: string;
|
|
@@ -507,12 +553,13 @@ async function ackCoveredPendingDeliveries(
|
|
|
507
553
|
runId: string;
|
|
508
554
|
},
|
|
509
555
|
): Promise<void> {
|
|
510
|
-
if (input.maxCoveredMessageId <= input.
|
|
556
|
+
if (input.maxCoveredMessageId <= input.startMessageId) return;
|
|
511
557
|
const pending = await client.listDeliveries(input.agent, 'pending', PENDING_DELIVERY_CANDIDATE_LIMIT);
|
|
512
558
|
const covered = pending.filter((delivery) => (
|
|
513
559
|
delivery.chat_id === input.chatId
|
|
514
|
-
&& delivery.message_id > input.
|
|
560
|
+
&& delivery.message_id > input.startMessageId
|
|
515
561
|
&& delivery.message_id <= input.maxCoveredMessageId
|
|
562
|
+
&& delivery.message_id !== input.triggerMessageId
|
|
516
563
|
));
|
|
517
564
|
for (const delivery of covered) {
|
|
518
565
|
try {
|
|
@@ -674,6 +721,7 @@ async function executeRun(client: LockClient, delivery: MessageDelivery, message
|
|
|
674
721
|
extraArgs: options.extraArgs,
|
|
675
722
|
});
|
|
676
723
|
const maxCoveredMessageId = deliveryContext?.messages.reduce((max, item) => Math.max(max, item.id), message.id) ?? message.id;
|
|
724
|
+
const startMessageId = deliveryContext?.startMessageId ?? message.id;
|
|
677
725
|
|
|
678
726
|
try {
|
|
679
727
|
const runtimeAttachments = runtimeAttachmentsForMessage({ message });
|
|
@@ -736,6 +784,56 @@ async function executeRun(client: LockClient, delivery: MessageDelivery, message
|
|
|
736
784
|
return 'done';
|
|
737
785
|
}
|
|
738
786
|
|
|
787
|
+
const stoppedByAction = await resolveStoppedRunActionAfterRuntime(client, {
|
|
788
|
+
runId: run.id,
|
|
789
|
+
observedAction: result.stoppedByAction,
|
|
790
|
+
});
|
|
791
|
+
if (stoppedByAction && stoppedByAction !== result.stoppedByAction) {
|
|
792
|
+
console.log(`${logPrefix} run=${run.id} persisted action=${stoppedByAction} observed after runtime exit`);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (stoppedByAction === 'restart') {
|
|
796
|
+
console.log(`${logPrefix} run=${run.id} finishing with restart`);
|
|
797
|
+
return finishStoppedRunAction(client, {
|
|
798
|
+
action: 'restart',
|
|
799
|
+
runId: run.id,
|
|
800
|
+
exitCode: result.exitCode,
|
|
801
|
+
output: result.output,
|
|
802
|
+
delivery,
|
|
803
|
+
active: activeHandle,
|
|
804
|
+
daemonId: options.daemonId,
|
|
805
|
+
connectionId: options.connectionId,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (stoppedByAction === 'kill') {
|
|
810
|
+
console.log(`${logPrefix} run=${run.id} finishing with killed`);
|
|
811
|
+
return finishStoppedRunAction(client, {
|
|
812
|
+
action: 'kill',
|
|
813
|
+
runId: run.id,
|
|
814
|
+
exitCode: result.exitCode,
|
|
815
|
+
output: result.output,
|
|
816
|
+
delivery,
|
|
817
|
+
active: activeHandle,
|
|
818
|
+
daemonId: options.daemonId,
|
|
819
|
+
connectionId: options.connectionId,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (stoppedByAction === 'interrupt') {
|
|
824
|
+
console.log(`${logPrefix} run=${run.id} finishing with cancelled`);
|
|
825
|
+
return finishStoppedRunAction(client, {
|
|
826
|
+
action: 'interrupt',
|
|
827
|
+
runId: run.id,
|
|
828
|
+
exitCode: result.exitCode,
|
|
829
|
+
output: result.output,
|
|
830
|
+
delivery,
|
|
831
|
+
active: activeHandle,
|
|
832
|
+
daemonId: options.daemonId,
|
|
833
|
+
connectionId: options.connectionId,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
739
837
|
if (options.dryRun && result.output.trim()) {
|
|
740
838
|
console.log(`${logPrefix} dry-run sending output back to chat`);
|
|
741
839
|
await client.sendMessage({
|
|
@@ -747,21 +845,6 @@ async function executeRun(client: LockClient, delivery: MessageDelivery, message
|
|
|
747
845
|
});
|
|
748
846
|
}
|
|
749
847
|
|
|
750
|
-
if (result.stoppedByAction === 'restart') {
|
|
751
|
-
console.log(`${logPrefix} run=${run.id} finishing with restart`);
|
|
752
|
-
await client.finishRun(run.id, { status: 'restarted', exit_code: result.exitCode, output: result.output });
|
|
753
|
-
await failSteeredDeliveries(client, activeHandle, options, run.id, 'active run restarted before completion');
|
|
754
|
-
return 'restart';
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
if (result.stoppedByAction === 'kill') {
|
|
758
|
-
console.log(`${logPrefix} run=${run.id} finishing with killed`);
|
|
759
|
-
await client.finishRun(run.id, { status: 'killed', exit_code: result.exitCode, output: result.output });
|
|
760
|
-
await client.ackDelivery(delivery.id, { daemon_id: options.daemonId, connection_id: options.connectionId, claim_token: delivery.claim_token, run_id: run.id });
|
|
761
|
-
await ackSteeredDeliveries(client, activeHandle, options, run.id);
|
|
762
|
-
return 'done';
|
|
763
|
-
}
|
|
764
|
-
|
|
765
848
|
if (result.exitCode === 0) {
|
|
766
849
|
console.log(`${logPrefix} run=${run.id} finishing with completed`);
|
|
767
850
|
await client.finishRun(run.id, { status: 'completed', exit_code: result.exitCode, output: result.output });
|
|
@@ -769,6 +852,7 @@ async function executeRun(client: LockClient, delivery: MessageDelivery, message
|
|
|
769
852
|
await ackCoveredPendingDeliveries(client, {
|
|
770
853
|
agent: options.agent,
|
|
771
854
|
chatId: message.chat_id,
|
|
855
|
+
startMessageId,
|
|
772
856
|
triggerMessageId: message.id,
|
|
773
857
|
maxCoveredMessageId,
|
|
774
858
|
daemonId: options.daemonId,
|
|
@@ -1058,7 +1142,7 @@ async function reconcileManagedAgents(input: {
|
|
|
1058
1142
|
return activatedAgents;
|
|
1059
1143
|
}
|
|
1060
1144
|
|
|
1061
|
-
async function main(): Promise<void> {
|
|
1145
|
+
export async function main(): Promise<void> {
|
|
1062
1146
|
const rawArgs = parseArgs();
|
|
1063
1147
|
// When launched as `bun run src/daemon.ts --agent foo`, the first arg is a flag,
|
|
1064
1148
|
// not a subcommand. Shift it back into flags/values.
|
package/src/db.ts
CHANGED
|
@@ -1448,9 +1448,9 @@ export class MessageStore {
|
|
|
1448
1448
|
this.palHome = options.palHome ?? defaultPalHomeDir();
|
|
1449
1449
|
ensureParentDir(path);
|
|
1450
1450
|
this.db = new Database(path);
|
|
1451
|
+
this.db.exec('PRAGMA busy_timeout = 5000');
|
|
1451
1452
|
this.db.exec('PRAGMA journal_mode = WAL');
|
|
1452
1453
|
this.db.exec('PRAGMA foreign_keys = ON');
|
|
1453
|
-
this.db.exec('PRAGMA busy_timeout = 5000');
|
|
1454
1454
|
runMigrations(this.db);
|
|
1455
1455
|
}
|
|
1456
1456
|
|
|
@@ -4646,7 +4646,7 @@ export class MessageStore {
|
|
|
4646
4646
|
const previous = this.db.query(`
|
|
4647
4647
|
SELECT COALESCE(MAX(message_id), 0) AS message_id
|
|
4648
4648
|
FROM message_deliveries
|
|
4649
|
-
WHERE agent = ? AND chat_id = ? AND message_id < ?
|
|
4649
|
+
WHERE agent = ? AND chat_id = ? AND message_id < ? AND status IN ('acked', 'failed', 'canceled')
|
|
4650
4650
|
`).get(input.agent, room.id, trigger.id) as { message_id: number } | null;
|
|
4651
4651
|
const startMessageId = Math.max(Number(cursor ?? 0), Number(previous?.message_id ?? 0), 0);
|
|
4652
4652
|
const total = this.db.query(`
|
|
@@ -5294,7 +5294,7 @@ export class MessageStore {
|
|
|
5294
5294
|
SELECT md.*
|
|
5295
5295
|
FROM message_deliveries md
|
|
5296
5296
|
INNER JOIN (
|
|
5297
|
-
SELECT pending.chat_id,
|
|
5297
|
+
SELECT pending.chat_id, MAX(pending.rowid) AS first_rowid
|
|
5298
5298
|
FROM message_deliveries pending
|
|
5299
5299
|
WHERE pending.agent = ? AND pending.status = ?
|
|
5300
5300
|
AND NOT EXISTS (
|
|
@@ -5320,7 +5320,7 @@ export class MessageStore {
|
|
|
5320
5320
|
SELECT md.*
|
|
5321
5321
|
FROM message_deliveries md
|
|
5322
5322
|
INNER JOIN (
|
|
5323
|
-
SELECT chat_id,
|
|
5323
|
+
SELECT chat_id, MAX(rowid) AS first_rowid
|
|
5324
5324
|
FROM message_deliveries
|
|
5325
5325
|
WHERE agent = ? AND status = ?
|
|
5326
5326
|
GROUP BY chat_id
|
|
@@ -5338,6 +5338,16 @@ export class MessageStore {
|
|
|
5338
5338
|
return rows.map(rowToDelivery);
|
|
5339
5339
|
}
|
|
5340
5340
|
|
|
5341
|
+
countPendingDeliveriesAfterMessage(input: { roomId: string; agent: string; afterMessageId: number }): number {
|
|
5342
|
+
this.releaseExpiredDeliveries();
|
|
5343
|
+
const row = this.db.query(`
|
|
5344
|
+
SELECT COUNT(*) AS count
|
|
5345
|
+
FROM message_deliveries
|
|
5346
|
+
WHERE chat_id = ? AND agent = ? AND status = 'pending' AND message_id > ?
|
|
5347
|
+
`).get(input.roomId, input.agent.trim(), input.afterMessageId) as { count: number } | null;
|
|
5348
|
+
return Number(row?.count ?? 0);
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5341
5351
|
listRecentDeliveriesForAgent(agent: string, limit = 20): MessageDelivery[] {
|
|
5342
5352
|
this.releaseExpiredDeliveries();
|
|
5343
5353
|
const rows = this.db.query(`
|
|
@@ -5429,7 +5439,7 @@ export class MessageStore {
|
|
|
5429
5439
|
}
|
|
5430
5440
|
const allRows = this.db.query(`
|
|
5431
5441
|
SELECT agent FROM agent_room_subscriptions
|
|
5432
|
-
WHERE room_id = ? AND channel_id IS NULL AND mode IN ('all', 'periodic')
|
|
5442
|
+
WHERE room_id = ? AND channel_id IS NULL AND mode IN ('all', 'available', 'periodic')
|
|
5433
5443
|
`).all(room.id) as Array<{ agent: string }>;
|
|
5434
5444
|
for (const row of allRows) candidates.add(row.agent);
|
|
5435
5445
|
|
|
@@ -5479,6 +5489,8 @@ export class MessageStore {
|
|
|
5479
5489
|
const mode = subscription?.mode ?? 'mentions';
|
|
5480
5490
|
if (mode === 'off') return false;
|
|
5481
5491
|
if (direct) return true;
|
|
5492
|
+
const hasAnyMention = message.recipient !== null || (message.mentions ?? []).length > 0;
|
|
5493
|
+
if (mode === 'available') return !hasAnyMention;
|
|
5482
5494
|
if (mode === 'all') return true;
|
|
5483
5495
|
return false;
|
|
5484
5496
|
}
|
|
@@ -5773,6 +5785,27 @@ export class MessageStore {
|
|
|
5773
5785
|
return this.getDelivery(id)!;
|
|
5774
5786
|
}
|
|
5775
5787
|
|
|
5788
|
+
cancelClaimedDelivery(id: string, input: FinishDeliveryInput): MessageDelivery {
|
|
5789
|
+
const existing = this.getDelivery(id);
|
|
5790
|
+
if (existing?.status === 'canceled') {
|
|
5791
|
+
if ((existing.connection_id ?? existing.daemon_id) === (input.connectionId ?? input.daemonId) && existing.claim_token === input.claimToken && existing.run_id === (input.runId ?? null)) {
|
|
5792
|
+
return existing;
|
|
5793
|
+
}
|
|
5794
|
+
throw new Error(`terminal delivery ${id} conflicts with cancel request`);
|
|
5795
|
+
}
|
|
5796
|
+
if (existing && ['acked', 'failed'].includes(existing.status)) {
|
|
5797
|
+
throw new Error(`terminal delivery ${id} cannot be canceled from ${existing.status}`);
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
const result = this.db.query(`
|
|
5801
|
+
UPDATE message_deliveries
|
|
5802
|
+
SET status = 'canceled', run_id = COALESCE(?, run_id)
|
|
5803
|
+
WHERE id = ? AND status IN ('claimed', 'processing_completed') AND (connection_id = ? OR (connection_id IS NULL AND daemon_id = ?)) AND claim_token = ?
|
|
5804
|
+
`).run(input.runId ?? null, id, input.connectionId ?? input.daemonId, input.daemonId, input.claimToken);
|
|
5805
|
+
if (result.changes === 0) throw new Error(`claimed delivery ${id} was not found`);
|
|
5806
|
+
return this.getDelivery(id)!;
|
|
5807
|
+
}
|
|
5808
|
+
|
|
5776
5809
|
ackDelivery(id: string, input: FinishDeliveryInput): MessageDelivery {
|
|
5777
5810
|
const existing = this.getDelivery(id);
|
|
5778
5811
|
if (existing?.status === 'acked') {
|
package/src/local-api.ts
CHANGED
|
@@ -244,7 +244,7 @@ function requireMessageWritePrincipal(principal: LocalApiPrincipal, body: SendBo
|
|
|
244
244
|
function requireAgentRoomScope(principal: LocalApiPrincipal, roomRef: string): void {
|
|
245
245
|
if (principal.kind === 'daemon') return;
|
|
246
246
|
if (roomRef !== principal.chatId) {
|
|
247
|
-
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only
|
|
247
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only access its active room by chat_id');
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -540,6 +540,17 @@ export async function localRoute(request: Request, options: LocalApiOptions): Pr
|
|
|
540
540
|
return json({ run: await client.restartRoomAgent(roomRef, restartAgent) });
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
+
const roomAgentInterruptMatch = pathname.match(/^\/local\/rooms\/([^/]+)\/agents\/([^/]+)\/interrupt$/);
|
|
544
|
+
if (request.method === 'POST' && roomAgentInterruptMatch) {
|
|
545
|
+
const roomRef = decodeURIComponent(roomAgentInterruptMatch[1]!);
|
|
546
|
+
requireAgentRoomScope(principal, roomRef);
|
|
547
|
+
const interruptAgent = decodeURIComponent(roomAgentInterruptMatch[2]!);
|
|
548
|
+
if (principal.kind === 'agent' && interruptAgent !== principal.agent) {
|
|
549
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only interrupt its own runtime');
|
|
550
|
+
}
|
|
551
|
+
return json(await client.interruptRoomAgent(roomRef, interruptAgent));
|
|
552
|
+
}
|
|
553
|
+
|
|
543
554
|
if (request.method === 'GET' && pathname === '/local/reminders') {
|
|
544
555
|
return json({
|
|
545
556
|
reminders: await client.listReminders({
|
|
@@ -92,7 +92,7 @@ export function up(db: Database): void {
|
|
|
92
92
|
agent TEXT NOT NULL,
|
|
93
93
|
room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
94
94
|
channel_id TEXT REFERENCES room_channels(id) ON DELETE CASCADE,
|
|
95
|
-
mode TEXT NOT NULL DEFAULT 'mentions' CHECK (mode IN ('all', 'periodic', 'mentions', 'muted', 'off')),
|
|
95
|
+
mode TEXT NOT NULL DEFAULT 'mentions' CHECK (mode IN ('all', 'available', 'periodic', 'mentions', 'muted', 'off')),
|
|
96
96
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
97
97
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
98
98
|
UNIQUE(agent, room_id, channel_id)
|
|
@@ -9,7 +9,7 @@ export function up(db: Database): void {
|
|
|
9
9
|
id TEXT PRIMARY KEY,
|
|
10
10
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
11
11
|
agent TEXT NOT NULL,
|
|
12
|
-
mode TEXT NOT NULL DEFAULT 'mentions' CHECK (mode IN ('all', 'periodic', 'mentions', 'muted', 'off')),
|
|
12
|
+
mode TEXT NOT NULL DEFAULT 'mentions' CHECK (mode IN ('all', 'available', 'periodic', 'mentions', 'muted', 'off')),
|
|
13
13
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
14
14
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
15
15
|
UNIQUE(project_id, agent)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 46;
|
|
4
|
+
export const name = 'agent_run_interrupt';
|
|
5
|
+
export const transaction = false;
|
|
6
|
+
|
|
7
|
+
export function up(db: Database): void {
|
|
8
|
+
const row = db.query(`
|
|
9
|
+
SELECT sql
|
|
10
|
+
FROM sqlite_master
|
|
11
|
+
WHERE type = 'table' AND name = 'agent_runs'
|
|
12
|
+
`).get() as { sql?: string } | null;
|
|
13
|
+
const sql = row?.sql ?? '';
|
|
14
|
+
if (sql.includes("'interrupt'") && sql.includes("'cancelled'")) return;
|
|
15
|
+
|
|
16
|
+
db.exec('PRAGMA foreign_keys = OFF');
|
|
17
|
+
db.exec('PRAGMA legacy_alter_table = ON');
|
|
18
|
+
try {
|
|
19
|
+
db.exec(`
|
|
20
|
+
DROP INDEX IF EXISTS idx_agent_runs_message_id;
|
|
21
|
+
DROP INDEX IF EXISTS idx_agent_runs_status;
|
|
22
|
+
DROP INDEX IF EXISTS idx_agent_runs_session_id;
|
|
23
|
+
DROP INDEX IF EXISTS idx_agent_runs_delivery_id;
|
|
24
|
+
DROP INDEX IF EXISTS idx_agent_runs_connection_id;
|
|
25
|
+
DROP INDEX IF EXISTS idx_agent_runs_computer_room_active;
|
|
26
|
+
|
|
27
|
+
ALTER TABLE agent_runs RENAME TO agent_runs_legacy;
|
|
28
|
+
|
|
29
|
+
CREATE TABLE agent_runs (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
32
|
+
agent TEXT NOT NULL,
|
|
33
|
+
status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'failed', 'killed', 'restarted', 'cancelled')),
|
|
34
|
+
action TEXT CHECK (action IN ('kill', 'restart', 'interrupt')),
|
|
35
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
36
|
+
pid INTEGER,
|
|
37
|
+
cwd TEXT NOT NULL,
|
|
38
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
39
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
40
|
+
ended_at TEXT,
|
|
41
|
+
exit_code INTEGER,
|
|
42
|
+
output TEXT NOT NULL DEFAULT '',
|
|
43
|
+
session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
|
|
44
|
+
trigger_message_id INTEGER REFERENCES messages(id) ON DELETE SET NULL,
|
|
45
|
+
daemon_id TEXT REFERENCES daemon_instances(id) ON DELETE SET NULL,
|
|
46
|
+
delivery_id TEXT REFERENCES message_deliveries(id) ON DELETE SET NULL,
|
|
47
|
+
computer_id TEXT REFERENCES computers(id) ON DELETE SET NULL,
|
|
48
|
+
connection_id TEXT REFERENCES computer_connections(id) ON DELETE SET NULL,
|
|
49
|
+
runtime_provider TEXT,
|
|
50
|
+
runtime_invocation_id TEXT
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
INSERT INTO agent_runs (
|
|
54
|
+
id, message_id, agent, status, action, attempt, pid, cwd, started_at, updated_at, ended_at,
|
|
55
|
+
exit_code, output, session_id, trigger_message_id, daemon_id, delivery_id, computer_id,
|
|
56
|
+
connection_id, runtime_provider, runtime_invocation_id
|
|
57
|
+
)
|
|
58
|
+
SELECT
|
|
59
|
+
id, message_id, agent, status, action, attempt, pid, cwd, started_at, updated_at, ended_at,
|
|
60
|
+
exit_code, output, session_id, trigger_message_id, daemon_id, delivery_id, computer_id,
|
|
61
|
+
connection_id, runtime_provider, runtime_invocation_id
|
|
62
|
+
FROM agent_runs_legacy;
|
|
63
|
+
|
|
64
|
+
DROP TABLE agent_runs_legacy;
|
|
65
|
+
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_message_id ON agent_runs(message_id, attempt);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_status ON agent_runs(status, updated_at);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_session_id ON agent_runs(session_id, attempt);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_delivery_id ON agent_runs(delivery_id);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_connection_id ON agent_runs(connection_id);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_computer_room_active
|
|
72
|
+
ON agent_runs(computer_id, agent, status, session_id);
|
|
73
|
+
`);
|
|
74
|
+
} finally {
|
|
75
|
+
db.exec('PRAGMA legacy_alter_table = OFF');
|
|
76
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 47;
|
|
4
|
+
export const name = 'available_receive_mode';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
ALTER TABLE agent_room_subscriptions RENAME TO agent_room_subscriptions_old;
|
|
9
|
+
|
|
10
|
+
CREATE TABLE agent_room_subscriptions (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
agent TEXT NOT NULL,
|
|
13
|
+
room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
14
|
+
channel_id TEXT REFERENCES room_channels(id) ON DELETE CASCADE,
|
|
15
|
+
mode TEXT NOT NULL DEFAULT 'mentions' CHECK (mode IN ('all', 'available', 'periodic', 'mentions', 'muted', 'off')),
|
|
16
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
17
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
18
|
+
last_read_message_id INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
last_read_at TEXT,
|
|
20
|
+
UNIQUE(agent, room_id, channel_id)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
INSERT INTO agent_room_subscriptions (
|
|
24
|
+
id, agent, room_id, channel_id, mode, created_at, updated_at, last_read_message_id, last_read_at
|
|
25
|
+
)
|
|
26
|
+
SELECT id, agent, room_id, channel_id, mode, created_at, updated_at, last_read_message_id, last_read_at
|
|
27
|
+
FROM agent_room_subscriptions_old;
|
|
28
|
+
|
|
29
|
+
DROP TABLE agent_room_subscriptions_old;
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_agent_room_subscriptions_room
|
|
32
|
+
ON agent_room_subscriptions(room_id, mode, agent);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_agent_room_subscriptions_read_state
|
|
34
|
+
ON agent_room_subscriptions(agent, room_id, last_read_message_id);
|
|
35
|
+
|
|
36
|
+
ALTER TABLE project_agent_defaults RENAME TO project_agent_defaults_old;
|
|
37
|
+
|
|
38
|
+
CREATE TABLE project_agent_defaults (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
41
|
+
agent TEXT NOT NULL,
|
|
42
|
+
mode TEXT NOT NULL DEFAULT 'mentions' CHECK (mode IN ('all', 'available', 'periodic', 'mentions', 'muted', 'off')),
|
|
43
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
44
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
45
|
+
UNIQUE(project_id, agent)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
INSERT INTO project_agent_defaults (id, project_id, agent, mode, created_at, updated_at)
|
|
49
|
+
SELECT id, project_id, agent, mode, created_at, updated_at
|
|
50
|
+
FROM project_agent_defaults_old;
|
|
51
|
+
|
|
52
|
+
DROP TABLE project_agent_defaults_old;
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_project_agent_defaults_project
|
|
55
|
+
ON project_agent_defaults(project_id, agent);
|
|
56
|
+
`);
|
|
57
|
+
}
|
package/src/migrations.ts
CHANGED
|
@@ -44,10 +44,13 @@ import * as messageAttachmentFileKind from './migrations/042_message_attachment_
|
|
|
44
44
|
import * as roomModeSkillRegistry from './migrations/043_room_mode_skill_registry.js';
|
|
45
45
|
import * as workflowRuntime from './migrations/044_workflow_runtime.js';
|
|
46
46
|
import * as skillRepositoryOwnership from './migrations/045_skill_repository_ownership.js';
|
|
47
|
+
import * as agentRunInterrupt from './migrations/046_agent_run_interrupt.js';
|
|
48
|
+
import * as availableReceiveMode from './migrations/047_available_receive_mode.js';
|
|
47
49
|
|
|
48
50
|
interface Migration {
|
|
49
51
|
version: number;
|
|
50
52
|
name: string;
|
|
53
|
+
transaction?: boolean;
|
|
51
54
|
up(db: Database): void;
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -97,6 +100,8 @@ const migrations: Migration[] = [
|
|
|
97
100
|
roomModeSkillRegistry,
|
|
98
101
|
workflowRuntime,
|
|
99
102
|
skillRepositoryOwnership,
|
|
103
|
+
agentRunInterrupt,
|
|
104
|
+
availableReceiveMode,
|
|
100
105
|
].sort((a, b) => a.version - b.version);
|
|
101
106
|
|
|
102
107
|
function assertContiguousMigrations(): void {
|
|
@@ -147,9 +152,14 @@ export function runMigrations(db: Database): void {
|
|
|
147
152
|
for (const migration of migrations) {
|
|
148
153
|
if (applied.has(migration.version)) continue;
|
|
149
154
|
|
|
150
|
-
|
|
155
|
+
const applyMigration = () => {
|
|
151
156
|
migration.up(db);
|
|
152
157
|
markApplied(db, migration);
|
|
153
|
-
}
|
|
158
|
+
};
|
|
159
|
+
if (migration.transaction === false) {
|
|
160
|
+
applyMigration();
|
|
161
|
+
} else {
|
|
162
|
+
db.transaction(applyMigration)();
|
|
163
|
+
}
|
|
154
164
|
}
|
|
155
165
|
}
|
package/src/types.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type ProjectStatus = 'active' | 'archived';
|
|
|
5
5
|
export type MessageType = 'message' | 'system';
|
|
6
6
|
export type RoomProvider = 'web' | 'lark' | 'wechat';
|
|
7
7
|
export type RoomTopicCapability = 'native' | 'unsupported';
|
|
8
|
-
export type AgentRoomSubscriptionMode = 'all' | 'periodic' | 'mentions' | 'muted' | 'off';
|
|
8
|
+
export type AgentRoomSubscriptionMode = 'all' | 'available' | 'periodic' | 'mentions' | 'muted' | 'off';
|
|
9
9
|
export type RoomSystemEventType = 'agent_invited' | 'agent_removed' | 'agent_receive_mode_changed';
|
|
10
10
|
export type RoomSystemEventActorKind = 'user' | 'agent' | 'system';
|
|
11
11
|
export type PalIdentityKind = 'user' | 'bot' | 'agent' | 'room';
|
|
@@ -181,8 +181,8 @@ export interface MessageAttachment extends MessageAttachmentMetadata {
|
|
|
181
181
|
source_ref: string | null;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
export type RunStatus = 'running' | 'completed' | 'failed' | 'killed' | 'restarted';
|
|
185
|
-
export type RunAction = 'kill' | 'restart';
|
|
184
|
+
export type RunStatus = 'running' | 'completed' | 'failed' | 'killed' | 'restarted' | 'cancelled';
|
|
185
|
+
export type RunAction = 'kill' | 'restart' | 'interrupt';
|
|
186
186
|
export type DeliveryStatus = 'pending' | 'claimed' | 'processing_completed' | 'acked' | 'failed' | 'canceled';
|
|
187
187
|
export type HeldDraftStatus = 'held' | 'abandoned' | 'sent_anyway';
|
|
188
188
|
export type RoomTaskStatus = 'todo' | 'in_progress' | 'in_review' | 'done' | 'closed';
|