@controlflow-ai/daemon 0.1.2 → 0.1.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/README.md +54 -6
- package/package.json +3 -1
- package/src/agent-avatar.ts +30 -0
- package/src/agent-key.ts +28 -0
- package/src/agent-permissions.ts +359 -0
- package/src/agent-runtime.ts +795 -28
- package/src/agent-workspace.ts +183 -0
- package/src/app.ts +1970 -79
- package/src/args.ts +54 -7
- package/src/cli.ts +873 -14
- package/src/client.ts +472 -10
- package/src/coco.ts +9 -40
- package/src/codex.ts +33 -5
- package/src/config.ts +28 -4
- package/src/console.ts +230 -20
- package/src/daemon-client.ts +116 -3
- package/src/daemon.ts +936 -98
- package/src/db.ts +3128 -122
- package/src/delivery-ws.ts +269 -0
- package/src/format.ts +4 -1
- package/src/lark/cli.ts +3 -3
- package/src/lark/event-router.ts +60 -4
- package/src/lark/inbound-events.ts +156 -3
- package/src/lark/server-integration.ts +659 -111
- package/src/lark/ws-daemon.ts +136 -10
- package/src/local-api.ts +545 -15
- package/src/local-auth.ts +33 -1
- package/src/message-attachments.ts +71 -0
- package/src/messaging-cli.ts +741 -0
- package/src/messaging-status.ts +669 -0
- package/src/migrations/024_agents_model.ts +10 -0
- package/src/migrations/025_room_archive.ts +44 -0
- package/src/migrations/026_project_archive.ts +44 -0
- package/src/migrations/027_agent_permission_profiles.ts +16 -0
- package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
- package/src/migrations/029_held_message_drafts.ts +32 -0
- package/src/migrations/030_agent_room_read_state.ts +25 -0
- package/src/migrations/031_room_tasks.ts +29 -0
- package/src/migrations/032_room_reminders.ts +29 -0
- package/src/migrations/033_room_saved_messages.ts +25 -0
- package/src/migrations/034_agent_activity_events.ts +27 -0
- package/src/migrations/035_agent_avatars.ts +17 -0
- package/src/migrations/036_project_agent_defaults.ts +21 -0
- package/src/migrations/037_message_attachments.ts +36 -0
- package/src/migrations/038_agent_activity_room_scope.ts +64 -0
- package/src/migrations/039_message_attachments_path.ts +34 -0
- package/src/migrations/040_message_attachments_file_schema.ts +80 -0
- package/src/migrations/041_room_system_events.ts +30 -0
- package/src/migrations/042_message_attachment_file_kind.ts +52 -0
- package/src/migrations/043_room_mode_skill_registry.ts +92 -0
- package/src/migrations/044_workflow_runtime.ts +69 -0
- package/src/migrations/045_skill_repository_ownership.ts +64 -0
- package/src/migrations.ts +69 -1
- package/src/neeko.ts +40 -4
- package/src/runtime-env.ts +179 -0
- package/src/runtime-registry.ts +83 -13
- package/src/server.ts +244 -4
- package/src/token-file.ts +13 -6
- package/src/types.ts +362 -0
- package/src/workflow-runtime.ts +275 -0
- package/src/web.ts +0 -904
package/src/db.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Database, type SQLQueryBindings } from 'bun:sqlite';
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
3
3
|
import { isAbsolute } from 'node:path';
|
|
4
|
-
import { ensureParentDir } from './config.js';
|
|
4
|
+
import { ensureParentDir, homeDir as defaultPalHomeDir } from './config.js';
|
|
5
5
|
import { runMigrations } from './migrations.js';
|
|
6
6
|
import { artifactExpiry, generateArtifactToken, hashArtifactToken, validateArtifactContent } from './artifacts.js';
|
|
7
|
+
import { storeMessageAttachmentFile, validateMessageAttachment } from './message-attachments.js';
|
|
7
8
|
import { palIdentityHandle } from './provider-identity.js';
|
|
8
|
-
import type { AgentDefinition, AgentRun, AgentSession, AgentValidationResult, Artifact, ArtifactMetadata, ChannelAccount, ChannelConversation, ChannelMessageMapping, ChannelOutboxRecord, Chat, ChatKind, Computer, ComputerAgentAssignment, ComputerConnection, DaemonInstance, LarkAuthorizedUser, LarkGroupRoomMapping, LockProviderEvidence, LockTranscriptMessage, Message, MessageDelivery, MessageType, AgentRoomSubscription, AgentRoomSubscriptionMode, PendingInboundEvent, Project, ProvisionedComputer, ProviderExternalType, ProviderIdentityBinding, PalIdentity, RoomChannel, RoomParticipant, RoomParticipantKind, RoomParticipantSource, RoomProvider, RunAction, RunStatus, TranscriptReadModel, WorkbenchArtifact } from './types.js';
|
|
9
|
+
import type { AgentActivityEvent, AgentActivityKind, AgentDefinition, AgentDeletionImpact, AgentDeletionLarkBindingImpact, AgentDeletionResult, AgentDeletionRoomImpact, AgentPermissionProfile, AgentRun, AgentSession, AgentValidationResult, AgentWorkbench, AgentWorkbenchCollaboration, AgentWorkbenchLarkBinding, AgentWorkbenchRoom, Artifact, ArtifactMetadata, ChannelAccount, ChannelConversation, ChannelMessageMapping, ChannelOutboxRecord, Chat, ChatKind, ChatStatus, Computer, ComputerAgentAssignment, ComputerConnection, ComputerDeletionImpact, ComputerDeletionResult, ComputerWorkload, DaemonInstance, DeliveryContext, EnabledSkill, HeldDraftStatus, HeldMessageDraft, LarkAuthorizedUser, LarkGroupRoomMapping, LockProviderEvidence, LockTranscriptMessage, Message, MessageAttachment, MessageAttachmentKind, MessageAttachmentMetadata, MessageDelivery, MessageType, AgentRoomSubscription, AgentRoomSubscriptionMode, PendingInboundEvent, Project, ProjectAgentDefault, ProjectStatus, ProvisionedComputer, ProviderExternalType, ProviderIdentityBinding, PalIdentity, RoomAgentActivityStatus, RoomChannel, RoomParticipant, RoomParticipantKind, RoomParticipantSource, RoomProvider, RoomReminder, RoomReminderStatus, RoomSavedMessage, RoomMode, RoomSystemEvent, RoomSystemEventActorKind, RoomSystemEventType, RoomTask, RoomTaskStatus, RunAction, RunStatus, SkillBinding, SkillBindingScope, SkillDefinition, SkillSource, TranscriptReadModel, WorkbenchArtifact, WorkflowNode, WorkflowNodeKind, WorkflowNodeStatus, WorkflowRun, WorkflowRunRecord, WorkflowRunStatus } from './types.js';
|
|
10
|
+
import { defaultAgentPermissionProfile, normalizeExtraWritableRoots, validateAgentPermissionProfile, type AgentPermissionProfile as RuntimeAgentPermissionProfile } from './agent-permissions.js';
|
|
11
|
+
import { normalizeAgentAvatar } from './agent-avatar.js';
|
|
9
12
|
|
|
10
13
|
export const ALL_AGENTS_MENTION = '__pal_all_agents__';
|
|
11
14
|
|
|
@@ -21,6 +24,16 @@ export interface CreateMessageInput {
|
|
|
21
24
|
channelId?: string | null;
|
|
22
25
|
provider?: RoomProvider;
|
|
23
26
|
mentions?: string[];
|
|
27
|
+
attachments?: CreateMessageAttachmentInput[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CreateMessageAttachmentInput {
|
|
31
|
+
kind: MessageAttachmentKind;
|
|
32
|
+
mimeType: string;
|
|
33
|
+
filename?: string | null;
|
|
34
|
+
content: Uint8Array;
|
|
35
|
+
sourceProvider?: RoomProvider | null;
|
|
36
|
+
sourceRef?: string | null;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export interface ListMessagesInput {
|
|
@@ -28,10 +41,36 @@ export interface ListMessagesInput {
|
|
|
28
41
|
chatName?: string;
|
|
29
42
|
parentId?: number | null;
|
|
30
43
|
after?: number;
|
|
44
|
+
before?: number;
|
|
31
45
|
limit?: number;
|
|
32
46
|
q?: string;
|
|
33
47
|
}
|
|
34
48
|
|
|
49
|
+
export interface UpdateAgentRoomSubscriptionInput {
|
|
50
|
+
roomId: string;
|
|
51
|
+
agent: string;
|
|
52
|
+
mode: AgentRoomSubscriptionMode;
|
|
53
|
+
actorKind?: RoomSystemEventActorKind;
|
|
54
|
+
actorId?: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RecordRoomSystemEventInput {
|
|
58
|
+
roomId: string;
|
|
59
|
+
eventType: RoomSystemEventType;
|
|
60
|
+
actorKind?: RoomSystemEventActorKind;
|
|
61
|
+
actorId?: string | null;
|
|
62
|
+
subjectAgent?: string | null;
|
|
63
|
+
metadata?: Record<string, unknown> | null;
|
|
64
|
+
content?: string;
|
|
65
|
+
deliveryAgents?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface MarkAgentRoomReadInput {
|
|
69
|
+
roomId: string;
|
|
70
|
+
agent: string;
|
|
71
|
+
messageId?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
export interface StartRunInput {
|
|
36
75
|
messageId: number;
|
|
37
76
|
agent: string;
|
|
@@ -54,6 +93,14 @@ export interface FinishRunInput {
|
|
|
54
93
|
output?: string;
|
|
55
94
|
}
|
|
56
95
|
|
|
96
|
+
export interface RecordAgentActivityInput {
|
|
97
|
+
runId: string;
|
|
98
|
+
kind: AgentActivityKind;
|
|
99
|
+
title: string;
|
|
100
|
+
detail?: string | null;
|
|
101
|
+
metadata?: Record<string, unknown> | null;
|
|
102
|
+
}
|
|
103
|
+
|
|
57
104
|
export interface RegisterDaemonInput {
|
|
58
105
|
id?: string;
|
|
59
106
|
name: string;
|
|
@@ -97,16 +144,155 @@ export interface ProvisionComputerInput {
|
|
|
97
144
|
packageName?: string;
|
|
98
145
|
}
|
|
99
146
|
|
|
147
|
+
export interface RegenerateComputerCommandInput {
|
|
148
|
+
serverUrl?: string;
|
|
149
|
+
packageName?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
100
152
|
export interface CreateDeliveryInput {
|
|
101
153
|
messageId: number;
|
|
102
154
|
agent: string;
|
|
103
155
|
}
|
|
104
156
|
|
|
157
|
+
function daemonCommandPrefix(input?: string): string {
|
|
158
|
+
const raw = input?.trim() || 'bun run daemon';
|
|
159
|
+
if (/\s/.test(raw) || raw.startsWith('./') || raw.startsWith('/') || raw.startsWith('node ')) return raw;
|
|
160
|
+
return `npx ${raw}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function shellValue(value: string): string {
|
|
164
|
+
if (/^[A-Za-z0-9_./:@-]+$/.test(value)) return value;
|
|
165
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function daemonStartCommand(input: { commandPrefix?: string; serverUrl: string; apiKey: string; name: string }): string {
|
|
169
|
+
return `PAL_SERVER=${shellValue(input.serverUrl)} PAL_API_KEY=${shellValue(input.apiKey)} ${daemonCommandPrefix(input.commandPrefix)} # ${input.name}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface CreateHeldMessageDraftInput {
|
|
173
|
+
chatId: string;
|
|
174
|
+
agent: string;
|
|
175
|
+
sender: string;
|
|
176
|
+
content: string;
|
|
177
|
+
mentions?: string[];
|
|
178
|
+
baseMessageId: number;
|
|
179
|
+
latestMessageIdAtHold: number;
|
|
180
|
+
holdReason: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface CreateRoomTasksInput {
|
|
184
|
+
roomId: string;
|
|
185
|
+
titles: string[];
|
|
186
|
+
createdBy?: string | null;
|
|
187
|
+
sourceMessageId?: number | null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface ClaimRoomTaskInput {
|
|
191
|
+
roomId: string;
|
|
192
|
+
taskNumber: number;
|
|
193
|
+
assignee: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface UpdateRoomTaskInput {
|
|
197
|
+
roomId: string;
|
|
198
|
+
taskNumber: number;
|
|
199
|
+
status: RoomTaskStatus;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface CreateRoomReminderInput {
|
|
203
|
+
sourceMessageId: number;
|
|
204
|
+
title: string;
|
|
205
|
+
createdBy?: string | null;
|
|
206
|
+
fireAt?: string | null;
|
|
207
|
+
delaySeconds?: number | null;
|
|
208
|
+
repeat?: string | null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface ListRoomRemindersInput {
|
|
212
|
+
roomId?: string | null;
|
|
213
|
+
status?: RoomReminderStatus | 'all';
|
|
214
|
+
createdBy?: string | null;
|
|
215
|
+
limit?: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface CreateRoomSavedMessageInput {
|
|
219
|
+
sourceMessageId: number;
|
|
220
|
+
savedBy: string;
|
|
221
|
+
note?: string | null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface ListRoomSavedMessagesInput {
|
|
225
|
+
roomId?: string | null;
|
|
226
|
+
savedBy?: string | null;
|
|
227
|
+
limit?: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface CreateWorkflowRunInput {
|
|
231
|
+
filePath: string;
|
|
232
|
+
goal?: string | null;
|
|
233
|
+
createdBy?: string | null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface FinishWorkflowRunInput {
|
|
237
|
+
status: WorkflowRunStatus;
|
|
238
|
+
finalOutput?: Record<string, unknown> | null;
|
|
239
|
+
error?: string | null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface CreateWorkflowNodeInput {
|
|
243
|
+
runId: string;
|
|
244
|
+
parentId?: string | null;
|
|
245
|
+
kind: WorkflowNodeKind;
|
|
246
|
+
title: string;
|
|
247
|
+
role?: string | null;
|
|
248
|
+
status?: WorkflowNodeStatus;
|
|
249
|
+
context?: Record<string, unknown> | null;
|
|
250
|
+
instruction?: string | null;
|
|
251
|
+
capabilities?: string[];
|
|
252
|
+
outputContract?: Record<string, unknown> | null;
|
|
253
|
+
output?: Record<string, unknown> | null;
|
|
254
|
+
evidence?: Record<string, unknown> | null;
|
|
255
|
+
taskId?: string | null;
|
|
256
|
+
messageId?: number | null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface UpdateWorkflowNodeInput {
|
|
260
|
+
status?: WorkflowNodeStatus;
|
|
261
|
+
output?: Record<string, unknown> | null;
|
|
262
|
+
evidence?: Record<string, unknown> | null;
|
|
263
|
+
taskId?: string | null;
|
|
264
|
+
messageId?: number | null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface SaveRoomMessageResult {
|
|
268
|
+
savedMessage: RoomSavedMessage;
|
|
269
|
+
created: boolean;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface FireDueRoomRemindersInput {
|
|
273
|
+
now?: string | Date | null;
|
|
274
|
+
limit?: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface FiredRoomReminder {
|
|
278
|
+
reminder: RoomReminder;
|
|
279
|
+
message: Message;
|
|
280
|
+
deliveries: MessageDelivery[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface FireDueRoomRemindersResult {
|
|
284
|
+
reminders: RoomReminder[];
|
|
285
|
+
messages: Message[];
|
|
286
|
+
deliveries: MessageDelivery[];
|
|
287
|
+
fired: FiredRoomReminder[];
|
|
288
|
+
}
|
|
289
|
+
|
|
105
290
|
export interface ClaimDeliveryInput {
|
|
106
291
|
daemonId: string;
|
|
107
292
|
connectionId?: string | null;
|
|
108
293
|
computerId?: string | null;
|
|
109
294
|
leaseMs?: number;
|
|
295
|
+
steerRunId?: string | null;
|
|
110
296
|
}
|
|
111
297
|
|
|
112
298
|
export interface FinishDeliveryInput {
|
|
@@ -117,6 +303,22 @@ export interface FinishDeliveryInput {
|
|
|
117
303
|
error?: string;
|
|
118
304
|
}
|
|
119
305
|
|
|
306
|
+
export interface DeliveryBacklogSummary {
|
|
307
|
+
agent: string;
|
|
308
|
+
pending: number;
|
|
309
|
+
claimed: number;
|
|
310
|
+
processing_completed: number;
|
|
311
|
+
expired_active: number;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface LarkWebsocketRestartState {
|
|
315
|
+
app_id: string;
|
|
316
|
+
restart_count: number;
|
|
317
|
+
last_restart_at: string | null;
|
|
318
|
+
last_restart_reason: string | null;
|
|
319
|
+
updated_at: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
120
322
|
export interface CreateArtifactInput {
|
|
121
323
|
title?: string;
|
|
122
324
|
filename?: string;
|
|
@@ -252,7 +454,7 @@ export interface UpsertRoomParticipantInput {
|
|
|
252
454
|
source: RoomParticipantSource;
|
|
253
455
|
}
|
|
254
456
|
|
|
255
|
-
const agentFieldAllowlist = new Set(['id', 'agent_key', 'display_name', 'description', 'runtime', 'created_at', 'updated_at']);
|
|
457
|
+
const agentFieldAllowlist = new Set(['id', 'agent_key', 'display_name', 'description', 'runtime', 'model', 'created_at', 'updated_at']);
|
|
256
458
|
const forbiddenAgentFieldPattern = /provider|adapter|enabled|disabled|status|state|mode|config|capabilities|route|routing|binding|permission|owner|workspace|daemon|delivery|channel|lark|acl|authz/i;
|
|
257
459
|
|
|
258
460
|
export type ChannelMessageConflictCode =
|
|
@@ -315,6 +517,156 @@ function normalizeChatName(name?: string): string {
|
|
|
315
517
|
return stripped;
|
|
316
518
|
}
|
|
317
519
|
|
|
520
|
+
const roomTaskStatuses: RoomTaskStatus[] = ['todo', 'in_progress', 'in_review', 'done', 'closed'];
|
|
521
|
+
const roomReminderStatuses: RoomReminderStatus[] = ['scheduled', 'fired', 'canceled'];
|
|
522
|
+
const roomModes: RoomMode[] = ['standard', 'idea_development'];
|
|
523
|
+
const skillSources: SkillSource[] = ['system', 'user', 'project'];
|
|
524
|
+
const skillBindingScopes: SkillBindingScope[] = ['project', 'room', 'agent'];
|
|
525
|
+
const skillStatuses: SkillDefinition['status'][] = ['active', 'disabled'];
|
|
526
|
+
const workflowRunStatuses: WorkflowRunStatus[] = ['running', 'completed', 'failed'];
|
|
527
|
+
const workflowNodeKinds: WorkflowNodeKind[] = ['phase', 'agent', 'task', 'message', 'final'];
|
|
528
|
+
const workflowNodeStatuses: WorkflowNodeStatus[] = ['pending', 'running', 'done', 'failed'];
|
|
529
|
+
|
|
530
|
+
function assertRoomMode(value: string): RoomMode {
|
|
531
|
+
if (!roomModes.includes(value as RoomMode)) throw new Error('INVALID_ROOM_MODE');
|
|
532
|
+
return value as RoomMode;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function assertSkillBindingScope(value: string): SkillBindingScope {
|
|
536
|
+
if (!skillBindingScopes.includes(value as SkillBindingScope)) throw new Error('INVALID_SKILL_BINDING_SCOPE');
|
|
537
|
+
return value as SkillBindingScope;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function assertSkillSource(value: string): SkillSource {
|
|
541
|
+
if (!skillSources.includes(value as SkillSource)) throw new Error('INVALID_SKILL_SOURCE');
|
|
542
|
+
return value as SkillSource;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function assertSkillStatus(value: string): SkillDefinition['status'] {
|
|
546
|
+
if (!skillStatuses.includes(value as SkillDefinition['status'])) throw new Error('INVALID_SKILL_STATUS');
|
|
547
|
+
return value as SkillDefinition['status'];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function assertRoomTaskStatus(value: string): RoomTaskStatus {
|
|
551
|
+
if (!roomTaskStatuses.includes(value as RoomTaskStatus)) throw new Error('INVALID_TASK_STATUS');
|
|
552
|
+
return value as RoomTaskStatus;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function assertRoomReminderStatus(value: string): RoomReminderStatus {
|
|
556
|
+
if (!roomReminderStatuses.includes(value as RoomReminderStatus)) throw new Error('INVALID_REMINDER_STATUS');
|
|
557
|
+
return value as RoomReminderStatus;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function assertWorkflowRunStatus(value: string): WorkflowRunStatus {
|
|
561
|
+
if (!workflowRunStatuses.includes(value as WorkflowRunStatus)) throw new Error('INVALID_WORKFLOW_RUN_STATUS');
|
|
562
|
+
return value as WorkflowRunStatus;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function assertWorkflowNodeKind(value: string): WorkflowNodeKind {
|
|
566
|
+
if (!workflowNodeKinds.includes(value as WorkflowNodeKind)) throw new Error('INVALID_WORKFLOW_NODE_KIND');
|
|
567
|
+
return value as WorkflowNodeKind;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function assertWorkflowNodeStatus(value: string): WorkflowNodeStatus {
|
|
571
|
+
if (!workflowNodeStatuses.includes(value as WorkflowNodeStatus)) throw new Error('INVALID_WORKFLOW_NODE_STATUS');
|
|
572
|
+
return value as WorkflowNodeStatus;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function jsonRecordOrNull(value: unknown): Record<string, unknown> | null {
|
|
576
|
+
if (value === null || value === undefined || value === '') return null;
|
|
577
|
+
try {
|
|
578
|
+
const parsed = JSON.parse(String(value)) as unknown;
|
|
579
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
580
|
+
return parsed as Record<string, unknown>;
|
|
581
|
+
} catch {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function jsonStringArray(value: unknown): string[] {
|
|
587
|
+
if (value === null || value === undefined || value === '') return [];
|
|
588
|
+
try {
|
|
589
|
+
const parsed = JSON.parse(String(value)) as unknown;
|
|
590
|
+
if (!Array.isArray(parsed)) return [];
|
|
591
|
+
return parsed.filter((entry): entry is string => typeof entry === 'string');
|
|
592
|
+
} catch {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function normalizeReminderFireAt(input: { fireAt?: string | null; delaySeconds?: number | null }): string {
|
|
598
|
+
if (input.fireAt?.trim()) {
|
|
599
|
+
const date = new Date(input.fireAt);
|
|
600
|
+
if (Number.isNaN(date.getTime())) throw new Error('INVALID_REMINDER_TIME');
|
|
601
|
+
return date.toISOString();
|
|
602
|
+
}
|
|
603
|
+
if (input.delaySeconds !== undefined && input.delaySeconds !== null) {
|
|
604
|
+
if (!Number.isFinite(input.delaySeconds) || input.delaySeconds < 0) throw new Error('INVALID_REMINDER_TIME');
|
|
605
|
+
return new Date(Date.now() + input.delaySeconds * 1000).toISOString();
|
|
606
|
+
}
|
|
607
|
+
throw new Error('INVALID_REMINDER_TIME');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function normalizeReminderFireNow(value?: string | Date | null): string {
|
|
611
|
+
if (value === undefined || value === null) return new Date().toISOString();
|
|
612
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
613
|
+
if (Number.isNaN(date.getTime())) throw new Error('INVALID_REMINDER_TIME');
|
|
614
|
+
return date.toISOString();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const reminderRepeatAliases = new Map<string, string>([
|
|
618
|
+
['hourly', 'every:1h'],
|
|
619
|
+
['daily', 'every:1d'],
|
|
620
|
+
['weekly', 'every:1w'],
|
|
621
|
+
]);
|
|
622
|
+
|
|
623
|
+
const reminderRepeatUnitMs: Record<string, number> = {
|
|
624
|
+
s: 1000,
|
|
625
|
+
m: 60 * 1000,
|
|
626
|
+
h: 60 * 60 * 1000,
|
|
627
|
+
d: 24 * 60 * 60 * 1000,
|
|
628
|
+
w: 7 * 24 * 60 * 60 * 1000,
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
function normalizeReminderRepeat(value?: string | null): string | null {
|
|
632
|
+
const raw = value?.trim().toLowerCase() || '';
|
|
633
|
+
if (!raw || raw === 'none' || raw === 'off') return null;
|
|
634
|
+
const aliased = reminderRepeatAliases.get(raw) ?? raw;
|
|
635
|
+
const match = aliased.match(/^every:(\d+)([smhdw])$/);
|
|
636
|
+
if (!match) throw new Error('INVALID_REMINDER_REPEAT');
|
|
637
|
+
const count = Number(match[1]);
|
|
638
|
+
const unit = match[2]!;
|
|
639
|
+
if (!Number.isInteger(count) || count < 1) throw new Error('INVALID_REMINDER_REPEAT');
|
|
640
|
+
return `every:${count}${unit}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function reminderRepeatMs(repeat: string): number {
|
|
644
|
+
const normalized = normalizeReminderRepeat(repeat);
|
|
645
|
+
if (!normalized) throw new Error('INVALID_REMINDER_REPEAT');
|
|
646
|
+
const match = normalized.match(/^every:(\d+)([smhdw])$/)!;
|
|
647
|
+
return Number(match[1]) * reminderRepeatUnitMs[match[2]!]!;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function nextReminderFireAt(reminder: RoomReminder, now: string): string {
|
|
651
|
+
const fireMs = new Date(reminder.fire_at).getTime();
|
|
652
|
+
const nowMs = new Date(now).getTime();
|
|
653
|
+
const intervalMs = reminderRepeatMs(reminder.repeat ?? '');
|
|
654
|
+
if (Number.isNaN(fireMs) || Number.isNaN(nowMs)) throw new Error('INVALID_REMINDER_TIME');
|
|
655
|
+
const periods = Math.max(1, Math.floor((nowMs - fireMs) / intervalMs) + 1);
|
|
656
|
+
return new Date(fireMs + periods * intervalMs).toISOString();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function reminderFireIdempotencyKey(reminder: RoomReminder): string {
|
|
660
|
+
return reminder.repeat ? `room-reminder:${reminder.id}:fire:${reminder.fire_at}` : `room-reminder:${reminder.id}:fire`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function reminderFireMessageContent(reminder: RoomReminder, source: Message | null): string {
|
|
664
|
+
const sourceLine = source
|
|
665
|
+
? `Source message #${source.id} from @${source.sender}: ${source.content}`
|
|
666
|
+
: `Source message #${reminder.source_message_id}`;
|
|
667
|
+
return `Reminder: ${reminder.title}\n${sourceLine}`;
|
|
668
|
+
}
|
|
669
|
+
|
|
318
670
|
function parseCapabilities(value: unknown): { topics: 'native' | 'unsupported' } {
|
|
319
671
|
if (typeof value !== 'string') return { topics: 'native' };
|
|
320
672
|
try {
|
|
@@ -334,12 +686,32 @@ function generateConnectionToken(): string {
|
|
|
334
686
|
}
|
|
335
687
|
|
|
336
688
|
function rowToAgent(row: Record<string, unknown>): AgentDefinition {
|
|
689
|
+
const agentKey = String(row.agent_key);
|
|
690
|
+
const displayName = String(row.display_name);
|
|
337
691
|
return {
|
|
338
692
|
id: String(row.id),
|
|
339
|
-
agent_key:
|
|
340
|
-
display_name:
|
|
693
|
+
agent_key: agentKey,
|
|
694
|
+
display_name: displayName,
|
|
341
695
|
description: row.description === null ? null : String(row.description),
|
|
342
696
|
runtime: row.runtime === null ? null : String(row.runtime),
|
|
697
|
+
model: row.model === null || row.model === undefined ? null : String(row.model),
|
|
698
|
+
avatar: normalizeAgentAvatar(row.avatar, agentKey, displayName),
|
|
699
|
+
created_at: String(row.created_at),
|
|
700
|
+
updated_at: String(row.updated_at),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function rowToAgentPermissionProfile(row: Record<string, unknown>): AgentPermissionProfile {
|
|
705
|
+
let roots: string[] = [];
|
|
706
|
+
try {
|
|
707
|
+
roots = normalizeExtraWritableRoots(JSON.parse(String(row.extra_writable_roots_json ?? '[]')));
|
|
708
|
+
} catch {
|
|
709
|
+
roots = [];
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
agent_key: String(row.agent_key),
|
|
713
|
+
filesystem_mode: String(row.filesystem_mode) as AgentPermissionProfile['filesystem_mode'],
|
|
714
|
+
extra_writable_roots: roots,
|
|
343
715
|
created_at: String(row.created_at),
|
|
344
716
|
updated_at: String(row.updated_at),
|
|
345
717
|
};
|
|
@@ -350,6 +722,7 @@ function rowToComputerAgentAssignment(row: Record<string, unknown>): ComputerAge
|
|
|
350
722
|
agent: String(row.agent),
|
|
351
723
|
display_name: String(row.display_name),
|
|
352
724
|
runtime: row.runtime === null ? null : String(row.runtime),
|
|
725
|
+
model: row.model === null || row.model === undefined ? null : String(row.model),
|
|
353
726
|
computer_id: String(row.computer_id),
|
|
354
727
|
cwd: String(row.cwd ?? ''),
|
|
355
728
|
status: String(row.status) as ComputerAgentAssignment['status'],
|
|
@@ -365,12 +738,64 @@ function rowToProject(row: Record<string, unknown>): Project {
|
|
|
365
738
|
computer_id: String(row.computer_id),
|
|
366
739
|
computer_name: row.computer_name === null || row.computer_name === undefined ? null : String(row.computer_name),
|
|
367
740
|
root_path: String(row.root_path),
|
|
741
|
+
status: row.status === null || row.status === undefined ? 'active' : String(row.status) as Project['status'],
|
|
368
742
|
room_count: Number(row.room_count ?? 0),
|
|
369
743
|
created_at: String(row.created_at),
|
|
370
744
|
updated_at: String(row.updated_at),
|
|
371
745
|
};
|
|
372
746
|
}
|
|
373
747
|
|
|
748
|
+
function rowToProjectAgentDefault(row: Record<string, unknown>): ProjectAgentDefault {
|
|
749
|
+
return {
|
|
750
|
+
id: String(row.id),
|
|
751
|
+
project_id: String(row.project_id),
|
|
752
|
+
agent: String(row.agent),
|
|
753
|
+
display_name: row.display_name === null || row.display_name === undefined ? null : String(row.display_name),
|
|
754
|
+
mode: String(row.mode) as AgentRoomSubscriptionMode,
|
|
755
|
+
created_at: String(row.created_at),
|
|
756
|
+
updated_at: String(row.updated_at),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function rowToSkillDefinition(row: Record<string, unknown>): SkillDefinition {
|
|
761
|
+
return {
|
|
762
|
+
key: String(row.key),
|
|
763
|
+
name: String(row.name),
|
|
764
|
+
description: String(row.description),
|
|
765
|
+
instruction_content: String(row.instruction_content),
|
|
766
|
+
status: String(row.status) as SkillDefinition['status'],
|
|
767
|
+
version: String(row.version),
|
|
768
|
+
source: String(row.source ?? 'system') as SkillSource,
|
|
769
|
+
owner_user_id: row.owner_user_id === null || row.owner_user_id === undefined ? null : String(row.owner_user_id),
|
|
770
|
+
project_id: row.project_id === null || row.project_id === undefined ? null : String(row.project_id),
|
|
771
|
+
repository_path: row.repository_path === null || row.repository_path === undefined ? null : String(row.repository_path),
|
|
772
|
+
created_at: String(row.created_at),
|
|
773
|
+
updated_at: String(row.updated_at),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function rowToSkillBinding(row: Record<string, unknown>): SkillBinding {
|
|
778
|
+
return {
|
|
779
|
+
id: String(row.id),
|
|
780
|
+
scope: String(row.scope) as SkillBindingScope,
|
|
781
|
+
scope_id: String(row.scope_id),
|
|
782
|
+
skill_key: String(row.skill_key),
|
|
783
|
+
enabled: Number(row.enabled) === 1,
|
|
784
|
+
priority: Number(row.priority),
|
|
785
|
+
created_at: String(row.created_at),
|
|
786
|
+
updated_at: String(row.updated_at),
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function rowToEnabledSkill(row: Record<string, unknown>): EnabledSkill {
|
|
791
|
+
return {
|
|
792
|
+
...rowToSkillDefinition(row),
|
|
793
|
+
binding_scope: String(row.binding_scope) as SkillBindingScope,
|
|
794
|
+
binding_scope_id: String(row.binding_scope_id),
|
|
795
|
+
binding_priority: Number(row.binding_priority),
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
374
799
|
function rowToMessage(row: Record<string, unknown>): Message {
|
|
375
800
|
return {
|
|
376
801
|
id: Number(row.id),
|
|
@@ -387,6 +812,161 @@ function rowToMessage(row: Record<string, unknown>): Message {
|
|
|
387
812
|
channel_id: row.channel_id === null || row.channel_id === undefined ? null : String(row.channel_id),
|
|
388
813
|
provider: row.provider === null || row.provider === undefined ? undefined : String(row.provider) as RoomProvider,
|
|
389
814
|
mentions: parseMentionsJson(row.mentions_json),
|
|
815
|
+
attachments: [],
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function rowToRoomSystemEvent(row: Record<string, unknown>): RoomSystemEvent {
|
|
820
|
+
let metadata: Record<string, unknown> = {};
|
|
821
|
+
try {
|
|
822
|
+
const parsed = JSON.parse(String(row.metadata_json ?? '{}')) as unknown;
|
|
823
|
+
metadata = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
824
|
+
} catch {
|
|
825
|
+
metadata = {};
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
id: String(row.id),
|
|
829
|
+
room_id: String(row.room_id),
|
|
830
|
+
message_id: Number(row.message_id),
|
|
831
|
+
event_type: String(row.event_type) as RoomSystemEventType,
|
|
832
|
+
actor_kind: String(row.actor_kind) as RoomSystemEventActorKind,
|
|
833
|
+
actor_id: row.actor_id === null || row.actor_id === undefined ? null : String(row.actor_id),
|
|
834
|
+
subject_agent: row.subject_agent === null || row.subject_agent === undefined ? null : String(row.subject_agent),
|
|
835
|
+
metadata,
|
|
836
|
+
created_at: String(row.created_at),
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function rowToMessageAttachmentMetadata(row: Record<string, unknown>): MessageAttachmentMetadata {
|
|
841
|
+
return {
|
|
842
|
+
id: String(row.id),
|
|
843
|
+
message_id: Number(row.message_id),
|
|
844
|
+
kind: String(row.kind) as MessageAttachmentKind,
|
|
845
|
+
mime_type: String(row.mime_type),
|
|
846
|
+
filename: String(row.filename ?? ''),
|
|
847
|
+
size_bytes: Number(row.size_bytes),
|
|
848
|
+
path: String(row.path ?? ''),
|
|
849
|
+
source_provider: row.source_provider === null || row.source_provider === undefined ? null : String(row.source_provider) as RoomProvider,
|
|
850
|
+
created_at: String(row.created_at),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function rowToMessageAttachment(row: Record<string, unknown>): MessageAttachment {
|
|
855
|
+
return {
|
|
856
|
+
...rowToMessageAttachmentMetadata(row),
|
|
857
|
+
source_ref: row.source_ref === null || row.source_ref === undefined ? null : String(row.source_ref),
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function rowToHeldMessageDraft(row: Record<string, unknown>): HeldMessageDraft {
|
|
862
|
+
return {
|
|
863
|
+
id: String(row.id),
|
|
864
|
+
chat_id: String(row.chat_id),
|
|
865
|
+
chat_name: String(row.chat_name ?? ''),
|
|
866
|
+
chat_display_name: row.chat_display_name === null || row.chat_display_name === undefined ? null : String(row.chat_display_name),
|
|
867
|
+
agent: String(row.agent),
|
|
868
|
+
sender: String(row.sender),
|
|
869
|
+
content: String(row.content),
|
|
870
|
+
mentions: parseMentionsJson(row.mentions_json),
|
|
871
|
+
base_message_id: Number(row.base_message_id),
|
|
872
|
+
latest_message_id_at_hold: Number(row.latest_message_id_at_hold),
|
|
873
|
+
status: String(row.status) as HeldDraftStatus,
|
|
874
|
+
hold_reason: String(row.hold_reason),
|
|
875
|
+
hold_count: Number(row.hold_count),
|
|
876
|
+
intervening_message_count: Number(row.intervening_message_count ?? 0),
|
|
877
|
+
resolved_message_id: row.resolved_message_id === null || row.resolved_message_id === undefined ? null : Number(row.resolved_message_id),
|
|
878
|
+
created_at: String(row.created_at),
|
|
879
|
+
updated_at: String(row.updated_at),
|
|
880
|
+
resolved_at: row.resolved_at === null || row.resolved_at === undefined ? null : String(row.resolved_at),
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function rowToRoomTask(row: Record<string, unknown>): RoomTask {
|
|
885
|
+
return {
|
|
886
|
+
id: String(row.id),
|
|
887
|
+
room_id: String(row.room_id),
|
|
888
|
+
room_name: String(row.room_name ?? ''),
|
|
889
|
+
task_number: Number(row.task_number),
|
|
890
|
+
title: String(row.title),
|
|
891
|
+
status: String(row.status) as RoomTaskStatus,
|
|
892
|
+
assignee: row.assignee === null || row.assignee === undefined ? null : String(row.assignee),
|
|
893
|
+
created_by: row.created_by === null || row.created_by === undefined ? null : String(row.created_by),
|
|
894
|
+
source_message_id: row.source_message_id === null || row.source_message_id === undefined ? null : Number(row.source_message_id),
|
|
895
|
+
created_at: String(row.created_at),
|
|
896
|
+
updated_at: String(row.updated_at),
|
|
897
|
+
completed_at: row.completed_at === null || row.completed_at === undefined ? null : String(row.completed_at),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function rowToRoomReminder(row: Record<string, unknown>): RoomReminder {
|
|
902
|
+
return {
|
|
903
|
+
id: String(row.id),
|
|
904
|
+
room_id: String(row.room_id),
|
|
905
|
+
room_name: String(row.room_name ?? ''),
|
|
906
|
+
source_message_id: Number(row.source_message_id),
|
|
907
|
+
title: String(row.title),
|
|
908
|
+
status: String(row.status) as RoomReminderStatus,
|
|
909
|
+
created_by: row.created_by === null || row.created_by === undefined ? null : String(row.created_by),
|
|
910
|
+
fire_at: String(row.fire_at),
|
|
911
|
+
repeat: row.repeat === null || row.repeat === undefined ? null : String(row.repeat),
|
|
912
|
+
fired_at: row.fired_at === null || row.fired_at === undefined ? null : String(row.fired_at),
|
|
913
|
+
canceled_at: row.canceled_at === null || row.canceled_at === undefined ? null : String(row.canceled_at),
|
|
914
|
+
created_at: String(row.created_at),
|
|
915
|
+
updated_at: String(row.updated_at),
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function rowToRoomSavedMessage(row: Record<string, unknown>): RoomSavedMessage {
|
|
920
|
+
return {
|
|
921
|
+
id: String(row.id),
|
|
922
|
+
room_id: String(row.room_id),
|
|
923
|
+
room_name: String(row.room_name ?? ''),
|
|
924
|
+
message_id: Number(row.message_id),
|
|
925
|
+
message_sender: String(row.message_sender),
|
|
926
|
+
message_content: String(row.message_content),
|
|
927
|
+
message_created_at: String(row.message_created_at),
|
|
928
|
+
saved_by: String(row.saved_by),
|
|
929
|
+
note: row.note === null || row.note === undefined ? null : String(row.note),
|
|
930
|
+
created_at: String(row.created_at),
|
|
931
|
+
updated_at: String(row.updated_at),
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function rowToWorkflowRun(row: Record<string, unknown>): WorkflowRun {
|
|
936
|
+
return {
|
|
937
|
+
id: String(row.id),
|
|
938
|
+
file_path: String(row.file_path),
|
|
939
|
+
goal: row.goal === null || row.goal === undefined ? null : String(row.goal),
|
|
940
|
+
status: String(row.status) as WorkflowRunStatus,
|
|
941
|
+
created_by: row.created_by === null || row.created_by === undefined ? null : String(row.created_by),
|
|
942
|
+
final_output: jsonRecordOrNull(row.final_output_json),
|
|
943
|
+
error: row.error === null || row.error === undefined ? null : String(row.error),
|
|
944
|
+
created_at: String(row.created_at),
|
|
945
|
+
updated_at: String(row.updated_at),
|
|
946
|
+
completed_at: row.completed_at === null || row.completed_at === undefined ? null : String(row.completed_at),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function rowToWorkflowNode(row: Record<string, unknown>): WorkflowNode {
|
|
951
|
+
return {
|
|
952
|
+
id: String(row.id),
|
|
953
|
+
run_id: String(row.run_id),
|
|
954
|
+
parent_id: row.parent_id === null || row.parent_id === undefined ? null : String(row.parent_id),
|
|
955
|
+
kind: String(row.kind) as WorkflowNodeKind,
|
|
956
|
+
title: String(row.title),
|
|
957
|
+
role: row.role === null || row.role === undefined ? null : String(row.role),
|
|
958
|
+
status: String(row.status) as WorkflowNodeStatus,
|
|
959
|
+
context: jsonRecordOrNull(row.context_json),
|
|
960
|
+
instruction: row.instruction === null || row.instruction === undefined ? null : String(row.instruction),
|
|
961
|
+
capabilities: jsonStringArray(row.capabilities_json),
|
|
962
|
+
output_contract: jsonRecordOrNull(row.output_contract_json),
|
|
963
|
+
output: jsonRecordOrNull(row.output_json),
|
|
964
|
+
evidence: jsonRecordOrNull(row.evidence_json),
|
|
965
|
+
task_id: row.task_id === null || row.task_id === undefined ? null : String(row.task_id),
|
|
966
|
+
message_id: row.message_id === null || row.message_id === undefined ? null : Number(row.message_id),
|
|
967
|
+
created_at: String(row.created_at),
|
|
968
|
+
updated_at: String(row.updated_at),
|
|
969
|
+
completed_at: row.completed_at === null || row.completed_at === undefined ? null : String(row.completed_at),
|
|
390
970
|
};
|
|
391
971
|
}
|
|
392
972
|
|
|
@@ -416,6 +996,28 @@ function rowToRun(row: Record<string, unknown>): AgentRun {
|
|
|
416
996
|
};
|
|
417
997
|
}
|
|
418
998
|
|
|
999
|
+
function rowToActivityEvent(row: Record<string, unknown>): AgentActivityEvent {
|
|
1000
|
+
let metadata: Record<string, unknown> = {};
|
|
1001
|
+
try {
|
|
1002
|
+
const parsed = JSON.parse(String(row.metadata ?? '{}')) as unknown;
|
|
1003
|
+
metadata = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
1004
|
+
} catch {
|
|
1005
|
+
metadata = {};
|
|
1006
|
+
}
|
|
1007
|
+
return {
|
|
1008
|
+
id: String(row.id),
|
|
1009
|
+
run_id: String(row.run_id),
|
|
1010
|
+
session_id: row.session_id === null || row.session_id === undefined ? null : String(row.session_id),
|
|
1011
|
+
agent: String(row.agent),
|
|
1012
|
+
room_id: String(row.room_id ?? row.chat_id),
|
|
1013
|
+
kind: String(row.kind) as AgentActivityKind,
|
|
1014
|
+
title: String(row.title),
|
|
1015
|
+
detail: String(row.detail ?? ''),
|
|
1016
|
+
metadata,
|
|
1017
|
+
created_at: String(row.created_at),
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
419
1021
|
function rowToSession(row: Record<string, unknown>): AgentSession {
|
|
420
1022
|
return {
|
|
421
1023
|
id: String(row.id),
|
|
@@ -475,6 +1077,7 @@ function rowToDelivery(row: Record<string, unknown>): MessageDelivery {
|
|
|
475
1077
|
id: String(row.id),
|
|
476
1078
|
message_id: Number(row.message_id),
|
|
477
1079
|
chat_id: String(row.chat_id),
|
|
1080
|
+
...(row.chat_name === undefined ? {} : { chat_name: row.chat_name === null ? null : String(row.chat_name) }),
|
|
478
1081
|
agent: String(row.agent),
|
|
479
1082
|
status: String(row.status) as MessageDelivery['status'],
|
|
480
1083
|
daemon_id: row.daemon_id === null ? null : String(row.daemon_id),
|
|
@@ -618,6 +1221,8 @@ function rowToAgentRoomSubscription(row: Record<string, unknown>): AgentRoomSubs
|
|
|
618
1221
|
room_id: String(row.room_id),
|
|
619
1222
|
channel_id: row.channel_id === null || row.channel_id === undefined ? null : String(row.channel_id),
|
|
620
1223
|
mode: String(row.mode) as AgentRoomSubscriptionMode,
|
|
1224
|
+
last_read_message_id: row.last_read_message_id === null || row.last_read_message_id === undefined ? 0 : Number(row.last_read_message_id),
|
|
1225
|
+
last_read_at: row.last_read_at === null || row.last_read_at === undefined ? null : String(row.last_read_at),
|
|
621
1226
|
created_at: String(row.created_at),
|
|
622
1227
|
updated_at: String(row.updated_at),
|
|
623
1228
|
};
|
|
@@ -721,6 +1326,40 @@ function normalizeMentionsInput(value: string[] | undefined): string[] {
|
|
|
721
1326
|
return result;
|
|
722
1327
|
}
|
|
723
1328
|
|
|
1329
|
+
function normalizeSystemEventActorKind(value?: RoomSystemEventActorKind): RoomSystemEventActorKind {
|
|
1330
|
+
if (value === 'user' || value === 'agent' || value === 'system') return value;
|
|
1331
|
+
return 'system';
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function normalizeSystemEventMetadata(value?: Record<string, unknown> | null): Record<string, unknown> {
|
|
1335
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
1336
|
+
return value;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function systemEventContent(input: {
|
|
1340
|
+
eventType: RoomSystemEventType;
|
|
1341
|
+
actorKind: RoomSystemEventActorKind;
|
|
1342
|
+
actorId: string | null;
|
|
1343
|
+
subjectAgent: string | null;
|
|
1344
|
+
metadata: Record<string, unknown>;
|
|
1345
|
+
}): string {
|
|
1346
|
+
const actor = input.actorId ? ` by ${input.actorKind}:${input.actorId}` : '';
|
|
1347
|
+
const agent = input.subjectAgent ? `@${input.subjectAgent}` : 'An agent';
|
|
1348
|
+
switch (input.eventType) {
|
|
1349
|
+
case 'agent_invited': {
|
|
1350
|
+
const mode = typeof input.metadata.mode === 'string' ? input.metadata.mode : 'mentions';
|
|
1351
|
+
return `System event: ${agent} was invited to this room with receive mode "${mode}"${actor}.`;
|
|
1352
|
+
}
|
|
1353
|
+
case 'agent_removed':
|
|
1354
|
+
return `System event: ${agent} was removed from this room${actor}.`;
|
|
1355
|
+
case 'agent_receive_mode_changed': {
|
|
1356
|
+
const oldMode = typeof input.metadata.old_mode === 'string' ? input.metadata.old_mode : 'unknown';
|
|
1357
|
+
const newMode = typeof input.metadata.new_mode === 'string' ? input.metadata.new_mode : 'unknown';
|
|
1358
|
+
return `System event: ${agent} changed room receive mode from "${oldMode}" to "${newMode}"${actor}.`;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
724
1363
|
function rowToTranscriptReadModel(row: Record<string, unknown>): TranscriptReadModel {
|
|
725
1364
|
return {
|
|
726
1365
|
id: String(row.id),
|
|
@@ -803,8 +1442,10 @@ function limitValue(value: number | undefined, fallback: number): number {
|
|
|
803
1442
|
|
|
804
1443
|
export class MessageStore {
|
|
805
1444
|
readonly db: Database;
|
|
1445
|
+
private readonly palHome: string;
|
|
806
1446
|
|
|
807
|
-
constructor(path: string) {
|
|
1447
|
+
constructor(path: string, options: { palHome?: string } = {}) {
|
|
1448
|
+
this.palHome = options.palHome ?? defaultPalHomeDir();
|
|
808
1449
|
ensureParentDir(path);
|
|
809
1450
|
this.db = new Database(path);
|
|
810
1451
|
this.db.exec('PRAGMA journal_mode = WAL');
|
|
@@ -817,6 +1458,43 @@ export class MessageStore {
|
|
|
817
1458
|
this.db.close();
|
|
818
1459
|
}
|
|
819
1460
|
|
|
1461
|
+
getLarkWebsocketRestartState(appId: string): LarkWebsocketRestartState | null {
|
|
1462
|
+
const key = appId.trim();
|
|
1463
|
+
if (!key) return null;
|
|
1464
|
+
const row = this.db.query(`
|
|
1465
|
+
SELECT app_id, restart_count, last_restart_at, last_restart_reason, updated_at
|
|
1466
|
+
FROM lark_websocket_restart_state
|
|
1467
|
+
WHERE app_id = ?
|
|
1468
|
+
LIMIT 1
|
|
1469
|
+
`).get(key) as LarkWebsocketRestartState | null;
|
|
1470
|
+
if (!row) return null;
|
|
1471
|
+
return {
|
|
1472
|
+
app_id: String(row.app_id),
|
|
1473
|
+
restart_count: Number(row.restart_count),
|
|
1474
|
+
last_restart_at: row.last_restart_at === null ? null : String(row.last_restart_at),
|
|
1475
|
+
last_restart_reason: row.last_restart_reason === null ? null : String(row.last_restart_reason),
|
|
1476
|
+
updated_at: String(row.updated_at),
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
recordLarkWebsocketRestart(input: { appId: string; restartedAt: string; reason: string }): LarkWebsocketRestartState {
|
|
1481
|
+
const appId = input.appId.trim();
|
|
1482
|
+
if (!appId) throw new Error('appId is required');
|
|
1483
|
+
const restartedAt = input.restartedAt.trim();
|
|
1484
|
+
if (!restartedAt) throw new Error('restartedAt is required');
|
|
1485
|
+
const reason = input.reason.trim() || 'unknown';
|
|
1486
|
+
this.db.query(`
|
|
1487
|
+
INSERT INTO lark_websocket_restart_state (app_id, restart_count, last_restart_at, last_restart_reason, updated_at)
|
|
1488
|
+
VALUES (?, 1, ?, ?, datetime('now'))
|
|
1489
|
+
ON CONFLICT(app_id) DO UPDATE SET
|
|
1490
|
+
restart_count = lark_websocket_restart_state.restart_count + 1,
|
|
1491
|
+
last_restart_at = excluded.last_restart_at,
|
|
1492
|
+
last_restart_reason = excluded.last_restart_reason,
|
|
1493
|
+
updated_at = datetime('now')
|
|
1494
|
+
`).run(appId, restartedAt, reason);
|
|
1495
|
+
return this.getLarkWebsocketRestartState(appId)!;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
820
1498
|
getOrCreateChat(name: string, kind: ChatKind = 'group'): Chat {
|
|
821
1499
|
const chatName = normalizeChatName(name);
|
|
822
1500
|
const existing = this.db.query('SELECT * FROM chat_stats WHERE name = ?').get(chatName) as Chat | null;
|
|
@@ -846,6 +1524,49 @@ export class MessageStore {
|
|
|
846
1524
|
return this.getProject(id)!;
|
|
847
1525
|
}
|
|
848
1526
|
|
|
1527
|
+
deleteProject(projectId: string): Project | null {
|
|
1528
|
+
const project = this.getProject(projectId);
|
|
1529
|
+
if (!project) return null;
|
|
1530
|
+
if (project.status !== 'archived') throw new Error('PROJECT_NOT_ARCHIVED: project must be archived before deletion');
|
|
1531
|
+
this.db.transaction(() => {
|
|
1532
|
+
this.db.query('DELETE FROM chats WHERE project_id = ?').run(project.id);
|
|
1533
|
+
this.db.query('DELETE FROM projects WHERE id = ?').run(project.id);
|
|
1534
|
+
})();
|
|
1535
|
+
return project;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
getOrCreateAgentDm(agentKey: string): Chat {
|
|
1539
|
+
const key = agentKey.trim();
|
|
1540
|
+
if (!key) throw new Error('agent is required');
|
|
1541
|
+
const agent = this.getAgent(key);
|
|
1542
|
+
if (!agent) throw new Error(`agent ${key} not found`);
|
|
1543
|
+
const chatName = normalizeChatName(`dm-owner-${key}`);
|
|
1544
|
+
const existing = this.getChatByName(chatName);
|
|
1545
|
+
if (existing && (existing.provider !== 'web' || existing.kind !== 'dm')) {
|
|
1546
|
+
throw new Error(`room ${chatName} already exists and is not a web DM`);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
let room = existing;
|
|
1550
|
+
if (!room) {
|
|
1551
|
+
const id = crypto.randomUUID();
|
|
1552
|
+
this.db.query(`
|
|
1553
|
+
INSERT INTO chats (id, name, display_name, kind, provider, dm_type, capabilities_json)
|
|
1554
|
+
VALUES (?, ?, ?, 'dm', 'web', 'user_agent', '{"topics":"unsupported"}')
|
|
1555
|
+
`).run(id, chatName, agent.display_name);
|
|
1556
|
+
room = this.getChatById(id)!;
|
|
1557
|
+
} else {
|
|
1558
|
+
this.db.query(`
|
|
1559
|
+
UPDATE chats
|
|
1560
|
+
SET display_name = ?, status = 'active'
|
|
1561
|
+
WHERE id = ?
|
|
1562
|
+
`).run(agent.display_name, room.id);
|
|
1563
|
+
room = this.getChatById(room.id)!;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
this.inviteAgentToRoom({ roomId: room.id, agent: key, mode: 'all' });
|
|
1567
|
+
return this.getChatById(room.id)!;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
849
1570
|
getProject(id: string): Project | null {
|
|
850
1571
|
const row = this.db.query(`
|
|
851
1572
|
SELECT p.*, c.name AS computer_name, COUNT(ch.id) AS room_count
|
|
@@ -858,6 +1579,34 @@ export class MessageStore {
|
|
|
858
1579
|
return row ? rowToProject(row) : null;
|
|
859
1580
|
}
|
|
860
1581
|
|
|
1582
|
+
private getWorkbenchProjectForAgent(agentKey: string, assignment: ComputerAgentAssignment | null): Project | null {
|
|
1583
|
+
const roomProject = this.db.query(`
|
|
1584
|
+
SELECT p.*, c.name AS computer_name, COUNT(ch.id) AS room_count
|
|
1585
|
+
FROM room_participants rp
|
|
1586
|
+
JOIN chats agent_room ON agent_room.id = rp.room_id
|
|
1587
|
+
JOIN projects p ON p.id = agent_room.project_id
|
|
1588
|
+
LEFT JOIN computers c ON c.id = p.computer_id
|
|
1589
|
+
LEFT JOIN chats ch ON ch.project_id = p.id
|
|
1590
|
+
WHERE rp.participant_id = ? AND rp.kind = 'agent' AND rp.status = 'active' AND p.status = 'active'
|
|
1591
|
+
GROUP BY p.id
|
|
1592
|
+
ORDER BY datetime(p.updated_at) DESC, p.name ASC
|
|
1593
|
+
LIMIT 1
|
|
1594
|
+
`).get(agentKey.trim()) as Record<string, unknown> | null;
|
|
1595
|
+
if (roomProject) return rowToProject(roomProject);
|
|
1596
|
+
if (!assignment) return null;
|
|
1597
|
+
const assignedProject = this.db.query(`
|
|
1598
|
+
SELECT p.*, c.name AS computer_name, COUNT(ch.id) AS room_count
|
|
1599
|
+
FROM projects p
|
|
1600
|
+
LEFT JOIN computers c ON c.id = p.computer_id
|
|
1601
|
+
LEFT JOIN chats ch ON ch.project_id = p.id
|
|
1602
|
+
WHERE p.computer_id = ? AND p.status = 'active'
|
|
1603
|
+
GROUP BY p.id
|
|
1604
|
+
ORDER BY datetime(p.updated_at) DESC, p.name ASC
|
|
1605
|
+
LIMIT 1
|
|
1606
|
+
`).get(assignment.computer_id) as Record<string, unknown> | null;
|
|
1607
|
+
return assignedProject ? rowToProject(assignedProject) : null;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
861
1610
|
listProjects(limit = 50): Project[] {
|
|
862
1611
|
const rows = this.db.query(`
|
|
863
1612
|
SELECT p.*, c.name AS computer_name, COUNT(ch.id) AS room_count
|
|
@@ -871,21 +1620,330 @@ export class MessageStore {
|
|
|
871
1620
|
return rows.map(rowToProject);
|
|
872
1621
|
}
|
|
873
1622
|
|
|
874
|
-
|
|
1623
|
+
listProjectsForComputer(computerId: string): Project[] {
|
|
1624
|
+
const rows = this.db.query(`
|
|
1625
|
+
SELECT p.*, c.name AS computer_name, COUNT(ch.id) AS room_count
|
|
1626
|
+
FROM projects p
|
|
1627
|
+
LEFT JOIN computers c ON c.id = p.computer_id
|
|
1628
|
+
LEFT JOIN chats ch ON ch.project_id = p.id
|
|
1629
|
+
WHERE p.computer_id = ?
|
|
1630
|
+
GROUP BY p.id
|
|
1631
|
+
ORDER BY p.status ASC, p.updated_at DESC, p.created_at DESC
|
|
1632
|
+
`).all(computerId.trim()) as Record<string, unknown>[];
|
|
1633
|
+
return rows.map(rowToProject);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
updateProjectStatus(projectId: string, status: ProjectStatus): Project {
|
|
1637
|
+
if (status !== 'active' && status !== 'archived') throw new Error('project status must be active or archived');
|
|
1638
|
+
const project = this.getProject(projectId);
|
|
1639
|
+
if (!project) throw new Error(`project ${projectId} was not found`);
|
|
1640
|
+
this.db.query(`UPDATE projects SET status = ?, updated_at = datetime('now') WHERE id = ?`).run(status, project.id);
|
|
1641
|
+
return this.getProject(project.id)!;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
listProjectAgentDefaults(projectId: string): ProjectAgentDefault[] {
|
|
1645
|
+
const project = this.getProject(projectId);
|
|
1646
|
+
if (!project) throw new Error(`project ${projectId} was not found`);
|
|
1647
|
+
const rows = this.db.query(`
|
|
1648
|
+
SELECT pad.*, a.display_name
|
|
1649
|
+
FROM project_agent_defaults pad
|
|
1650
|
+
LEFT JOIN agents a ON a.agent_key = pad.agent
|
|
1651
|
+
WHERE pad.project_id = ?
|
|
1652
|
+
ORDER BY pad.agent ASC
|
|
1653
|
+
`).all(project.id) as Record<string, unknown>[];
|
|
1654
|
+
return rows.map(rowToProjectAgentDefault);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
setProjectAgentDefaults(projectId: string, defaults: Array<{ agent: string; mode: AgentRoomSubscriptionMode }>): ProjectAgentDefault[] {
|
|
1658
|
+
const project = this.getProject(projectId);
|
|
1659
|
+
if (!project) throw new Error(`project ${projectId} was not found`);
|
|
1660
|
+
const normalized = new Map<string, AgentRoomSubscriptionMode>();
|
|
1661
|
+
for (const item of defaults) {
|
|
1662
|
+
const agent = item.agent.trim();
|
|
1663
|
+
if (!agent) continue;
|
|
1664
|
+
if (!this.getAgent(agent)) throw new Error(`agent ${agent} not found`);
|
|
1665
|
+
normalized.set(agent, item.mode);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
this.db.transaction(() => {
|
|
1669
|
+
const agents = [...normalized.keys()];
|
|
1670
|
+
if (agents.length) {
|
|
1671
|
+
const placeholders = agents.map(() => '?').join(', ');
|
|
1672
|
+
this.db.query(`DELETE FROM project_agent_defaults WHERE project_id = ? AND agent NOT IN (${placeholders})`).run(project.id, ...agents);
|
|
1673
|
+
} else {
|
|
1674
|
+
this.db.query(`DELETE FROM project_agent_defaults WHERE project_id = ?`).run(project.id);
|
|
1675
|
+
}
|
|
1676
|
+
for (const [agent, mode] of normalized) {
|
|
1677
|
+
this.db.query(`
|
|
1678
|
+
INSERT INTO project_agent_defaults (id, project_id, agent, mode, created_at, updated_at)
|
|
1679
|
+
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
1680
|
+
ON CONFLICT(project_id, agent) DO UPDATE SET
|
|
1681
|
+
mode = excluded.mode,
|
|
1682
|
+
updated_at = datetime('now')
|
|
1683
|
+
`).run(crypto.randomUUID(), project.id, agent, mode);
|
|
1684
|
+
}
|
|
1685
|
+
this.db.query(`UPDATE projects SET updated_at = datetime('now') WHERE id = ?`).run(project.id);
|
|
1686
|
+
})();
|
|
1687
|
+
|
|
1688
|
+
return this.listProjectAgentDefaults(project.id);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
getSkillDefinition(key: string): SkillDefinition | null {
|
|
1692
|
+
const row = this.db.query('SELECT * FROM skill_definitions WHERE key = ?').get(key.trim()) as Record<string, unknown> | null;
|
|
1693
|
+
return row ? rowToSkillDefinition(row) : null;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
listSkillDefinitions(input: { source?: SkillSource; ownerUserId?: string | null; projectId?: string | null; includeDisabled?: boolean } = {}): SkillDefinition[] {
|
|
1697
|
+
const where: string[] = [];
|
|
1698
|
+
const params: SQLQueryBindings[] = [];
|
|
1699
|
+
if (input.source) {
|
|
1700
|
+
where.push('source = ?');
|
|
1701
|
+
params.push(assertSkillSource(input.source));
|
|
1702
|
+
}
|
|
1703
|
+
if (input.ownerUserId !== undefined) {
|
|
1704
|
+
if (input.ownerUserId === null) {
|
|
1705
|
+
where.push('owner_user_id IS NULL');
|
|
1706
|
+
} else {
|
|
1707
|
+
where.push('owner_user_id = ?');
|
|
1708
|
+
params.push(input.ownerUserId.trim());
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (input.projectId !== undefined) {
|
|
1712
|
+
if (input.projectId === null) {
|
|
1713
|
+
where.push('project_id IS NULL');
|
|
1714
|
+
} else {
|
|
1715
|
+
where.push('project_id = ?');
|
|
1716
|
+
params.push(input.projectId.trim());
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
if (!input.includeDisabled) where.push("status = 'active'");
|
|
1720
|
+
const rows = this.db.query(`
|
|
1721
|
+
SELECT * FROM skill_definitions
|
|
1722
|
+
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
1723
|
+
ORDER BY source ASC, owner_user_id ASC, project_id ASC, key ASC
|
|
1724
|
+
`).all(...params) as Record<string, unknown>[];
|
|
1725
|
+
return rows.map(rowToSkillDefinition);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
upsertSkillDefinition(input: {
|
|
1729
|
+
key: string;
|
|
1730
|
+
name: string;
|
|
1731
|
+
description?: string;
|
|
1732
|
+
instructionContent: string;
|
|
1733
|
+
status?: SkillDefinition['status'];
|
|
1734
|
+
version?: string;
|
|
1735
|
+
source?: SkillSource;
|
|
1736
|
+
ownerUserId?: string | null;
|
|
1737
|
+
projectId?: string | null;
|
|
1738
|
+
repositoryPath?: string | null;
|
|
1739
|
+
}): SkillDefinition {
|
|
1740
|
+
const key = input.key.trim();
|
|
1741
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{1,80}$/.test(key)) throw new Error('INVALID_SKILL_KEY');
|
|
1742
|
+
const name = input.name.trim();
|
|
1743
|
+
if (!name) throw new Error('skill name is required');
|
|
1744
|
+
const instructionContent = input.instructionContent.trim();
|
|
1745
|
+
if (!instructionContent) throw new Error('skill instruction_content is required');
|
|
1746
|
+
const source = assertSkillSource(input.source ?? 'user');
|
|
1747
|
+
const status = assertSkillStatus(input.status ?? 'active');
|
|
1748
|
+
const version = input.version?.trim() || '1';
|
|
1749
|
+
const ownerUserId = input.ownerUserId?.trim() || null;
|
|
1750
|
+
const projectId = input.projectId?.trim() || null;
|
|
1751
|
+
if (source === 'user' && !ownerUserId) throw new Error('owner_user_id is required for user skills');
|
|
1752
|
+
if (source === 'project' && !projectId) throw new Error('project_id is required for project skills');
|
|
1753
|
+
const repositoryPath = input.repositoryPath?.trim() || (source === 'user' ? `user/${ownerUserId}/${key}` : source === 'project' ? `project/${projectId}/${key}` : null);
|
|
1754
|
+
const existing = this.getSkillDefinition(key);
|
|
1755
|
+
if (existing) {
|
|
1756
|
+
if (existing.source !== source) throw new Error('SKILL_SOURCE_IMMUTABLE');
|
|
1757
|
+
if (existing.source === 'system' && (ownerUserId || projectId || repositoryPath)) {
|
|
1758
|
+
throw new Error('SYSTEM_SKILL_OWNERSHIP_IMMUTABLE');
|
|
1759
|
+
}
|
|
1760
|
+
if (existing.source === 'user' && existing.owner_user_id !== ownerUserId) throw new Error('SKILL_OWNER_IMMUTABLE');
|
|
1761
|
+
if (existing.source === 'project' && existing.project_id !== projectId) throw new Error('SKILL_PROJECT_IMMUTABLE');
|
|
1762
|
+
}
|
|
1763
|
+
this.db.query(`
|
|
1764
|
+
INSERT INTO skill_definitions (
|
|
1765
|
+
key, name, description, instruction_content, status, version, source, owner_user_id, project_id, repository_path, created_at, updated_at
|
|
1766
|
+
)
|
|
1767
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
1768
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
1769
|
+
name = excluded.name,
|
|
1770
|
+
description = excluded.description,
|
|
1771
|
+
instruction_content = excluded.instruction_content,
|
|
1772
|
+
status = excluded.status,
|
|
1773
|
+
version = excluded.version,
|
|
1774
|
+
source = excluded.source,
|
|
1775
|
+
owner_user_id = excluded.owner_user_id,
|
|
1776
|
+
project_id = excluded.project_id,
|
|
1777
|
+
repository_path = excluded.repository_path,
|
|
1778
|
+
updated_at = datetime('now')
|
|
1779
|
+
`).run(
|
|
1780
|
+
key,
|
|
1781
|
+
name,
|
|
1782
|
+
input.description?.trim() || '',
|
|
1783
|
+
instructionContent,
|
|
1784
|
+
status,
|
|
1785
|
+
version,
|
|
1786
|
+
source,
|
|
1787
|
+
ownerUserId,
|
|
1788
|
+
projectId,
|
|
1789
|
+
repositoryPath,
|
|
1790
|
+
);
|
|
1791
|
+
const skill = this.getSkillDefinition(key);
|
|
1792
|
+
if (!skill) throw new Error('SKILL_NOT_FOUND');
|
|
1793
|
+
return skill;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
createSkillDefinition(input: {
|
|
1797
|
+
key: string;
|
|
1798
|
+
name: string;
|
|
1799
|
+
description?: string;
|
|
1800
|
+
instructionContent: string;
|
|
1801
|
+
status?: SkillDefinition['status'];
|
|
1802
|
+
source?: SkillSource;
|
|
1803
|
+
ownerUserId?: string | null;
|
|
1804
|
+
projectId?: string | null;
|
|
1805
|
+
repositoryPath?: string | null;
|
|
1806
|
+
}): SkillDefinition {
|
|
1807
|
+
const key = input.key.trim();
|
|
1808
|
+
if (this.getSkillDefinition(key)) throw new Error('SKILL_ALREADY_EXISTS');
|
|
1809
|
+
if ((input.source ?? 'user') === 'system') throw new Error('SYSTEM_SKILL_CREATE_FORBIDDEN');
|
|
1810
|
+
return this.upsertSkillDefinition(input);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
updateSkillDefinition(key: string, input: {
|
|
1814
|
+
name?: string;
|
|
1815
|
+
description?: string;
|
|
1816
|
+
instructionContent?: string;
|
|
1817
|
+
status?: SkillDefinition['status'];
|
|
1818
|
+
ownerUserId?: string | null;
|
|
1819
|
+
projectId?: string | null;
|
|
1820
|
+
repositoryPath?: string | null;
|
|
1821
|
+
}): SkillDefinition {
|
|
1822
|
+
const existing = this.getSkillDefinition(key);
|
|
1823
|
+
if (!existing) throw new Error(`skill ${key} was not found`);
|
|
1824
|
+
if (existing.source === 'system') throw new Error('SYSTEM_SKILL_READ_ONLY');
|
|
1825
|
+
return this.upsertSkillDefinition({
|
|
1826
|
+
key: existing.key,
|
|
1827
|
+
name: input.name ?? existing.name,
|
|
1828
|
+
description: input.description ?? existing.description,
|
|
1829
|
+
instructionContent: input.instructionContent ?? existing.instruction_content,
|
|
1830
|
+
status: input.status ?? existing.status,
|
|
1831
|
+
version: existing.version,
|
|
1832
|
+
source: existing.source,
|
|
1833
|
+
ownerUserId: input.ownerUserId === undefined ? existing.owner_user_id : input.ownerUserId,
|
|
1834
|
+
projectId: input.projectId === undefined ? existing.project_id : input.projectId,
|
|
1835
|
+
repositoryPath: input.repositoryPath === undefined ? existing.repository_path : input.repositoryPath,
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
upsertSkillBinding(input: { scope: SkillBindingScope; scopeId: string; skillKey: string; enabled?: boolean; priority?: number }): SkillBinding {
|
|
1840
|
+
const scope = assertSkillBindingScope(input.scope);
|
|
1841
|
+
const scopeId = input.scopeId.trim();
|
|
1842
|
+
const skillKey = input.skillKey.trim();
|
|
1843
|
+
if (!scopeId) throw new Error('scope_id is required');
|
|
1844
|
+
if (!skillKey) throw new Error('skill_key is required');
|
|
1845
|
+
if (!this.getSkillDefinition(skillKey)) throw new Error(`skill ${skillKey} was not found`);
|
|
1846
|
+
const enabled = input.enabled === false ? 0 : 1;
|
|
1847
|
+
const priority = Number.isInteger(input.priority) ? Number(input.priority) : 100;
|
|
1848
|
+
const id = crypto.randomUUID();
|
|
1849
|
+
this.db.query(`
|
|
1850
|
+
INSERT INTO skill_bindings (id, scope, scope_id, skill_key, enabled, priority, created_at, updated_at)
|
|
1851
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
1852
|
+
ON CONFLICT(scope, scope_id, skill_key) DO UPDATE SET
|
|
1853
|
+
enabled = excluded.enabled,
|
|
1854
|
+
priority = excluded.priority,
|
|
1855
|
+
updated_at = datetime('now')
|
|
1856
|
+
`).run(id, scope, scopeId, skillKey, enabled, priority);
|
|
1857
|
+
const row = this.db.query(`
|
|
1858
|
+
SELECT * FROM skill_bindings
|
|
1859
|
+
WHERE scope = ? AND scope_id = ? AND skill_key = ?
|
|
1860
|
+
`).get(scope, scopeId, skillKey) as Record<string, unknown> | null;
|
|
1861
|
+
if (!row) throw new Error('SKILL_BINDING_NOT_FOUND');
|
|
1862
|
+
return rowToSkillBinding(row);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
listSkillBindings(input: { scope?: SkillBindingScope; scopeId?: string } = {}): SkillBinding[] {
|
|
1866
|
+
const params: SQLQueryBindings[] = [];
|
|
1867
|
+
let where = '';
|
|
1868
|
+
if (input.scope) {
|
|
1869
|
+
where += ' WHERE scope = ?';
|
|
1870
|
+
params.push(assertSkillBindingScope(input.scope));
|
|
1871
|
+
}
|
|
1872
|
+
if (input.scopeId?.trim()) {
|
|
1873
|
+
where += where ? ' AND scope_id = ?' : ' WHERE scope_id = ?';
|
|
1874
|
+
params.push(input.scopeId.trim());
|
|
1875
|
+
}
|
|
1876
|
+
const rows = this.db.query(`
|
|
1877
|
+
SELECT * FROM skill_bindings
|
|
1878
|
+
${where}
|
|
1879
|
+
ORDER BY scope ASC, scope_id ASC, priority ASC, skill_key ASC
|
|
1880
|
+
`).all(...params) as Record<string, unknown>[];
|
|
1881
|
+
return rows.map(rowToSkillBinding);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
deleteSkillBinding(input: { scope: SkillBindingScope; scopeId: string; skillKey: string }): boolean {
|
|
1885
|
+
const scope = assertSkillBindingScope(input.scope);
|
|
1886
|
+
const scopeId = input.scopeId.trim();
|
|
1887
|
+
const skillKey = input.skillKey.trim();
|
|
1888
|
+
if (!scopeId) throw new Error('scope_id is required');
|
|
1889
|
+
if (!skillKey) throw new Error('skill_key is required');
|
|
1890
|
+
const result = this.db.query('DELETE FROM skill_bindings WHERE scope = ? AND scope_id = ? AND skill_key = ?').run(scope, scopeId, skillKey);
|
|
1891
|
+
return result.changes > 0;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
listEnabledSkills(input: { projectId?: string | null; roomId?: string | null; agent?: string | null }): EnabledSkill[] {
|
|
1895
|
+
const orderedScopes: Array<{ scope: SkillBindingScope; scopeId: string; rank: number }> = [];
|
|
1896
|
+
if (input.projectId?.trim()) orderedScopes.push({ scope: 'project', scopeId: input.projectId.trim(), rank: 0 });
|
|
1897
|
+
if (input.roomId?.trim()) orderedScopes.push({ scope: 'room', scopeId: input.roomId.trim(), rank: 1 });
|
|
1898
|
+
if (input.agent?.trim()) orderedScopes.push({ scope: 'agent', scopeId: input.agent.trim(), rank: 2 });
|
|
1899
|
+
if (orderedScopes.length === 0) return [];
|
|
1900
|
+
|
|
1901
|
+
const rows: EnabledSkill[] = [];
|
|
1902
|
+
for (const scope of orderedScopes) {
|
|
1903
|
+
const scopeRows = this.db.query(`
|
|
1904
|
+
SELECT
|
|
1905
|
+
sd.*,
|
|
1906
|
+
sb.scope AS binding_scope,
|
|
1907
|
+
sb.scope_id AS binding_scope_id,
|
|
1908
|
+
sb.priority AS binding_priority
|
|
1909
|
+
FROM skill_bindings sb
|
|
1910
|
+
JOIN skill_definitions sd ON sd.key = sb.skill_key
|
|
1911
|
+
WHERE sb.scope = ?
|
|
1912
|
+
AND sb.scope_id = ?
|
|
1913
|
+
AND sb.enabled = 1
|
|
1914
|
+
AND sd.status = 'active'
|
|
1915
|
+
ORDER BY sb.priority ASC, sd.key ASC
|
|
1916
|
+
`).all(scope.scope, scope.scopeId) as Record<string, unknown>[];
|
|
1917
|
+
rows.push(...scopeRows.map(rowToEnabledSkill));
|
|
1918
|
+
}
|
|
1919
|
+
return rows;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
createProjectRoom(input: { projectId: string; name: string; kind?: ChatKind; mode?: RoomMode }): Chat {
|
|
875
1923
|
const project = this.getProject(input.projectId);
|
|
876
1924
|
if (!project) throw new Error(`project ${input.projectId} was not found`);
|
|
1925
|
+
if (project.status === 'archived') throw new Error('PROJECT_ARCHIVED: archived projects cannot create rooms');
|
|
877
1926
|
const kind = input.kind ?? 'group';
|
|
878
1927
|
if (kind !== 'group' && kind !== 'dm') throw new Error('kind must be group or dm');
|
|
1928
|
+
const mode = assertRoomMode(input.mode ?? 'standard');
|
|
879
1929
|
const chatName = normalizeChatName(input.name);
|
|
880
1930
|
if (!chatName) throw new Error('room name is required');
|
|
881
1931
|
if (this.getChatByName(chatName)) throw new Error(`room ${chatName} already exists`);
|
|
882
1932
|
|
|
883
1933
|
const id = crypto.randomUUID();
|
|
884
|
-
this.db.
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1934
|
+
this.db.transaction(() => {
|
|
1935
|
+
this.db.query(`
|
|
1936
|
+
INSERT INTO chats (id, name, kind, provider, capabilities_json, project_id, mode)
|
|
1937
|
+
VALUES (?, ?, ?, 'web', ?, ?, ?)
|
|
1938
|
+
`).run(id, chatName, kind, kind === 'dm' ? '{"topics":"unsupported"}' : '{"topics":"native"}', project.id, mode);
|
|
1939
|
+
if (mode === 'idea_development') {
|
|
1940
|
+
this.upsertSkillBinding({ scope: 'room', scopeId: id, skillKey: 'idea_development', enabled: true, priority: 100 });
|
|
1941
|
+
}
|
|
1942
|
+
for (const defaultMember of this.listProjectAgentDefaults(project.id)) {
|
|
1943
|
+
this.inviteAgentToRoom({ roomId: id, agent: defaultMember.agent, mode: defaultMember.mode });
|
|
1944
|
+
}
|
|
1945
|
+
this.db.query(`UPDATE projects SET updated_at = datetime('now') WHERE id = ?`).run(project.id);
|
|
1946
|
+
})();
|
|
889
1947
|
return this.getChatById(id)!;
|
|
890
1948
|
}
|
|
891
1949
|
|
|
@@ -897,11 +1955,20 @@ export class MessageStore {
|
|
|
897
1955
|
return this.getChatById(room.id)!;
|
|
898
1956
|
}
|
|
899
1957
|
|
|
1958
|
+
updateRoomStatus(roomId: string, status: ChatStatus): Chat {
|
|
1959
|
+
if (status !== 'active' && status !== 'archived') throw new Error('room status must be active or archived');
|
|
1960
|
+
const room = this.getChatById(roomId);
|
|
1961
|
+
if (!room) throw new Error(`room ${roomId} was not found`);
|
|
1962
|
+
if (room.provider !== 'web') throw new Error('EXTERNAL_ROOM_STATUS_READ_ONLY: external provider rooms cannot be archived from Web');
|
|
1963
|
+
this.db.query('UPDATE chats SET status = ? WHERE id = ?').run(status, room.id);
|
|
1964
|
+
return this.getChatById(room.id)!;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
900
1967
|
backfillLarkDmDisplayNames(): number {
|
|
901
1968
|
const result = this.db.query(`
|
|
902
1969
|
UPDATE chats
|
|
903
1970
|
SET display_name = (
|
|
904
|
-
SELECT
|
|
1971
|
+
SELECT rp.display_name
|
|
905
1972
|
FROM room_participants rp
|
|
906
1973
|
WHERE rp.room_id = chats.id
|
|
907
1974
|
AND rp.kind = 'bot'
|
|
@@ -913,7 +1980,7 @@ export class MessageStore {
|
|
|
913
1980
|
)
|
|
914
1981
|
WHERE provider = 'lark'
|
|
915
1982
|
AND kind = 'dm'
|
|
916
|
-
AND (display_name IS NULL OR trim(display_name) = '' OR display_name LIKE 'lark:%')
|
|
1983
|
+
AND (display_name IS NULL OR trim(display_name) = '' OR display_name LIKE 'lark:%' OR display_name LIKE 'DM with %')
|
|
917
1984
|
AND EXISTS (
|
|
918
1985
|
SELECT 1
|
|
919
1986
|
FROM room_participants rp
|
|
@@ -927,6 +1994,15 @@ export class MessageStore {
|
|
|
927
1994
|
return result.changes;
|
|
928
1995
|
}
|
|
929
1996
|
|
|
1997
|
+
deleteRoom(roomId: string): Chat | null {
|
|
1998
|
+
const room = this.getChatById(roomId);
|
|
1999
|
+
if (!room) return null;
|
|
2000
|
+
if (room.provider !== 'web') throw new Error('EXTERNAL_ROOM_DELETE_READ_ONLY: external provider rooms cannot be deleted from Web');
|
|
2001
|
+
if (room.status !== 'archived') throw new Error('ROOM_NOT_ARCHIVED: room must be archived before deletion');
|
|
2002
|
+
this.db.query('DELETE FROM chats WHERE id = ?').run(room.id);
|
|
2003
|
+
return room;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
930
2006
|
getChatById(id: string): Chat | null {
|
|
931
2007
|
const row = this.db.query('SELECT * FROM chat_stats WHERE id = ?').get(id) as Chat | null;
|
|
932
2008
|
return row ?? null;
|
|
@@ -941,6 +2017,10 @@ export class MessageStore {
|
|
|
941
2017
|
return this.db.query('SELECT * FROM chat_stats ORDER BY COALESCE(last_message_at, created_at) DESC').all() as Chat[];
|
|
942
2018
|
}
|
|
943
2019
|
|
|
2020
|
+
listChatsForAgent(agent: string): Chat[] {
|
|
2021
|
+
return this.listChats().filter((room) => this.canAgentParticipateInRoom(agent.trim(), room));
|
|
2022
|
+
}
|
|
2023
|
+
|
|
944
2024
|
listLarkAuthorizedUsers(): LarkAuthorizedUser[] {
|
|
945
2025
|
const rows = this.db.query('SELECT * FROM lark_authorized_users ORDER BY COALESCE(display_name, user_id), user_id').all() as Record<string, unknown>[];
|
|
946
2026
|
return rows.map(rowToLarkAuthorizedUser);
|
|
@@ -1089,24 +2169,115 @@ export class MessageStore {
|
|
|
1089
2169
|
return Number(row.id ?? 0);
|
|
1090
2170
|
}
|
|
1091
2171
|
|
|
2172
|
+
private systemEventDeliveryAgents(room: Chat, extraAgents: string[] = []): string[] {
|
|
2173
|
+
const candidates = new Set<string>();
|
|
2174
|
+
for (const agent of this.listRoomAgents(room)) {
|
|
2175
|
+
const mode = this.getAgentRoomSubscription(agent, room.id)?.mode ?? 'mentions';
|
|
2176
|
+
if (mode !== 'off' && mode !== 'muted') candidates.add(agent);
|
|
2177
|
+
}
|
|
2178
|
+
for (const agent of extraAgents) {
|
|
2179
|
+
const normalized = agent.trim();
|
|
2180
|
+
if (normalized && this.getAgent(normalized)) candidates.add(normalized);
|
|
2181
|
+
}
|
|
2182
|
+
return [...candidates].sort((left, right) => left.localeCompare(right));
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
recordRoomSystemEvent(input: RecordRoomSystemEventInput): { event: RoomSystemEvent; message: Message; deliveries: MessageDelivery[] } {
|
|
2186
|
+
const room = this.getChatById(input.roomId);
|
|
2187
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
2188
|
+
const actorKind = normalizeSystemEventActorKind(input.actorKind);
|
|
2189
|
+
const actorId = input.actorId?.trim() || null;
|
|
2190
|
+
const subjectAgent = input.subjectAgent?.trim() || null;
|
|
2191
|
+
const metadata = normalizeSystemEventMetadata(input.metadata);
|
|
2192
|
+
const content = input.content?.trim() || systemEventContent({
|
|
2193
|
+
eventType: input.eventType,
|
|
2194
|
+
actorKind,
|
|
2195
|
+
actorId,
|
|
2196
|
+
subjectAgent,
|
|
2197
|
+
metadata,
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
return this.db.transaction(() => {
|
|
2201
|
+
const message = this.createMessage({
|
|
2202
|
+
chatId: room.id,
|
|
2203
|
+
sender: 'system',
|
|
2204
|
+
content,
|
|
2205
|
+
type: 'system',
|
|
2206
|
+
mentions: subjectAgent ? [subjectAgent] : [],
|
|
2207
|
+
});
|
|
2208
|
+
const id = crypto.randomUUID();
|
|
2209
|
+
this.db.query(`
|
|
2210
|
+
INSERT INTO room_system_events (id, room_id, message_id, event_type, actor_kind, actor_id, subject_agent, metadata_json, created_at)
|
|
2211
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
2212
|
+
`).run(id, room.id, message.id, input.eventType, actorKind, actorId, subjectAgent, JSON.stringify(metadata));
|
|
2213
|
+
|
|
2214
|
+
const deliveryAgents = input.deliveryAgents ?? this.systemEventDeliveryAgents(room);
|
|
2215
|
+
const deliveries: MessageDelivery[] = [];
|
|
2216
|
+
for (const agent of normalizeMentionsInput(deliveryAgents)) {
|
|
2217
|
+
if (agent === message.sender) continue;
|
|
2218
|
+
try {
|
|
2219
|
+
deliveries.push(this.createDelivery({ messageId: message.id, agent }));
|
|
2220
|
+
} catch {
|
|
2221
|
+
// Delivery eligibility is best-effort here; the event record and message remain the source of truth.
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
return { event: this.getRoomSystemEvent(id)!, message, deliveries };
|
|
2225
|
+
})();
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
getRoomSystemEvent(id: string): RoomSystemEvent | null {
|
|
2229
|
+
const row = this.db.query('SELECT * FROM room_system_events WHERE id = ?').get(id) as Record<string, unknown> | null;
|
|
2230
|
+
return row ? rowToRoomSystemEvent(row) : null;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
getRoomSystemEventForMessage(messageId: number): RoomSystemEvent | null {
|
|
2234
|
+
const row = this.db.query('SELECT * FROM room_system_events WHERE message_id = ?').get(messageId) as Record<string, unknown> | null;
|
|
2235
|
+
return row ? rowToRoomSystemEvent(row) : null;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
1092
2238
|
inviteAgentToRoom(input: { roomId: string; agent: string; mode?: AgentRoomSubscriptionMode }): { participant: RoomParticipant; subscription: AgentRoomSubscription } {
|
|
1093
2239
|
const room = this.getChatById(input.roomId);
|
|
1094
2240
|
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
1095
2241
|
const agent = input.agent.trim();
|
|
1096
2242
|
if (!agent) throw new Error('agent is required');
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
2243
|
+
const mode = input.mode ?? 'mentions';
|
|
2244
|
+
let result!: { participant: RoomParticipant; subscription: AgentRoomSubscription };
|
|
2245
|
+
this.db.transaction(() => {
|
|
2246
|
+
const existingParticipant = this.getRoomParticipant(room.id, agent);
|
|
2247
|
+
const existingSubscription = this.getAgentRoomSubscription(agent, room.id);
|
|
2248
|
+
const participant = this.upsertRoomParticipant({
|
|
2249
|
+
roomId: room.id,
|
|
2250
|
+
participantId: agent,
|
|
2251
|
+
kind: 'agent',
|
|
2252
|
+
displayName: this.getAgent(agent)?.display_name ?? agent,
|
|
2253
|
+
source: 'local_agent',
|
|
2254
|
+
});
|
|
2255
|
+
this.db.query(`
|
|
2256
|
+
UPDATE room_participants
|
|
2257
|
+
SET delivery_cursor_message_id = COALESCE(delivery_cursor_message_id, ?), updated_at = datetime('now')
|
|
2258
|
+
WHERE room_id = ? AND participant_id = ?
|
|
2259
|
+
`).run(this.getLatestMessageId(room.id), room.id, agent);
|
|
2260
|
+
const subscription = this.upsertAgentRoomSubscription({ agent, roomId: room.id, mode });
|
|
2261
|
+
if (existingParticipant?.status !== 'active') {
|
|
2262
|
+
this.recordRoomSystemEvent({
|
|
2263
|
+
roomId: room.id,
|
|
2264
|
+
eventType: 'agent_invited',
|
|
2265
|
+
subjectAgent: agent,
|
|
2266
|
+
metadata: { mode },
|
|
2267
|
+
deliveryAgents: [],
|
|
2268
|
+
});
|
|
2269
|
+
} else if (existingSubscription && existingSubscription.mode !== mode) {
|
|
2270
|
+
this.recordRoomSystemEvent({
|
|
2271
|
+
roomId: room.id,
|
|
2272
|
+
eventType: 'agent_receive_mode_changed',
|
|
2273
|
+
subjectAgent: agent,
|
|
2274
|
+
metadata: { old_mode: existingSubscription.mode, new_mode: mode },
|
|
2275
|
+
deliveryAgents: this.systemEventDeliveryAgents(room),
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
result = { participant: this.getRoomParticipant(room.id, agent) ?? participant, subscription };
|
|
2279
|
+
})();
|
|
2280
|
+
return result;
|
|
1110
2281
|
}
|
|
1111
2282
|
|
|
1112
2283
|
upsertAgentRoomSubscription(input: { agent: string; roomId: string; mode: AgentRoomSubscriptionMode; channelId?: string | null }): AgentRoomSubscription {
|
|
@@ -1125,10 +2296,11 @@ export class MessageStore {
|
|
|
1125
2296
|
return this.getAgentRoomSubscription(agent, room.id)!;
|
|
1126
2297
|
}
|
|
1127
2298
|
const id = crypto.randomUUID();
|
|
2299
|
+
const latestMessageId = this.getLatestMessageId(room.id);
|
|
1128
2300
|
this.db.query(`
|
|
1129
|
-
INSERT INTO agent_room_subscriptions (id, agent, room_id, channel_id, mode, created_at, updated_at)
|
|
1130
|
-
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
1131
|
-
`).run(id, agent, room.id, channelId, input.mode);
|
|
2301
|
+
INSERT INTO agent_room_subscriptions (id, agent, room_id, channel_id, mode, last_read_message_id, created_at, updated_at)
|
|
2302
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
2303
|
+
`).run(id, agent, room.id, channelId, input.mode, latestMessageId);
|
|
1132
2304
|
const row = this.db.query('SELECT * FROM agent_room_subscriptions WHERE id = ?').get(id) as Record<string, unknown>;
|
|
1133
2305
|
return rowToAgentRoomSubscription(row);
|
|
1134
2306
|
}
|
|
@@ -1142,6 +2314,208 @@ export class MessageStore {
|
|
|
1142
2314
|
return row ? rowToAgentRoomSubscription(row) : null;
|
|
1143
2315
|
}
|
|
1144
2316
|
|
|
2317
|
+
listAgentRoomSubscriptions(roomId: string): AgentRoomSubscription[] {
|
|
2318
|
+
const rows = this.db.query(`
|
|
2319
|
+
SELECT *
|
|
2320
|
+
FROM agent_room_subscriptions
|
|
2321
|
+
WHERE room_id = ? AND channel_id IS NULL
|
|
2322
|
+
ORDER BY agent ASC
|
|
2323
|
+
`).all(roomId) as Record<string, unknown>[];
|
|
2324
|
+
return rows.map(rowToAgentRoomSubscription);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
private unreadCountForAgentRoom(agent: string, roomId: string, lastReadMessageId: number): number {
|
|
2328
|
+
const row = this.db.query(`
|
|
2329
|
+
SELECT COUNT(*) AS count
|
|
2330
|
+
FROM messages
|
|
2331
|
+
WHERE chat_id = ? AND id > ? AND sender != ?
|
|
2332
|
+
`).get(roomId, lastReadMessageId, agent.trim()) as { count: number };
|
|
2333
|
+
return Number(row.count ?? 0);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
private taskCountsForRoom(roomId: string): { task_count: number; open_task_count: number } {
|
|
2337
|
+
const row = this.db.query(`
|
|
2338
|
+
SELECT
|
|
2339
|
+
COUNT(*) AS task_count,
|
|
2340
|
+
SUM(CASE WHEN status NOT IN ('done', 'closed') THEN 1 ELSE 0 END) AS open_task_count
|
|
2341
|
+
FROM room_tasks
|
|
2342
|
+
WHERE room_id = ?
|
|
2343
|
+
`).get(roomId) as { task_count: number; open_task_count: number | null };
|
|
2344
|
+
return {
|
|
2345
|
+
task_count: Number(row.task_count ?? 0),
|
|
2346
|
+
open_task_count: Number(row.open_task_count ?? 0),
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
private scheduledReminderCountForAgentRoom(agent: string, roomId: string): number {
|
|
2351
|
+
const row = this.db.query(`
|
|
2352
|
+
SELECT COUNT(*) AS count
|
|
2353
|
+
FROM room_reminders
|
|
2354
|
+
WHERE room_id = ? AND created_by = ? AND status = 'scheduled'
|
|
2355
|
+
`).get(roomId, agent.trim()) as { count: number };
|
|
2356
|
+
return Number(row.count ?? 0);
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
private savedMessageCountForAgentRoom(agent: string, roomId: string): number {
|
|
2360
|
+
const row = this.db.query(`
|
|
2361
|
+
SELECT COUNT(*) AS count
|
|
2362
|
+
FROM room_saved_messages
|
|
2363
|
+
WHERE room_id = ? AND saved_by = ?
|
|
2364
|
+
`).get(roomId, agent.trim()) as { count: number };
|
|
2365
|
+
return Number(row.count ?? 0);
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
private buildAgentWorkbenchRoom(agentKey: string, row: Record<string, unknown>): AgentWorkbenchRoom {
|
|
2369
|
+
const key = agentKey.trim();
|
|
2370
|
+
const room = row as unknown as Chat;
|
|
2371
|
+
const participant = this.getRoomParticipant(room.id, key);
|
|
2372
|
+
const subscription = this.getAgentRoomSubscription(key, room.id);
|
|
2373
|
+
const latestMessageId = this.getLatestMessageId(room.id);
|
|
2374
|
+
const lastReadMessageId = subscription?.last_read_message_id ?? participant?.delivery_cursor_message_id ?? 0;
|
|
2375
|
+
const heldDraftCount = this.db.query(`
|
|
2376
|
+
SELECT COUNT(*) AS count
|
|
2377
|
+
FROM held_message_drafts
|
|
2378
|
+
WHERE agent = ? AND chat_id = ? AND status = 'held'
|
|
2379
|
+
`).get(key, room.id) as { count: number };
|
|
2380
|
+
const taskCounts = this.taskCountsForRoom(room.id);
|
|
2381
|
+
return {
|
|
2382
|
+
...room,
|
|
2383
|
+
participant,
|
|
2384
|
+
subscription,
|
|
2385
|
+
held_draft_count: Number(heldDraftCount.count ?? 0),
|
|
2386
|
+
latest_message_id: latestMessageId,
|
|
2387
|
+
last_read_message_id: lastReadMessageId,
|
|
2388
|
+
last_read_at: subscription?.last_read_at ?? null,
|
|
2389
|
+
unread_count: this.unreadCountForAgentRoom(key, room.id, lastReadMessageId),
|
|
2390
|
+
is_followed: subscription ? !['muted', 'off'].includes(subscription.mode) : false,
|
|
2391
|
+
task_count: taskCounts.task_count,
|
|
2392
|
+
open_task_count: taskCounts.open_task_count,
|
|
2393
|
+
scheduled_reminder_count: this.scheduledReminderCountForAgentRoom(key, room.id),
|
|
2394
|
+
saved_message_count: this.savedMessageCountForAgentRoom(key, room.id),
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
getAgentWorkbenchRoom(agentKey: string, roomId: string): AgentWorkbenchRoom | null {
|
|
2399
|
+
const key = agentKey.trim();
|
|
2400
|
+
if (!key) throw new Error('agent is required');
|
|
2401
|
+
const row = this.db.query('SELECT * FROM chat_stats WHERE id = ?').get(roomId) as Record<string, unknown> | null;
|
|
2402
|
+
return row ? this.buildAgentWorkbenchRoom(key, row) : null;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
listAgentWorkbenchRooms(agentKey: string): AgentWorkbenchRoom[] {
|
|
2406
|
+
const key = agentKey.trim();
|
|
2407
|
+
const rows = this.db.query(`
|
|
2408
|
+
SELECT c.*
|
|
2409
|
+
FROM room_participants rp
|
|
2410
|
+
JOIN chat_stats c ON c.id = rp.room_id
|
|
2411
|
+
WHERE rp.participant_id = ? AND rp.kind = 'agent' AND rp.status = 'active'
|
|
2412
|
+
ORDER BY COALESCE(c.display_name, c.name) ASC
|
|
2413
|
+
`).all(key) as Record<string, unknown>[];
|
|
2414
|
+
return rows.map((row) => this.buildAgentWorkbenchRoom(key, row));
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
updateAgentRoomSubscriptionMode(input: UpdateAgentRoomSubscriptionInput): { subscription: AgentRoomSubscription; room: AgentWorkbenchRoom } {
|
|
2418
|
+
const agent = input.agent.trim();
|
|
2419
|
+
if (!agent) throw new Error('agent is required');
|
|
2420
|
+
const room = this.getChatById(input.roomId);
|
|
2421
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
2422
|
+
if (!this.getAgent(agent)) throw new Error(`agent ${agent} not found`);
|
|
2423
|
+
if (!this.canAgentParticipateInRoom(agent, room)) throw new Error('ROOM_ACCESS_DENIED');
|
|
2424
|
+
const existing = this.getAgentRoomSubscription(agent, room.id);
|
|
2425
|
+
const subscription = this.upsertAgentRoomSubscription({ agent, roomId: room.id, mode: input.mode });
|
|
2426
|
+
if ((existing?.mode ?? null) !== subscription.mode) {
|
|
2427
|
+
this.recordRoomSystemEvent({
|
|
2428
|
+
roomId: room.id,
|
|
2429
|
+
eventType: 'agent_receive_mode_changed',
|
|
2430
|
+
actorKind: input.actorKind,
|
|
2431
|
+
actorId: input.actorId,
|
|
2432
|
+
subjectAgent: agent,
|
|
2433
|
+
metadata: { old_mode: existing?.mode ?? null, new_mode: subscription.mode },
|
|
2434
|
+
deliveryAgents: this.systemEventDeliveryAgents(room),
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
return { subscription, room: this.getAgentWorkbenchRoom(agent, room.id)! };
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
markAgentRoomRead(input: MarkAgentRoomReadInput): AgentWorkbenchRoom {
|
|
2441
|
+
const agent = input.agent.trim();
|
|
2442
|
+
if (!agent) throw new Error('agent is required');
|
|
2443
|
+
const room = this.getChatById(input.roomId);
|
|
2444
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
2445
|
+
if (!this.getAgent(agent)) throw new Error(`agent ${agent} not found`);
|
|
2446
|
+
if (!this.canAgentParticipateInRoom(agent, room)) throw new Error('ROOM_ACCESS_DENIED');
|
|
2447
|
+
|
|
2448
|
+
const existing = this.getAgentRoomSubscription(agent, room.id)
|
|
2449
|
+
?? this.upsertAgentRoomSubscription({ agent, roomId: room.id, mode: 'mentions' });
|
|
2450
|
+
const latestMessageId = this.getLatestMessageId(room.id);
|
|
2451
|
+
const requestedMessageId = input.messageId === undefined ? latestMessageId : Number(input.messageId);
|
|
2452
|
+
if (!Number.isInteger(requestedMessageId) || requestedMessageId < 0) {
|
|
2453
|
+
throw new Error('INVALID_READ_MESSAGE_ID');
|
|
2454
|
+
}
|
|
2455
|
+
const nextReadMessageId = Math.max(existing.last_read_message_id, Math.min(requestedMessageId, latestMessageId));
|
|
2456
|
+
this.db.query(`
|
|
2457
|
+
UPDATE agent_room_subscriptions
|
|
2458
|
+
SET last_read_message_id = ?, last_read_at = datetime('now'), updated_at = datetime('now')
|
|
2459
|
+
WHERE id = ?
|
|
2460
|
+
`).run(nextReadMessageId, existing.id);
|
|
2461
|
+
return this.getAgentWorkbenchRoom(agent, room.id)!;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
leaveAgentRoom(input: { roomId: string; agent: string }): { room: Chat; agent: string; participant_removed: boolean; subscriptions_removed: number; active_deliveries_canceled: number } {
|
|
2465
|
+
const room = this.getChatById(input.roomId);
|
|
2466
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
2467
|
+
const agent = input.agent.trim();
|
|
2468
|
+
if (!agent) throw new Error('agent is required');
|
|
2469
|
+
if (!this.getAgent(agent)) throw new Error(`agent ${agent} not found`);
|
|
2470
|
+
|
|
2471
|
+
let participantRemoved = false;
|
|
2472
|
+
let subscriptionsRemoved = 0;
|
|
2473
|
+
let activeDeliveriesCanceled = 0;
|
|
2474
|
+
let removalMessageId = 0;
|
|
2475
|
+
|
|
2476
|
+
this.db.transaction(() => {
|
|
2477
|
+
const existingParticipant = this.getRoomParticipant(room.id, agent);
|
|
2478
|
+
if (existingParticipant?.status === 'active') {
|
|
2479
|
+
const eventResult = this.recordRoomSystemEvent({
|
|
2480
|
+
roomId: room.id,
|
|
2481
|
+
eventType: 'agent_removed',
|
|
2482
|
+
subjectAgent: agent,
|
|
2483
|
+
deliveryAgents: this.systemEventDeliveryAgents(room, [agent]),
|
|
2484
|
+
});
|
|
2485
|
+
removalMessageId = eventResult.message.id;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
const participantResult = this.db.query(`
|
|
2489
|
+
UPDATE room_participants
|
|
2490
|
+
SET status = 'removed', updated_at = datetime('now')
|
|
2491
|
+
WHERE room_id = ? AND participant_id = ? AND kind = 'agent' AND status = 'active'
|
|
2492
|
+
`).run(room.id, agent);
|
|
2493
|
+
participantRemoved = participantResult.changes > 0;
|
|
2494
|
+
|
|
2495
|
+
const subscriptionResult = this.db.query(`
|
|
2496
|
+
DELETE FROM agent_room_subscriptions
|
|
2497
|
+
WHERE room_id = ? AND agent = ?
|
|
2498
|
+
`).run(room.id, agent);
|
|
2499
|
+
subscriptionsRemoved = subscriptionResult.changes;
|
|
2500
|
+
|
|
2501
|
+
const deliveryResult = this.db.query(`
|
|
2502
|
+
UPDATE message_deliveries
|
|
2503
|
+
SET status = 'canceled', last_error = CASE WHEN last_error = '' THEN 'agent left room' ELSE last_error END
|
|
2504
|
+
WHERE chat_id = ? AND agent = ? AND status IN ('pending', 'claimed', 'processing_completed')
|
|
2505
|
+
AND message_id != ?
|
|
2506
|
+
`).run(room.id, agent, removalMessageId);
|
|
2507
|
+
activeDeliveriesCanceled = deliveryResult.changes;
|
|
2508
|
+
})();
|
|
2509
|
+
|
|
2510
|
+
return {
|
|
2511
|
+
room,
|
|
2512
|
+
agent,
|
|
2513
|
+
participant_removed: participantRemoved,
|
|
2514
|
+
subscriptions_removed: subscriptionsRemoved,
|
|
2515
|
+
active_deliveries_canceled: activeDeliveriesCanceled,
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
|
|
1145
2519
|
createRoomChannel(input: { roomId: string; name: string; createdBy?: string | null; externalRef?: string | null }): RoomChannel {
|
|
1146
2520
|
const room = this.getChatById(input.roomId);
|
|
1147
2521
|
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
@@ -1162,7 +2536,7 @@ export class MessageStore {
|
|
|
1162
2536
|
|
|
1163
2537
|
listAgents(limit = 50): AgentDefinition[] {
|
|
1164
2538
|
const rows = this.db.query(`
|
|
1165
|
-
SELECT id, agent_key, display_name, description, runtime, created_at, updated_at
|
|
2539
|
+
SELECT id, agent_key, display_name, description, runtime, model, avatar, created_at, updated_at
|
|
1166
2540
|
FROM agents
|
|
1167
2541
|
ORDER BY agent_key ASC
|
|
1168
2542
|
LIMIT ?
|
|
@@ -1174,7 +2548,7 @@ export class MessageStore {
|
|
|
1174
2548
|
const key = identifier.trim();
|
|
1175
2549
|
if (!key) return null;
|
|
1176
2550
|
const row = this.db.query(`
|
|
1177
|
-
SELECT id, agent_key, display_name, description, runtime, created_at, updated_at
|
|
2551
|
+
SELECT id, agent_key, display_name, description, runtime, model, avatar, created_at, updated_at
|
|
1178
2552
|
FROM agents
|
|
1179
2553
|
WHERE id = ? OR agent_key = ?
|
|
1180
2554
|
LIMIT 1
|
|
@@ -1182,46 +2556,323 @@ export class MessageStore {
|
|
|
1182
2556
|
return row ? rowToAgent(row) : null;
|
|
1183
2557
|
}
|
|
1184
2558
|
|
|
1185
|
-
getAgentRuntime(agentKey: string): string | null {
|
|
1186
|
-
const row = this.db.query(`
|
|
1187
|
-
SELECT runtime FROM agents WHERE agent_key = ? LIMIT 1
|
|
1188
|
-
`).get(agentKey.trim()) as { runtime: string | null } | null;
|
|
1189
|
-
return row?.runtime ?? null;
|
|
2559
|
+
getAgentRuntime(agentKey: string): string | null {
|
|
2560
|
+
const row = this.db.query(`
|
|
2561
|
+
SELECT runtime FROM agents WHERE agent_key = ? LIMIT 1
|
|
2562
|
+
`).get(agentKey.trim()) as { runtime: string | null } | null;
|
|
2563
|
+
return row?.runtime ?? null;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
getAgentPermissionProfile(agentKey: string): AgentPermissionProfile {
|
|
2567
|
+
const key = agentKey.trim();
|
|
2568
|
+
if (!key) throw new Error('agent_key is required');
|
|
2569
|
+
if (!this.getAgent(key)) throw new Error(`agent ${key} not found`);
|
|
2570
|
+
const row = this.db.query(`
|
|
2571
|
+
SELECT * FROM agent_permission_profiles WHERE agent_key = ? LIMIT 1
|
|
2572
|
+
`).get(key) as Record<string, unknown> | null;
|
|
2573
|
+
if (row) return rowToAgentPermissionProfile(row);
|
|
2574
|
+
const now = new Date().toISOString();
|
|
2575
|
+
return {
|
|
2576
|
+
agent_key: key,
|
|
2577
|
+
filesystem_mode: defaultAgentPermissionProfile.filesystemMode,
|
|
2578
|
+
extra_writable_roots: [...defaultAgentPermissionProfile.extraWritableRoots],
|
|
2579
|
+
created_at: now,
|
|
2580
|
+
updated_at: now,
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
upsertAgentPermissionProfile(agentKey: string, input: RuntimeAgentPermissionProfile): AgentPermissionProfile {
|
|
2585
|
+
const key = agentKey.trim();
|
|
2586
|
+
if (!key) throw new Error('agent_key is required');
|
|
2587
|
+
if (!this.getAgent(key)) throw new Error(`agent ${key} not found`);
|
|
2588
|
+
const validation = validateAgentPermissionProfile(input);
|
|
2589
|
+
if (!validation.ok) {
|
|
2590
|
+
throw new Error(validation.diagnostics.filter((diagnostic) => diagnostic.level === 'error').map((diagnostic) => diagnostic.message).join('; '));
|
|
2591
|
+
}
|
|
2592
|
+
this.db.query(`
|
|
2593
|
+
INSERT INTO agent_permission_profiles (agent_key, filesystem_mode, extra_writable_roots_json, created_at, updated_at)
|
|
2594
|
+
VALUES (?, ?, ?, datetime('now'), datetime('now'))
|
|
2595
|
+
ON CONFLICT(agent_key) DO UPDATE SET
|
|
2596
|
+
filesystem_mode = excluded.filesystem_mode,
|
|
2597
|
+
extra_writable_roots_json = excluded.extra_writable_roots_json,
|
|
2598
|
+
updated_at = datetime('now')
|
|
2599
|
+
`).run(key, validation.profile.filesystemMode, JSON.stringify(validation.profile.extraWritableRoots));
|
|
2600
|
+
return this.getAgentPermissionProfile(key);
|
|
1190
2601
|
}
|
|
1191
2602
|
|
|
1192
|
-
upsertAgent(input: { id?: string; agent_key: string; display_name: string; description?: string | null; runtime?: string | null }): AgentDefinition {
|
|
2603
|
+
upsertAgent(input: { id?: string; agent_key: string; display_name: string; description?: string | null; runtime?: string | null; model?: string | null; avatar?: string | null }): AgentDefinition {
|
|
1193
2604
|
const id = input.id?.trim() || crypto.randomUUID();
|
|
1194
2605
|
const agentKey = input.agent_key.trim();
|
|
1195
2606
|
const displayName = input.display_name.trim();
|
|
1196
2607
|
if (!agentKey) throw new Error('agent_key is required');
|
|
1197
2608
|
if (!displayName) throw new Error('display_name is required');
|
|
1198
2609
|
|
|
2610
|
+
const avatar = normalizeAgentAvatar(input.avatar, agentKey, displayName);
|
|
2611
|
+
|
|
1199
2612
|
this.db.query(`
|
|
1200
|
-
INSERT INTO agents (id, agent_key, display_name, description, runtime, created_at, updated_at)
|
|
1201
|
-
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
2613
|
+
INSERT INTO agents (id, agent_key, display_name, description, runtime, model, avatar, created_at, updated_at)
|
|
2614
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
1202
2615
|
ON CONFLICT(agent_key) DO UPDATE SET
|
|
1203
2616
|
display_name = excluded.display_name,
|
|
1204
2617
|
description = COALESCE(excluded.description, agents.description),
|
|
1205
2618
|
runtime = COALESCE(excluded.runtime, agents.runtime),
|
|
2619
|
+
model = COALESCE(excluded.model, agents.model),
|
|
2620
|
+
avatar = COALESCE(NULLIF(agents.avatar, ''), excluded.avatar),
|
|
1206
2621
|
updated_at = datetime('now')
|
|
1207
|
-
`).run(id, agentKey, displayName, input.description ?? null, input.runtime ?? null);
|
|
2622
|
+
`).run(id, agentKey, displayName, input.description ?? null, input.runtime ?? null, input.model ?? null, avatar);
|
|
1208
2623
|
|
|
1209
2624
|
return this.getAgent(agentKey)!;
|
|
1210
2625
|
}
|
|
1211
2626
|
|
|
1212
2627
|
updateAgentRuntime(agentKey: string, runtime: string | null): AgentDefinition | null {
|
|
2628
|
+
return this.updateAgentRuntimeConfig(agentKey, { runtime });
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
updateAgentRuntimeConfig(agentKey: string, input: { runtime?: string | null; model?: string | null }): AgentDefinition | null {
|
|
1213
2632
|
const key = agentKey.trim();
|
|
1214
2633
|
if (!key) throw new Error('agent_key is required');
|
|
1215
2634
|
const existing = this.getAgent(key);
|
|
1216
2635
|
if (!existing) return null;
|
|
1217
2636
|
|
|
2637
|
+
const runtime = Object.prototype.hasOwnProperty.call(input, 'runtime') ? input.runtime ?? null : existing.runtime;
|
|
2638
|
+
const model = Object.prototype.hasOwnProperty.call(input, 'model') ? input.model ?? null : existing.model;
|
|
1218
2639
|
this.db.query(`
|
|
1219
|
-
UPDATE agents SET runtime = ?, updated_at = datetime('now') WHERE agent_key = ?
|
|
1220
|
-
`).run(runtime, key);
|
|
2640
|
+
UPDATE agents SET runtime = ?, model = ?, updated_at = datetime('now') WHERE agent_key = ?
|
|
2641
|
+
`).run(runtime, model, key);
|
|
1221
2642
|
|
|
1222
2643
|
return this.getAgent(key);
|
|
1223
2644
|
}
|
|
1224
2645
|
|
|
2646
|
+
listAgentWorkbenchLarkBindings(agentKey: string): AgentWorkbenchLarkBinding[] {
|
|
2647
|
+
const key = agentKey.trim();
|
|
2648
|
+
const rows = this.db.query(`
|
|
2649
|
+
SELECT 'provider_account' AS source,
|
|
2650
|
+
external_account_id AS app_id,
|
|
2651
|
+
NULL AS label,
|
|
2652
|
+
bot_external_user_id AS bot_open_id,
|
|
2653
|
+
status
|
|
2654
|
+
FROM provider_accounts
|
|
2655
|
+
WHERE provider = 'lark' AND agent = ?
|
|
2656
|
+
UNION ALL
|
|
2657
|
+
SELECT 'channel_account' AS source,
|
|
2658
|
+
app_id,
|
|
2659
|
+
name AS label,
|
|
2660
|
+
bot_open_id,
|
|
2661
|
+
status
|
|
2662
|
+
FROM channel_accounts
|
|
2663
|
+
WHERE channel = 'lark' AND agent = ?
|
|
2664
|
+
ORDER BY app_id ASC, source ASC
|
|
2665
|
+
`).all(key, key) as Array<Record<string, unknown>>;
|
|
2666
|
+
return rows.map((row): AgentWorkbenchLarkBinding => ({
|
|
2667
|
+
source: String(row.source) as AgentWorkbenchLarkBinding['source'],
|
|
2668
|
+
app_id: String(row.app_id),
|
|
2669
|
+
label: row.label === null || row.label === undefined ? null : String(row.label),
|
|
2670
|
+
bot_open_id: row.bot_open_id === null || row.bot_open_id === undefined ? null : String(row.bot_open_id),
|
|
2671
|
+
status: String(row.status) as AgentWorkbenchLarkBinding['status'],
|
|
2672
|
+
}));
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
getAgentWorkbenchCollaboration(agentKey: string, limit = 20): AgentWorkbenchCollaboration {
|
|
2676
|
+
const key = agentKey.trim();
|
|
2677
|
+
if (!key) throw new Error('agent_key is required');
|
|
2678
|
+
const cappedLimit = limitValue(limit, 20);
|
|
2679
|
+
const taskRows = this.db.query(`
|
|
2680
|
+
SELECT rt.*, c.name AS room_name
|
|
2681
|
+
FROM room_tasks rt
|
|
2682
|
+
JOIN chats c ON c.id = rt.room_id
|
|
2683
|
+
JOIN room_participants rp ON rp.room_id = rt.room_id
|
|
2684
|
+
WHERE rp.participant_id = ?
|
|
2685
|
+
AND rp.kind = 'agent'
|
|
2686
|
+
AND rp.status = 'active'
|
|
2687
|
+
AND rt.status NOT IN ('done', 'closed')
|
|
2688
|
+
AND (rt.assignee = ? OR rt.created_by = ?)
|
|
2689
|
+
ORDER BY CASE WHEN rt.assignee = ? THEN 0 ELSE 1 END, rt.updated_at DESC, rt.task_number ASC
|
|
2690
|
+
LIMIT ?
|
|
2691
|
+
`).all(key, key, key, key, cappedLimit) as Record<string, unknown>[];
|
|
2692
|
+
const reminderRows = this.db.query(`
|
|
2693
|
+
SELECT rr.*, c.name AS room_name
|
|
2694
|
+
FROM room_reminders rr
|
|
2695
|
+
JOIN chats c ON c.id = rr.room_id
|
|
2696
|
+
JOIN room_participants rp ON rp.room_id = rr.room_id
|
|
2697
|
+
WHERE rp.participant_id = ?
|
|
2698
|
+
AND rp.kind = 'agent'
|
|
2699
|
+
AND rp.status = 'active'
|
|
2700
|
+
AND rr.created_by = ?
|
|
2701
|
+
AND rr.status = 'scheduled'
|
|
2702
|
+
ORDER BY rr.fire_at ASC, rr.created_at ASC
|
|
2703
|
+
LIMIT ?
|
|
2704
|
+
`).all(key, key, cappedLimit) as Record<string, unknown>[];
|
|
2705
|
+
const savedRows = this.db.query(`
|
|
2706
|
+
SELECT
|
|
2707
|
+
rsm.*,
|
|
2708
|
+
c.name AS room_name,
|
|
2709
|
+
m.sender AS message_sender,
|
|
2710
|
+
m.content AS message_content,
|
|
2711
|
+
m.created_at AS message_created_at
|
|
2712
|
+
FROM room_saved_messages rsm
|
|
2713
|
+
JOIN chats c ON c.id = rsm.room_id
|
|
2714
|
+
JOIN messages m ON m.id = rsm.message_id
|
|
2715
|
+
JOIN room_participants rp ON rp.room_id = rsm.room_id
|
|
2716
|
+
WHERE rp.participant_id = ?
|
|
2717
|
+
AND rp.kind = 'agent'
|
|
2718
|
+
AND rp.status = 'active'
|
|
2719
|
+
AND rsm.saved_by = ?
|
|
2720
|
+
ORDER BY rsm.updated_at DESC, rsm.created_at DESC
|
|
2721
|
+
LIMIT ?
|
|
2722
|
+
`).all(key, key, cappedLimit) as Record<string, unknown>[];
|
|
2723
|
+
|
|
2724
|
+
return {
|
|
2725
|
+
tasks: taskRows.map(rowToRoomTask),
|
|
2726
|
+
reminders: reminderRows.map(rowToRoomReminder),
|
|
2727
|
+
saved_messages: savedRows.map(rowToRoomSavedMessage),
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
getAgentWorkbench(agentKey: string): AgentWorkbench | null {
|
|
2732
|
+
const key = agentKey.trim();
|
|
2733
|
+
if (!key) throw new Error('agent_key is required');
|
|
2734
|
+
const agent = this.getAgent(key);
|
|
2735
|
+
if (!agent) return null;
|
|
2736
|
+
const assignment = this.getComputerAgentAssignment(agent.agent_key);
|
|
2737
|
+
return {
|
|
2738
|
+
agent,
|
|
2739
|
+
permissions: this.getAgentPermissionProfile(agent.agent_key),
|
|
2740
|
+
assignment,
|
|
2741
|
+
machine: assignment ? this.getComputer(assignment.computer_id) : null,
|
|
2742
|
+
project: this.getWorkbenchProjectForAgent(agent.agent_key, assignment),
|
|
2743
|
+
rooms: this.listAgentWorkbenchRooms(agent.agent_key),
|
|
2744
|
+
inbox: this.listInbox(agent.agent_key, 0, 20),
|
|
2745
|
+
deliveries: this.listRecentDeliveriesForAgent(agent.agent_key, 20),
|
|
2746
|
+
runs: this.listRunsForAgent(agent.agent_key, 10),
|
|
2747
|
+
activity: this.listActivityForAgent(agent.agent_key, 80),
|
|
2748
|
+
sessions: this.listSessionsForAgent(agent.agent_key, 10),
|
|
2749
|
+
held_drafts: this.listHeldMessageDrafts(agent.agent_key, 'held', 20),
|
|
2750
|
+
collaboration: this.getAgentWorkbenchCollaboration(agent.agent_key),
|
|
2751
|
+
lark_bindings: this.listAgentWorkbenchLarkBindings(agent.agent_key),
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
getAgentDeletionImpact(agentKey: string): AgentDeletionImpact | null {
|
|
2756
|
+
const key = agentKey.trim();
|
|
2757
|
+
if (!key) throw new Error('agent_key is required');
|
|
2758
|
+
const agent = this.getAgent(key);
|
|
2759
|
+
if (!agent) return null;
|
|
2760
|
+
const rooms = this.db.query(`
|
|
2761
|
+
SELECT c.id, c.name, c.display_name, c.kind, c.provider, c.status
|
|
2762
|
+
FROM room_participants rp
|
|
2763
|
+
INNER JOIN chat_stats c ON c.id = rp.room_id
|
|
2764
|
+
WHERE rp.participant_id = ? AND rp.kind = 'agent' AND rp.status = 'active'
|
|
2765
|
+
ORDER BY COALESCE(c.display_name, c.name) ASC
|
|
2766
|
+
`).all(key) as Array<Record<string, unknown>>;
|
|
2767
|
+
const providerBindings = this.db.query(`
|
|
2768
|
+
SELECT 'provider_account' AS source, external_account_id AS app_id, NULL AS label
|
|
2769
|
+
FROM provider_accounts
|
|
2770
|
+
WHERE provider = 'lark' AND agent = ? AND status = 'active'
|
|
2771
|
+
UNION ALL
|
|
2772
|
+
SELECT 'channel_account' AS source, app_id, name AS label
|
|
2773
|
+
FROM channel_accounts
|
|
2774
|
+
WHERE channel = 'lark' AND agent = ? AND status = 'active'
|
|
2775
|
+
ORDER BY app_id ASC, source ASC
|
|
2776
|
+
`).all(key, key) as Array<Record<string, unknown>>;
|
|
2777
|
+
return {
|
|
2778
|
+
agent,
|
|
2779
|
+
rooms: rooms.map((row): AgentDeletionRoomImpact => ({
|
|
2780
|
+
id: String(row.id),
|
|
2781
|
+
name: String(row.name),
|
|
2782
|
+
display_name: row.display_name === null || row.display_name === undefined ? null : String(row.display_name),
|
|
2783
|
+
kind: String(row.kind) as ChatKind,
|
|
2784
|
+
provider: String(row.provider) as RoomProvider,
|
|
2785
|
+
status: String(row.status) as ChatStatus,
|
|
2786
|
+
})),
|
|
2787
|
+
lark_bindings: providerBindings.map((row): AgentDeletionLarkBindingImpact => ({
|
|
2788
|
+
source: String(row.source) as AgentDeletionLarkBindingImpact['source'],
|
|
2789
|
+
app_id: String(row.app_id),
|
|
2790
|
+
label: row.label === null || row.label === undefined ? null : String(row.label),
|
|
2791
|
+
})),
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
deleteAgent(agentKey: string): AgentDeletionResult | null {
|
|
2796
|
+
const key = agentKey.trim();
|
|
2797
|
+
if (!key) throw new Error('agent_key is required');
|
|
2798
|
+
const existing = this.getAgent(key);
|
|
2799
|
+
if (!existing) return null;
|
|
2800
|
+
|
|
2801
|
+
const result: AgentDeletionResult = {
|
|
2802
|
+
agent: existing,
|
|
2803
|
+
rooms_left: 0,
|
|
2804
|
+
subscriptions_removed: 0,
|
|
2805
|
+
assignments_removed: 0,
|
|
2806
|
+
daemon_registrations_removed: 0,
|
|
2807
|
+
provider_accounts_disabled: 0,
|
|
2808
|
+
channel_accounts_disabled: 0,
|
|
2809
|
+
active_deliveries_canceled: 0,
|
|
2810
|
+
running_runs_marked_kill: 0,
|
|
2811
|
+
};
|
|
2812
|
+
|
|
2813
|
+
this.db.transaction(() => {
|
|
2814
|
+
const roomsResult = this.db.query(`
|
|
2815
|
+
UPDATE room_participants
|
|
2816
|
+
SET status = 'removed', updated_at = datetime('now')
|
|
2817
|
+
WHERE participant_id = ? AND kind = 'agent' AND status = 'active'
|
|
2818
|
+
`).run(key);
|
|
2819
|
+
result.rooms_left = roomsResult.changes;
|
|
2820
|
+
|
|
2821
|
+
const subscriptionsResult = this.db.query(`
|
|
2822
|
+
DELETE FROM agent_room_subscriptions
|
|
2823
|
+
WHERE agent = ?
|
|
2824
|
+
`).run(key);
|
|
2825
|
+
result.subscriptions_removed = subscriptionsResult.changes;
|
|
2826
|
+
|
|
2827
|
+
const assignmentsResult = this.db.query(`
|
|
2828
|
+
DELETE FROM computer_agent_assignments
|
|
2829
|
+
WHERE agent = ?
|
|
2830
|
+
`).run(key);
|
|
2831
|
+
result.assignments_removed = assignmentsResult.changes;
|
|
2832
|
+
|
|
2833
|
+
const daemonResult = this.db.query(`
|
|
2834
|
+
DELETE FROM daemon_agents
|
|
2835
|
+
WHERE agent = ?
|
|
2836
|
+
`).run(key);
|
|
2837
|
+
result.daemon_registrations_removed = daemonResult.changes;
|
|
2838
|
+
|
|
2839
|
+
const providerResult = this.db.query(`
|
|
2840
|
+
UPDATE provider_accounts
|
|
2841
|
+
SET status = 'disabled', updated_at = datetime('now')
|
|
2842
|
+
WHERE agent = ? AND status = 'active'
|
|
2843
|
+
`).run(key);
|
|
2844
|
+
result.provider_accounts_disabled = providerResult.changes;
|
|
2845
|
+
|
|
2846
|
+
const channelResult = this.db.query(`
|
|
2847
|
+
UPDATE channel_accounts
|
|
2848
|
+
SET status = 'disabled', updated_at = datetime('now')
|
|
2849
|
+
WHERE agent = ? AND status = 'active'
|
|
2850
|
+
`).run(key);
|
|
2851
|
+
result.channel_accounts_disabled = channelResult.changes;
|
|
2852
|
+
|
|
2853
|
+
const deliveryResult = this.db.query(`
|
|
2854
|
+
UPDATE message_deliveries
|
|
2855
|
+
SET status = 'canceled', last_error = CASE WHEN last_error = '' THEN 'agent deleted' ELSE last_error END
|
|
2856
|
+
WHERE agent = ? AND status IN ('pending', 'claimed', 'processing_completed')
|
|
2857
|
+
`).run(key);
|
|
2858
|
+
result.active_deliveries_canceled = deliveryResult.changes;
|
|
2859
|
+
|
|
2860
|
+
const runResult = this.db.query(`
|
|
2861
|
+
UPDATE agent_runs
|
|
2862
|
+
SET action = 'kill', updated_at = datetime('now')
|
|
2863
|
+
WHERE agent = ? AND status = 'running'
|
|
2864
|
+
`).run(key);
|
|
2865
|
+
result.running_runs_marked_kill = runResult.changes;
|
|
2866
|
+
|
|
2867
|
+
this.db.query(`
|
|
2868
|
+
DELETE FROM agents
|
|
2869
|
+
WHERE agent_key = ?
|
|
2870
|
+
`).run(key);
|
|
2871
|
+
})();
|
|
2872
|
+
|
|
2873
|
+
return result;
|
|
2874
|
+
}
|
|
2875
|
+
|
|
1225
2876
|
assignAgentToComputer(input: { agent: string; computerId: string }): ComputerAgentAssignment {
|
|
1226
2877
|
const agent = input.agent.trim();
|
|
1227
2878
|
const computerId = input.computerId.trim();
|
|
@@ -1243,9 +2894,19 @@ export class MessageStore {
|
|
|
1243
2894
|
return this.getComputerAgentAssignment(agent)!;
|
|
1244
2895
|
}
|
|
1245
2896
|
|
|
2897
|
+
unassignAgentFromComputer(agent: string): boolean {
|
|
2898
|
+
const key = agent.trim();
|
|
2899
|
+
if (!key) throw new Error('agent is required');
|
|
2900
|
+
const result = this.db.query(`
|
|
2901
|
+
DELETE FROM computer_agent_assignments
|
|
2902
|
+
WHERE agent = ?
|
|
2903
|
+
`).run(key);
|
|
2904
|
+
return result.changes > 0;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
1246
2907
|
getComputerAgentAssignment(agent: string): ComputerAgentAssignment | null {
|
|
1247
2908
|
const row = this.db.query(`
|
|
1248
|
-
SELECT caa.*, a.display_name, a.runtime
|
|
2909
|
+
SELECT caa.*, a.display_name, a.runtime, a.model
|
|
1249
2910
|
FROM computer_agent_assignments caa
|
|
1250
2911
|
JOIN agents a ON a.agent_key = caa.agent
|
|
1251
2912
|
WHERE caa.agent = ?
|
|
@@ -1256,7 +2917,7 @@ export class MessageStore {
|
|
|
1256
2917
|
|
|
1257
2918
|
listComputerAgentAssignments(computerId: string): ComputerAgentAssignment[] {
|
|
1258
2919
|
const rows = this.db.query(`
|
|
1259
|
-
SELECT caa.*, a.display_name, a.runtime
|
|
2920
|
+
SELECT caa.*, a.display_name, a.runtime, a.model
|
|
1260
2921
|
FROM computer_agent_assignments caa
|
|
1261
2922
|
JOIN agents a ON a.agent_key = caa.agent
|
|
1262
2923
|
WHERE caa.computer_id = ? AND caa.status = 'active'
|
|
@@ -1297,6 +2958,10 @@ export class MessageStore {
|
|
|
1297
2958
|
diagnostics.push({ level: 'error', code: 'INVALID_FIELD', message: 'runtime must be a string or null' });
|
|
1298
2959
|
}
|
|
1299
2960
|
|
|
2961
|
+
if ('model' in input && input.model !== null && typeof input.model !== 'string') {
|
|
2962
|
+
diagnostics.push({ level: 'error', code: 'INVALID_FIELD', message: 'model must be a string or null' });
|
|
2963
|
+
}
|
|
2964
|
+
|
|
1300
2965
|
for (const key of ['created_at', 'updated_at']) {
|
|
1301
2966
|
if (key in input && typeof input[key] !== 'string') {
|
|
1302
2967
|
diagnostics.push({ level: 'error', code: 'INVALID_FIELD', message: `${key} must be a string when provided` });
|
|
@@ -2105,9 +3770,13 @@ export class MessageStore {
|
|
|
2105
3770
|
|
|
2106
3771
|
createMessage(input: CreateMessageInput): Message {
|
|
2107
3772
|
const sender = input.sender.trim();
|
|
2108
|
-
const
|
|
3773
|
+
const attachments = input.attachments ?? [];
|
|
3774
|
+
const content = input.content.trim() || (attachments.length > 0 ? (attachments.some((attachment) => attachment.kind === 'image') ? '[image]' : '[file]') : '');
|
|
2109
3775
|
if (!sender) throw new Error('sender is required');
|
|
2110
3776
|
if (!content) throw new Error('content is required');
|
|
3777
|
+
for (const attachment of attachments) {
|
|
3778
|
+
validateMessageAttachment({ kind: attachment.kind, mimeType: attachment.mimeType, content: attachment.content });
|
|
3779
|
+
}
|
|
2111
3780
|
|
|
2112
3781
|
let chat: Chat | null = null;
|
|
2113
3782
|
let parentId: number | null = null;
|
|
@@ -2138,7 +3807,7 @@ export class MessageStore {
|
|
|
2138
3807
|
JOIN chats c ON c.id = m.chat_id
|
|
2139
3808
|
WHERE m.idempotency_key = ?
|
|
2140
3809
|
`).get(idempotencyKey) as Record<string, unknown> | null;
|
|
2141
|
-
if (existing) return rowToMessage(existing)
|
|
3810
|
+
if (existing) return this.attachMessageAttachments([rowToMessage(existing)])[0]!;
|
|
2142
3811
|
}
|
|
2143
3812
|
|
|
2144
3813
|
const mentions = normalizeMentionsInput(input.mentions);
|
|
@@ -2148,80 +3817,864 @@ export class MessageStore {
|
|
|
2148
3817
|
if (!channel) throw new Error(`channel ${channelId} was not found`);
|
|
2149
3818
|
if (channel.room_id !== chat.id) throw new Error(`channel ${channelId} does not belong to room ${chat.id}`);
|
|
2150
3819
|
}
|
|
2151
|
-
const provider = input.provider ?? chat.provider ?? 'web';
|
|
2152
|
-
const
|
|
2153
|
-
|
|
2154
|
-
|
|
3820
|
+
const provider = input.provider ?? chat.provider ?? 'web';
|
|
3821
|
+
const id = this.db.transaction(() => {
|
|
3822
|
+
const result = this.db.query(`
|
|
3823
|
+
INSERT INTO messages (chat_id, parent_id, depth, sender, recipient, content, type, idempotency_key, channel_id, provider, mentions_json)
|
|
3824
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3825
|
+
`).run(
|
|
3826
|
+
chat.id,
|
|
3827
|
+
parentId,
|
|
3828
|
+
depth,
|
|
3829
|
+
sender,
|
|
3830
|
+
input.recipient?.trim() || null,
|
|
3831
|
+
content,
|
|
3832
|
+
input.type ?? 'message',
|
|
3833
|
+
idempotencyKey,
|
|
3834
|
+
channelId,
|
|
3835
|
+
provider,
|
|
3836
|
+
JSON.stringify(mentions),
|
|
3837
|
+
);
|
|
3838
|
+
const messageId = Number(result.lastInsertRowid);
|
|
3839
|
+
for (const attachment of attachments) {
|
|
3840
|
+
const attachmentId = crypto.randomUUID();
|
|
3841
|
+
const stored = storeMessageAttachmentFile({
|
|
3842
|
+
palHome: this.palHome,
|
|
3843
|
+
roomId: chat.id,
|
|
3844
|
+
messageId,
|
|
3845
|
+
attachmentId,
|
|
3846
|
+
filename: attachment.filename,
|
|
3847
|
+
mimeType: attachment.mimeType,
|
|
3848
|
+
content: attachment.content,
|
|
3849
|
+
});
|
|
3850
|
+
this.db.query(`
|
|
3851
|
+
INSERT INTO message_attachments (id, message_id, kind, mime_type, filename, size_bytes, path, source_provider, source_ref)
|
|
3852
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3853
|
+
`).run(
|
|
3854
|
+
attachmentId,
|
|
3855
|
+
messageId,
|
|
3856
|
+
attachment.kind,
|
|
3857
|
+
attachment.mimeType,
|
|
3858
|
+
stored.filename,
|
|
3859
|
+
attachment.content.length,
|
|
3860
|
+
stored.path,
|
|
3861
|
+
attachment.sourceProvider ?? provider,
|
|
3862
|
+
attachment.sourceRef?.trim() || null,
|
|
3863
|
+
);
|
|
3864
|
+
}
|
|
3865
|
+
return messageId;
|
|
3866
|
+
})();
|
|
3867
|
+
return this.getMessage(id)!;
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
private attachMessageAttachments(messages: Message[]): Message[] {
|
|
3871
|
+
if (messages.length === 0) return messages;
|
|
3872
|
+
const ids = messages.map((message) => message.id);
|
|
3873
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
3874
|
+
const rows = this.db.query(`
|
|
3875
|
+
SELECT id, message_id, kind, mime_type, filename, size_bytes, path, source_provider, created_at
|
|
3876
|
+
FROM message_attachments
|
|
3877
|
+
WHERE message_id IN (${placeholders})
|
|
3878
|
+
ORDER BY message_id ASC, id ASC
|
|
3879
|
+
`).all(...ids) as Record<string, unknown>[];
|
|
3880
|
+
const byMessage = new Map<number, MessageAttachmentMetadata[]>();
|
|
3881
|
+
for (const row of rows) {
|
|
3882
|
+
const attachment = rowToMessageAttachmentMetadata(row);
|
|
3883
|
+
const list = byMessage.get(attachment.message_id) ?? [];
|
|
3884
|
+
list.push(attachment);
|
|
3885
|
+
byMessage.set(attachment.message_id, list);
|
|
3886
|
+
}
|
|
3887
|
+
return messages.map((message) => ({ ...message, attachments: byMessage.get(message.id) ?? [] }));
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
listMessageAttachments(messageId: number): MessageAttachmentMetadata[] {
|
|
3891
|
+
const rows = this.db.query(`
|
|
3892
|
+
SELECT id, message_id, kind, mime_type, filename, size_bytes, path, source_provider, created_at
|
|
3893
|
+
FROM message_attachments
|
|
3894
|
+
WHERE message_id = ?
|
|
3895
|
+
ORDER BY id ASC
|
|
3896
|
+
`).all(messageId) as Record<string, unknown>[];
|
|
3897
|
+
return rows.map(rowToMessageAttachmentMetadata);
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
addMessageAttachment(messageId: number, attachment: CreateMessageAttachmentInput): MessageAttachmentMetadata {
|
|
3901
|
+
const message = this.getMessage(messageId);
|
|
3902
|
+
if (!message) throw new Error(`message ${messageId} was not found`);
|
|
3903
|
+
validateMessageAttachment({ kind: attachment.kind, mimeType: attachment.mimeType, content: attachment.content });
|
|
3904
|
+
const id = crypto.randomUUID();
|
|
3905
|
+
const stored = storeMessageAttachmentFile({
|
|
3906
|
+
palHome: this.palHome,
|
|
3907
|
+
roomId: message.chat_id,
|
|
3908
|
+
messageId,
|
|
3909
|
+
attachmentId: id,
|
|
3910
|
+
filename: attachment.filename,
|
|
3911
|
+
mimeType: attachment.mimeType,
|
|
3912
|
+
content: attachment.content,
|
|
3913
|
+
});
|
|
3914
|
+
this.db.query(`
|
|
3915
|
+
INSERT INTO message_attachments (id, message_id, kind, mime_type, filename, size_bytes, path, source_provider, source_ref)
|
|
3916
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3917
|
+
`).run(
|
|
3918
|
+
id,
|
|
3919
|
+
messageId,
|
|
3920
|
+
attachment.kind,
|
|
3921
|
+
attachment.mimeType,
|
|
3922
|
+
stored.filename,
|
|
3923
|
+
attachment.content.length,
|
|
3924
|
+
stored.path,
|
|
3925
|
+
attachment.sourceProvider ?? message.provider ?? null,
|
|
3926
|
+
attachment.sourceRef?.trim() || null,
|
|
3927
|
+
);
|
|
3928
|
+
return this.listMessageAttachments(messageId).find((item) => item.id === id)!;
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
getMessageAttachment(id: string): MessageAttachment | null {
|
|
3932
|
+
const row = this.db.query('SELECT * FROM message_attachments WHERE id = ?').get(id.trim()) as Record<string, unknown> | null;
|
|
3933
|
+
return row ? rowToMessageAttachment(row) : null;
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
getMessage(id: number): Message | null {
|
|
3937
|
+
const row = this.db.query(`
|
|
3938
|
+
SELECT m.*, c.name AS chat_name
|
|
3939
|
+
FROM messages m
|
|
3940
|
+
JOIN chats c ON c.id = m.chat_id
|
|
3941
|
+
WHERE m.id = ?
|
|
3942
|
+
`).get(id) as Record<string, unknown> | null;
|
|
3943
|
+
return row ? this.attachMessageAttachments([rowToMessage(row)])[0]! : null;
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
listMessages(input: ListMessagesInput = {}): Message[] {
|
|
3947
|
+
const conditions: string[] = [];
|
|
3948
|
+
const params: SQLQueryBindings[] = [];
|
|
3949
|
+
|
|
3950
|
+
if (input.chatId) {
|
|
3951
|
+
conditions.push('m.chat_id = ?');
|
|
3952
|
+
params.push(input.chatId);
|
|
3953
|
+
} else if (input.chatName) {
|
|
3954
|
+
conditions.push('c.name = ?');
|
|
3955
|
+
params.push(normalizeChatName(input.chatName));
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
if (input.parentId !== undefined) {
|
|
3959
|
+
if (input.parentId === null) {
|
|
3960
|
+
conditions.push('m.parent_id IS NULL');
|
|
3961
|
+
} else {
|
|
3962
|
+
conditions.push('(m.id = ? OR m.parent_id = ?)');
|
|
3963
|
+
params.push(input.parentId, input.parentId);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
if (input.after !== undefined) {
|
|
3968
|
+
conditions.push('m.id > ?');
|
|
3969
|
+
params.push(input.after);
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
if (input.before !== undefined) {
|
|
3973
|
+
conditions.push('m.id < ?');
|
|
3974
|
+
params.push(input.before);
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
if (input.q) {
|
|
3978
|
+
conditions.push('m.content LIKE ?');
|
|
3979
|
+
params.push(`%${input.q}%`);
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
3983
|
+
const limit = limitValue(input.limit, 50);
|
|
3984
|
+
const useLatestWindow = input.after === undefined && input.q === undefined;
|
|
3985
|
+
const rows = useLatestWindow
|
|
3986
|
+
? this.db.query(`
|
|
3987
|
+
SELECT *
|
|
3988
|
+
FROM (
|
|
3989
|
+
SELECT m.*, c.name AS chat_name
|
|
3990
|
+
FROM messages m
|
|
3991
|
+
JOIN chats c ON c.id = m.chat_id
|
|
3992
|
+
${where}
|
|
3993
|
+
ORDER BY m.id DESC
|
|
3994
|
+
LIMIT ?
|
|
3995
|
+
)
|
|
3996
|
+
ORDER BY id ASC
|
|
3997
|
+
`).all(...params, limit) as Record<string, unknown>[]
|
|
3998
|
+
: this.db.query(`
|
|
3999
|
+
SELECT m.*, c.name AS chat_name
|
|
4000
|
+
FROM messages m
|
|
4001
|
+
JOIN chats c ON c.id = m.chat_id
|
|
4002
|
+
${where}
|
|
4003
|
+
ORDER BY m.id ASC
|
|
4004
|
+
LIMIT ?
|
|
4005
|
+
`).all(...params, limit) as Record<string, unknown>[];
|
|
4006
|
+
|
|
4007
|
+
return this.attachMessageAttachments(rows.map(rowToMessage));
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
getLatestMessageIdForFreshness(chatId: string, sender: string): number {
|
|
4011
|
+
const row = this.db.query(`
|
|
4012
|
+
SELECT COALESCE(MAX(id), 0) AS latest
|
|
4013
|
+
FROM messages
|
|
4014
|
+
WHERE chat_id = ? AND sender != ?
|
|
4015
|
+
`).get(chatId, sender.trim()) as { latest: number } | null;
|
|
4016
|
+
return Number(row?.latest ?? 0);
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
listInterveningMessagesForFreshness(input: { chatId: string; baseMessageId: number; sender: string; limit?: number }): Message[] {
|
|
4020
|
+
const rows = this.db.query(`
|
|
4021
|
+
SELECT m.*, c.name AS chat_name
|
|
4022
|
+
FROM messages m
|
|
4023
|
+
JOIN chats c ON c.id = m.chat_id
|
|
4024
|
+
WHERE m.chat_id = ?
|
|
4025
|
+
AND m.id > ?
|
|
4026
|
+
AND m.sender != ?
|
|
4027
|
+
ORDER BY m.id ASC
|
|
4028
|
+
LIMIT ?
|
|
4029
|
+
`).all(input.chatId, input.baseMessageId, input.sender.trim(), limitValue(input.limit, 50)) as Record<string, unknown>[];
|
|
4030
|
+
return this.attachMessageAttachments(rows.map(rowToMessage));
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
listRoomTasks(roomId: string, status: RoomTaskStatus | 'all' = 'all', limit = 50): RoomTask[] {
|
|
4034
|
+
const room = this.getChatById(roomId);
|
|
4035
|
+
if (!room) throw new Error(`room ${roomId} was not found`);
|
|
4036
|
+
const conditions = ['rt.room_id = ?'];
|
|
4037
|
+
const params: SQLQueryBindings[] = [room.id];
|
|
4038
|
+
if (status !== 'all') {
|
|
4039
|
+
conditions.push('rt.status = ?');
|
|
4040
|
+
params.push(assertRoomTaskStatus(status));
|
|
4041
|
+
}
|
|
4042
|
+
const rows = this.db.query(`
|
|
4043
|
+
SELECT rt.*, c.name AS room_name
|
|
4044
|
+
FROM room_tasks rt
|
|
4045
|
+
JOIN chats c ON c.id = rt.room_id
|
|
4046
|
+
WHERE ${conditions.join(' AND ')}
|
|
4047
|
+
ORDER BY rt.task_number ASC
|
|
4048
|
+
LIMIT ?
|
|
4049
|
+
`).all(...params, limitValue(limit, 50)) as Record<string, unknown>[];
|
|
4050
|
+
return rows.map(rowToRoomTask);
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
getRoomTask(roomId: string, taskNumber: number): RoomTask | null {
|
|
4054
|
+
const row = this.db.query(`
|
|
4055
|
+
SELECT rt.*, c.name AS room_name
|
|
4056
|
+
FROM room_tasks rt
|
|
4057
|
+
JOIN chats c ON c.id = rt.room_id
|
|
4058
|
+
WHERE rt.room_id = ? AND rt.task_number = ?
|
|
4059
|
+
`).get(roomId, taskNumber) as Record<string, unknown> | null;
|
|
4060
|
+
return row ? rowToRoomTask(row) : null;
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
createRoomTasks(input: CreateRoomTasksInput): RoomTask[] {
|
|
4064
|
+
const room = this.getChatById(input.roomId);
|
|
4065
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
4066
|
+
const titles = input.titles.map((title) => title.trim()).filter(Boolean);
|
|
4067
|
+
if (!titles.length) throw new Error('task title is required');
|
|
4068
|
+
|
|
4069
|
+
const sourceMessageId = input.sourceMessageId === undefined ? null : input.sourceMessageId;
|
|
4070
|
+
if (sourceMessageId !== null) {
|
|
4071
|
+
if (!Number.isInteger(sourceMessageId) || sourceMessageId < 1) throw new Error('INVALID_SOURCE_MESSAGE_ID');
|
|
4072
|
+
const sourceMessage = this.getMessage(sourceMessageId);
|
|
4073
|
+
if (!sourceMessage || sourceMessage.chat_id !== room.id) throw new Error('SOURCE_MESSAGE_NOT_FOUND');
|
|
4074
|
+
}
|
|
4075
|
+
const createdBy = input.createdBy?.trim() || null;
|
|
4076
|
+
|
|
4077
|
+
return this.db.transaction(() => {
|
|
4078
|
+
const nextRow = this.db.query(`
|
|
4079
|
+
SELECT COALESCE(MAX(task_number), 0) + 1 AS task_number
|
|
4080
|
+
FROM room_tasks
|
|
4081
|
+
WHERE room_id = ?
|
|
4082
|
+
`).get(room.id) as { task_number: number };
|
|
4083
|
+
let taskNumber = Number(nextRow.task_number ?? 1);
|
|
4084
|
+
const created: RoomTask[] = [];
|
|
4085
|
+
for (const title of titles) {
|
|
4086
|
+
const id = crypto.randomUUID();
|
|
4087
|
+
this.db.query(`
|
|
4088
|
+
INSERT INTO room_tasks (id, room_id, task_number, title, status, assignee, created_by, source_message_id, created_at, updated_at)
|
|
4089
|
+
VALUES (?, ?, ?, ?, 'todo', NULL, ?, ?, datetime('now'), datetime('now'))
|
|
4090
|
+
`).run(id, room.id, taskNumber, title, createdBy, sourceMessageId);
|
|
4091
|
+
created.push(this.getRoomTask(room.id, taskNumber)!);
|
|
4092
|
+
taskNumber += 1;
|
|
4093
|
+
}
|
|
4094
|
+
return created;
|
|
4095
|
+
})();
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
claimRoomTask(input: ClaimRoomTaskInput): RoomTask | null {
|
|
4099
|
+
const room = this.getChatById(input.roomId);
|
|
4100
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
4101
|
+
const assignee = input.assignee.trim();
|
|
4102
|
+
if (!assignee) throw new Error('assignee is required');
|
|
4103
|
+
if (!Number.isInteger(input.taskNumber) || input.taskNumber < 1) throw new Error('INVALID_TASK_NUMBER');
|
|
4104
|
+
const existing = this.getRoomTask(room.id, input.taskNumber);
|
|
4105
|
+
if (!existing) return null;
|
|
4106
|
+
this.db.query(`
|
|
4107
|
+
UPDATE room_tasks
|
|
4108
|
+
SET assignee = ?, status = 'in_progress', completed_at = NULL, updated_at = datetime('now')
|
|
4109
|
+
WHERE room_id = ? AND task_number = ?
|
|
4110
|
+
`).run(assignee, room.id, input.taskNumber);
|
|
4111
|
+
return this.getRoomTask(room.id, input.taskNumber);
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
unclaimRoomTask(roomId: string, taskNumber: number): RoomTask | null {
|
|
4115
|
+
const room = this.getChatById(roomId);
|
|
4116
|
+
if (!room) throw new Error(`room ${roomId} was not found`);
|
|
4117
|
+
if (!Number.isInteger(taskNumber) || taskNumber < 1) throw new Error('INVALID_TASK_NUMBER');
|
|
4118
|
+
const existing = this.getRoomTask(room.id, taskNumber);
|
|
4119
|
+
if (!existing) return null;
|
|
4120
|
+
this.db.query(`
|
|
4121
|
+
UPDATE room_tasks
|
|
4122
|
+
SET assignee = NULL, status = 'todo', completed_at = NULL, updated_at = datetime('now')
|
|
4123
|
+
WHERE room_id = ? AND task_number = ?
|
|
4124
|
+
`).run(room.id, taskNumber);
|
|
4125
|
+
return this.getRoomTask(room.id, taskNumber);
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
updateRoomTaskStatus(input: UpdateRoomTaskInput): RoomTask | null {
|
|
4129
|
+
const room = this.getChatById(input.roomId);
|
|
4130
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
4131
|
+
if (!Number.isInteger(input.taskNumber) || input.taskNumber < 1) throw new Error('INVALID_TASK_NUMBER');
|
|
4132
|
+
const status = assertRoomTaskStatus(input.status);
|
|
4133
|
+
const existing = this.getRoomTask(room.id, input.taskNumber);
|
|
4134
|
+
if (!existing) return null;
|
|
4135
|
+
const completedAtSql = status === 'done' || status === 'closed'
|
|
4136
|
+
? "COALESCE(completed_at, datetime('now'))"
|
|
4137
|
+
: 'NULL';
|
|
4138
|
+
this.db.query(`
|
|
4139
|
+
UPDATE room_tasks
|
|
4140
|
+
SET status = ?, completed_at = ${completedAtSql}, updated_at = datetime('now')
|
|
4141
|
+
WHERE room_id = ? AND task_number = ?
|
|
4142
|
+
`).run(status, room.id, input.taskNumber);
|
|
4143
|
+
return this.getRoomTask(room.id, input.taskNumber);
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
getRoomReminder(id: string): RoomReminder | null {
|
|
4147
|
+
const row = this.db.query(`
|
|
4148
|
+
SELECT rr.*, c.name AS room_name
|
|
4149
|
+
FROM room_reminders rr
|
|
4150
|
+
JOIN chats c ON c.id = rr.room_id
|
|
4151
|
+
WHERE rr.id = ?
|
|
4152
|
+
`).get(id) as Record<string, unknown> | null;
|
|
4153
|
+
return row ? rowToRoomReminder(row) : null;
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
listRoomReminders(input: ListRoomRemindersInput = {}): RoomReminder[] {
|
|
4157
|
+
const conditions: string[] = [];
|
|
4158
|
+
const params: SQLQueryBindings[] = [];
|
|
4159
|
+
if (input.roomId?.trim()) {
|
|
4160
|
+
const room = this.resolveRoom(input.roomId);
|
|
4161
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
4162
|
+
conditions.push('rr.room_id = ?');
|
|
4163
|
+
params.push(room.id);
|
|
4164
|
+
}
|
|
4165
|
+
if ((input.status ?? 'scheduled') !== 'all') {
|
|
4166
|
+
conditions.push('rr.status = ?');
|
|
4167
|
+
params.push(assertRoomReminderStatus(input.status ?? 'scheduled'));
|
|
4168
|
+
}
|
|
4169
|
+
if (input.createdBy?.trim()) {
|
|
4170
|
+
conditions.push('rr.created_by = ?');
|
|
4171
|
+
params.push(input.createdBy.trim());
|
|
4172
|
+
}
|
|
4173
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
4174
|
+
const rows = this.db.query(`
|
|
4175
|
+
SELECT rr.*, c.name AS room_name
|
|
4176
|
+
FROM room_reminders rr
|
|
4177
|
+
JOIN chats c ON c.id = rr.room_id
|
|
4178
|
+
${where}
|
|
4179
|
+
ORDER BY rr.fire_at ASC, rr.created_at ASC
|
|
4180
|
+
LIMIT ?
|
|
4181
|
+
`).all(...params, limitValue(input.limit, 50)) as Record<string, unknown>[];
|
|
4182
|
+
return rows.map(rowToRoomReminder);
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
createRoomReminder(input: CreateRoomReminderInput): RoomReminder {
|
|
4186
|
+
if (!Number.isInteger(input.sourceMessageId) || input.sourceMessageId < 1) throw new Error('INVALID_SOURCE_MESSAGE_ID');
|
|
4187
|
+
const sourceMessage = this.getMessage(input.sourceMessageId);
|
|
4188
|
+
if (!sourceMessage) throw new Error('SOURCE_MESSAGE_NOT_FOUND');
|
|
4189
|
+
const title = input.title.trim();
|
|
4190
|
+
if (!title) throw new Error('reminder title is required');
|
|
4191
|
+
const fireAt = normalizeReminderFireAt(input);
|
|
4192
|
+
const createdBy = input.createdBy?.trim() || null;
|
|
4193
|
+
const repeat = normalizeReminderRepeat(input.repeat);
|
|
4194
|
+
const id = crypto.randomUUID();
|
|
4195
|
+
this.db.query(`
|
|
4196
|
+
INSERT INTO room_reminders (id, room_id, source_message_id, title, status, created_by, fire_at, repeat, created_at, updated_at)
|
|
4197
|
+
VALUES (?, ?, ?, ?, 'scheduled', ?, ?, ?, datetime('now'), datetime('now'))
|
|
4198
|
+
`).run(id, sourceMessage.chat_id, sourceMessage.id, title, createdBy, fireAt, repeat);
|
|
4199
|
+
return this.getRoomReminder(id)!;
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
cancelRoomReminder(id: string): RoomReminder | null {
|
|
4203
|
+
const reminderId = id.trim();
|
|
4204
|
+
if (!reminderId) return null;
|
|
4205
|
+
const existing = this.getRoomReminder(reminderId);
|
|
4206
|
+
if (!existing) return null;
|
|
4207
|
+
if (existing.status === 'scheduled') {
|
|
4208
|
+
this.db.query(`
|
|
4209
|
+
UPDATE room_reminders
|
|
4210
|
+
SET status = 'canceled', canceled_at = datetime('now'), updated_at = datetime('now')
|
|
4211
|
+
WHERE id = ?
|
|
4212
|
+
`).run(reminderId);
|
|
4213
|
+
}
|
|
4214
|
+
return this.getRoomReminder(reminderId);
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
getRoomSavedMessage(id: string): RoomSavedMessage | null {
|
|
4218
|
+
const savedMessageId = id.trim();
|
|
4219
|
+
if (!savedMessageId) return null;
|
|
4220
|
+
const row = this.db.query(`
|
|
4221
|
+
SELECT
|
|
4222
|
+
rsm.*,
|
|
4223
|
+
c.name AS room_name,
|
|
4224
|
+
m.sender AS message_sender,
|
|
4225
|
+
m.content AS message_content,
|
|
4226
|
+
m.created_at AS message_created_at
|
|
4227
|
+
FROM room_saved_messages rsm
|
|
4228
|
+
JOIN chats c ON c.id = rsm.room_id
|
|
4229
|
+
JOIN messages m ON m.id = rsm.message_id
|
|
4230
|
+
WHERE rsm.id = ?
|
|
4231
|
+
`).get(savedMessageId) as Record<string, unknown> | null;
|
|
4232
|
+
return row ? rowToRoomSavedMessage(row) : null;
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
listRoomSavedMessages(input: ListRoomSavedMessagesInput = {}): RoomSavedMessage[] {
|
|
4236
|
+
const conditions: string[] = [];
|
|
4237
|
+
const params: SQLQueryBindings[] = [];
|
|
4238
|
+
if (input.roomId?.trim()) {
|
|
4239
|
+
const room = this.resolveRoom(input.roomId);
|
|
4240
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
4241
|
+
conditions.push('rsm.room_id = ?');
|
|
4242
|
+
params.push(room.id);
|
|
4243
|
+
}
|
|
4244
|
+
if (input.savedBy?.trim()) {
|
|
4245
|
+
conditions.push('rsm.saved_by = ?');
|
|
4246
|
+
params.push(input.savedBy.trim());
|
|
4247
|
+
}
|
|
4248
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
4249
|
+
const rows = this.db.query(`
|
|
4250
|
+
SELECT
|
|
4251
|
+
rsm.*,
|
|
4252
|
+
c.name AS room_name,
|
|
4253
|
+
m.sender AS message_sender,
|
|
4254
|
+
m.content AS message_content,
|
|
4255
|
+
m.created_at AS message_created_at
|
|
4256
|
+
FROM room_saved_messages rsm
|
|
4257
|
+
JOIN chats c ON c.id = rsm.room_id
|
|
4258
|
+
JOIN messages m ON m.id = rsm.message_id
|
|
4259
|
+
${where}
|
|
4260
|
+
ORDER BY rsm.created_at DESC, rsm.updated_at DESC
|
|
4261
|
+
LIMIT ?
|
|
4262
|
+
`).all(...params, limitValue(input.limit, 50)) as Record<string, unknown>[];
|
|
4263
|
+
return rows.map(rowToRoomSavedMessage);
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4266
|
+
saveRoomMessage(input: CreateRoomSavedMessageInput): SaveRoomMessageResult {
|
|
4267
|
+
if (!Number.isInteger(input.sourceMessageId) || input.sourceMessageId < 1) throw new Error('INVALID_SOURCE_MESSAGE_ID');
|
|
4268
|
+
const sourceMessage = this.getMessage(input.sourceMessageId);
|
|
4269
|
+
if (!sourceMessage) throw new Error('SOURCE_MESSAGE_NOT_FOUND');
|
|
4270
|
+
const savedBy = input.savedBy.trim();
|
|
4271
|
+
if (!savedBy) throw new Error('saved_by is required');
|
|
4272
|
+
const note = input.note?.trim() || null;
|
|
4273
|
+
const existing = this.db.query(`
|
|
4274
|
+
SELECT id
|
|
4275
|
+
FROM room_saved_messages
|
|
4276
|
+
WHERE message_id = ? AND saved_by = ?
|
|
4277
|
+
`).get(sourceMessage.id, savedBy) as { id: string } | null;
|
|
4278
|
+
if (existing) {
|
|
4279
|
+
this.db.query(`
|
|
4280
|
+
UPDATE room_saved_messages
|
|
4281
|
+
SET note = ?, updated_at = datetime('now')
|
|
4282
|
+
WHERE id = ?
|
|
4283
|
+
`).run(note, existing.id);
|
|
4284
|
+
return { savedMessage: this.getRoomSavedMessage(existing.id)!, created: false };
|
|
4285
|
+
}
|
|
4286
|
+
const id = crypto.randomUUID();
|
|
4287
|
+
this.db.query(`
|
|
4288
|
+
INSERT INTO room_saved_messages (id, room_id, message_id, saved_by, note, created_at, updated_at)
|
|
4289
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
4290
|
+
`).run(id, sourceMessage.chat_id, sourceMessage.id, savedBy, note);
|
|
4291
|
+
return { savedMessage: this.getRoomSavedMessage(id)!, created: true };
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
removeRoomSavedMessage(id: string): RoomSavedMessage | null {
|
|
4295
|
+
const savedMessage = this.getRoomSavedMessage(id);
|
|
4296
|
+
if (!savedMessage) return null;
|
|
4297
|
+
this.db.query('DELETE FROM room_saved_messages WHERE id = ?').run(savedMessage.id);
|
|
4298
|
+
return savedMessage;
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
createWorkflowRun(input: CreateWorkflowRunInput): WorkflowRun {
|
|
4302
|
+
const filePath = input.filePath.trim();
|
|
4303
|
+
if (!filePath) throw new Error('workflow file_path is required');
|
|
4304
|
+
const id = crypto.randomUUID();
|
|
4305
|
+
this.db.query(`
|
|
4306
|
+
INSERT INTO workflow_runs (id, file_path, goal, status, created_by, created_at, updated_at)
|
|
4307
|
+
VALUES (?, ?, ?, 'running', ?, datetime('now'), datetime('now'))
|
|
4308
|
+
`).run(id, filePath, input.goal?.trim() || null, input.createdBy?.trim() || null);
|
|
4309
|
+
return this.getWorkflowRun(id)!;
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
getWorkflowRun(id: string): WorkflowRun | null {
|
|
4313
|
+
const row = this.db.query('SELECT * FROM workflow_runs WHERE id = ?').get(id.trim()) as Record<string, unknown> | null;
|
|
4314
|
+
return row ? rowToWorkflowRun(row) : null;
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
listWorkflowRuns(input: { status?: WorkflowRunStatus | 'all'; limit?: number } = {}): WorkflowRun[] {
|
|
4318
|
+
const status = input.status ?? 'all';
|
|
4319
|
+
const rows = status === 'all'
|
|
4320
|
+
? this.db.query(`
|
|
4321
|
+
SELECT * FROM workflow_runs
|
|
4322
|
+
ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC
|
|
4323
|
+
LIMIT ?
|
|
4324
|
+
`).all(limitValue(input.limit, 50)) as Record<string, unknown>[]
|
|
4325
|
+
: this.db.query(`
|
|
4326
|
+
SELECT * FROM workflow_runs
|
|
4327
|
+
WHERE status = ?
|
|
4328
|
+
ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC
|
|
4329
|
+
LIMIT ?
|
|
4330
|
+
`).all(assertWorkflowRunStatus(status), limitValue(input.limit, 50)) as Record<string, unknown>[];
|
|
4331
|
+
return rows.map(rowToWorkflowRun);
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
finishWorkflowRun(id: string, input: FinishWorkflowRunInput): WorkflowRun {
|
|
4335
|
+
const run = this.getWorkflowRun(id);
|
|
4336
|
+
if (!run) throw new Error(`workflow run ${id} was not found`);
|
|
4337
|
+
const status = assertWorkflowRunStatus(input.status);
|
|
4338
|
+
const finalOutput = input.finalOutput === undefined ? run.final_output : input.finalOutput;
|
|
4339
|
+
const error = input.error === undefined ? run.error : input.error;
|
|
4340
|
+
const completedAtSql = status === 'running' ? 'NULL' : "COALESCE(completed_at, datetime('now'))";
|
|
4341
|
+
this.db.query(`
|
|
4342
|
+
UPDATE workflow_runs
|
|
4343
|
+
SET status = ?,
|
|
4344
|
+
final_output_json = ?,
|
|
4345
|
+
error = ?,
|
|
4346
|
+
completed_at = ${completedAtSql},
|
|
4347
|
+
updated_at = datetime('now')
|
|
4348
|
+
WHERE id = ?
|
|
4349
|
+
`).run(status, finalOutput ? JSON.stringify(finalOutput) : null, error?.trim() || null, run.id);
|
|
4350
|
+
return this.getWorkflowRun(run.id)!;
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
createWorkflowNode(input: CreateWorkflowNodeInput): WorkflowNode {
|
|
4354
|
+
const run = this.getWorkflowRun(input.runId);
|
|
4355
|
+
if (!run) throw new Error(`workflow run ${input.runId} was not found`);
|
|
4356
|
+
const parentId = input.parentId?.trim() || null;
|
|
4357
|
+
if (parentId) {
|
|
4358
|
+
const parent = this.getWorkflowNode(parentId);
|
|
4359
|
+
if (!parent) throw new Error(`workflow parent node ${parentId} was not found`);
|
|
4360
|
+
if (parent.run_id !== run.id) throw new Error('WORKFLOW_PARENT_RUN_MISMATCH');
|
|
4361
|
+
}
|
|
4362
|
+
const title = input.title.trim();
|
|
4363
|
+
if (!title) throw new Error('workflow node title is required');
|
|
4364
|
+
const status = assertWorkflowNodeStatus(input.status ?? 'pending');
|
|
4365
|
+
const id = crypto.randomUUID();
|
|
4366
|
+
const completedAtSql = status === 'done' || status === 'failed' ? "datetime('now')" : 'NULL';
|
|
4367
|
+
this.db.query(`
|
|
4368
|
+
INSERT INTO workflow_nodes (
|
|
4369
|
+
id, run_id, parent_id, kind, title, role, status, context_json, instruction,
|
|
4370
|
+
capabilities_json, output_contract_json, output_json, evidence_json, task_id,
|
|
4371
|
+
message_id, created_at, updated_at, completed_at
|
|
4372
|
+
)
|
|
4373
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'), ${completedAtSql})
|
|
2155
4374
|
`).run(
|
|
2156
|
-
|
|
4375
|
+
id,
|
|
4376
|
+
run.id,
|
|
2157
4377
|
parentId,
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
input.
|
|
2161
|
-
|
|
2162
|
-
input.
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
JSON.stringify(
|
|
4378
|
+
assertWorkflowNodeKind(input.kind),
|
|
4379
|
+
title,
|
|
4380
|
+
input.role?.trim() || null,
|
|
4381
|
+
status,
|
|
4382
|
+
input.context ? JSON.stringify(input.context) : null,
|
|
4383
|
+
input.instruction?.trim() || null,
|
|
4384
|
+
JSON.stringify(input.capabilities ?? []),
|
|
4385
|
+
input.outputContract ? JSON.stringify(input.outputContract) : null,
|
|
4386
|
+
input.output ? JSON.stringify(input.output) : null,
|
|
4387
|
+
input.evidence ? JSON.stringify(input.evidence) : null,
|
|
4388
|
+
input.taskId?.trim() || null,
|
|
4389
|
+
input.messageId ?? null,
|
|
2167
4390
|
);
|
|
2168
|
-
|
|
2169
|
-
const id = Number(result.lastInsertRowid);
|
|
2170
|
-
return this.getMessage(id)!;
|
|
4391
|
+
return this.getWorkflowNode(id)!;
|
|
2171
4392
|
}
|
|
2172
4393
|
|
|
2173
|
-
|
|
2174
|
-
const row = this.db.query(
|
|
2175
|
-
|
|
2176
|
-
FROM messages m
|
|
2177
|
-
JOIN chats c ON c.id = m.chat_id
|
|
2178
|
-
WHERE m.id = ?
|
|
2179
|
-
`).get(id) as Record<string, unknown> | null;
|
|
2180
|
-
return row ? rowToMessage(row) : null;
|
|
4394
|
+
getWorkflowNode(id: string): WorkflowNode | null {
|
|
4395
|
+
const row = this.db.query('SELECT * FROM workflow_nodes WHERE id = ?').get(id.trim()) as Record<string, unknown> | null;
|
|
4396
|
+
return row ? rowToWorkflowNode(row) : null;
|
|
2181
4397
|
}
|
|
2182
4398
|
|
|
2183
|
-
|
|
2184
|
-
const
|
|
2185
|
-
|
|
4399
|
+
updateWorkflowNode(id: string, input: UpdateWorkflowNodeInput): WorkflowNode {
|
|
4400
|
+
const node = this.getWorkflowNode(id);
|
|
4401
|
+
if (!node) throw new Error(`workflow node ${id} was not found`);
|
|
4402
|
+
const status = input.status ? assertWorkflowNodeStatus(input.status) : node.status;
|
|
4403
|
+
const completedAtSql = status === 'done' || status === 'failed'
|
|
4404
|
+
? "COALESCE(completed_at, datetime('now'))"
|
|
4405
|
+
: 'NULL';
|
|
4406
|
+
const output = input.output === undefined ? node.output : input.output;
|
|
4407
|
+
const evidence = input.evidence === undefined ? node.evidence : input.evidence;
|
|
4408
|
+
const taskId = input.taskId === undefined ? node.task_id : input.taskId;
|
|
4409
|
+
const messageId = input.messageId === undefined ? node.message_id : input.messageId;
|
|
4410
|
+
this.db.query(`
|
|
4411
|
+
UPDATE workflow_nodes
|
|
4412
|
+
SET status = ?,
|
|
4413
|
+
output_json = ?,
|
|
4414
|
+
evidence_json = ?,
|
|
4415
|
+
task_id = ?,
|
|
4416
|
+
message_id = ?,
|
|
4417
|
+
completed_at = ${completedAtSql},
|
|
4418
|
+
updated_at = datetime('now')
|
|
4419
|
+
WHERE id = ?
|
|
4420
|
+
`).run(
|
|
4421
|
+
status,
|
|
4422
|
+
output ? JSON.stringify(output) : null,
|
|
4423
|
+
evidence ? JSON.stringify(evidence) : null,
|
|
4424
|
+
taskId?.trim() || null,
|
|
4425
|
+
messageId ?? null,
|
|
4426
|
+
node.id,
|
|
4427
|
+
);
|
|
4428
|
+
return this.getWorkflowNode(node.id)!;
|
|
4429
|
+
}
|
|
2186
4430
|
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
4431
|
+
listWorkflowNodes(runId: string): WorkflowNode[] {
|
|
4432
|
+
const run = this.getWorkflowRun(runId);
|
|
4433
|
+
if (!run) throw new Error(`workflow run ${runId} was not found`);
|
|
4434
|
+
const rows = this.db.query(`
|
|
4435
|
+
SELECT *
|
|
4436
|
+
FROM workflow_nodes
|
|
4437
|
+
WHERE run_id = ?
|
|
4438
|
+
ORDER BY datetime(created_at) ASC, rowid ASC
|
|
4439
|
+
`).all(run.id) as Record<string, unknown>[];
|
|
4440
|
+
return rows.map(rowToWorkflowNode);
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
getWorkflowRunRecord(id: string): WorkflowRunRecord | null {
|
|
4444
|
+
const run = this.getWorkflowRun(id);
|
|
4445
|
+
if (!run) return null;
|
|
4446
|
+
return { run, nodes: this.listWorkflowNodes(run.id) };
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
fireDueRoomReminders(input: FireDueRoomRemindersInput = {}): FireDueRoomRemindersResult {
|
|
4450
|
+
const now = normalizeReminderFireNow(input.now);
|
|
4451
|
+
const due = this.db.query(`
|
|
4452
|
+
SELECT rr.*, c.name AS room_name
|
|
4453
|
+
FROM room_reminders rr
|
|
4454
|
+
JOIN chats c ON c.id = rr.room_id
|
|
4455
|
+
WHERE rr.status = 'scheduled' AND rr.fire_at <= ?
|
|
4456
|
+
ORDER BY rr.fire_at ASC, rr.created_at ASC
|
|
4457
|
+
LIMIT ?
|
|
4458
|
+
`).all(now, limitValue(input.limit, 25)) as Record<string, unknown>[];
|
|
2194
4459
|
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
4460
|
+
return this.db.transaction(() => {
|
|
4461
|
+
const fired: FiredRoomReminder[] = [];
|
|
4462
|
+
const messages: Message[] = [];
|
|
4463
|
+
const deliveries: MessageDelivery[] = [];
|
|
4464
|
+
|
|
4465
|
+
for (const row of due) {
|
|
4466
|
+
const reminder = this.getRoomReminder(String(row.id));
|
|
4467
|
+
if (!reminder || reminder.status !== 'scheduled' || reminder.fire_at > now) continue;
|
|
4468
|
+
const source = this.getMessage(reminder.source_message_id);
|
|
4469
|
+
const recipient = reminder.created_by?.trim() || null;
|
|
4470
|
+
const message = this.createMessage({
|
|
4471
|
+
chatId: reminder.room_id,
|
|
4472
|
+
sender: 'pal',
|
|
4473
|
+
recipient,
|
|
4474
|
+
content: reminderFireMessageContent(reminder, source),
|
|
4475
|
+
type: 'system',
|
|
4476
|
+
idempotencyKey: reminderFireIdempotencyKey(reminder),
|
|
4477
|
+
mentions: recipient ? [recipient] : [],
|
|
4478
|
+
});
|
|
4479
|
+
const reminderDeliveries = this.resolveDeliveriesForMessage(message.id);
|
|
4480
|
+
const updated = reminder.repeat
|
|
4481
|
+
? this.db.query(`
|
|
4482
|
+
UPDATE room_reminders
|
|
4483
|
+
SET fire_at = ?, fired_at = ?, updated_at = ?
|
|
4484
|
+
WHERE id = ? AND status = 'scheduled'
|
|
4485
|
+
`).run(nextReminderFireAt(reminder, now), now, now, reminder.id)
|
|
4486
|
+
: this.db.query(`
|
|
4487
|
+
UPDATE room_reminders
|
|
4488
|
+
SET status = 'fired', fired_at = ?, updated_at = ?
|
|
4489
|
+
WHERE id = ? AND status = 'scheduled'
|
|
4490
|
+
`).run(now, now, reminder.id);
|
|
4491
|
+
if (updated.changes === 0) continue;
|
|
4492
|
+
const firedReminder = this.getRoomReminder(reminder.id)!;
|
|
4493
|
+
fired.push({ reminder: firedReminder, message, deliveries: reminderDeliveries });
|
|
4494
|
+
messages.push(message);
|
|
4495
|
+
deliveries.push(...reminderDeliveries);
|
|
2201
4496
|
}
|
|
2202
|
-
}
|
|
2203
4497
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
4498
|
+
return {
|
|
4499
|
+
fired,
|
|
4500
|
+
reminders: fired.map((item) => item.reminder),
|
|
4501
|
+
messages,
|
|
4502
|
+
deliveries,
|
|
4503
|
+
};
|
|
4504
|
+
})();
|
|
4505
|
+
}
|
|
2208
4506
|
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
4507
|
+
createHeldMessageDraft(input: CreateHeldMessageDraftInput): HeldMessageDraft {
|
|
4508
|
+
const room = this.getChatById(input.chatId);
|
|
4509
|
+
if (!room) throw new Error(`chat ${input.chatId} was not found`);
|
|
4510
|
+
const agent = input.agent.trim();
|
|
4511
|
+
const sender = input.sender.trim();
|
|
4512
|
+
const content = input.content.trim();
|
|
4513
|
+
if (!agent) throw new Error('agent is required');
|
|
4514
|
+
if (!sender) throw new Error('sender is required');
|
|
4515
|
+
if (!content) throw new Error('content is required');
|
|
4516
|
+
if (!this.getAgent(agent)) throw new Error(`agent ${agent} not found`);
|
|
4517
|
+
const id = crypto.randomUUID();
|
|
4518
|
+
this.db.query(`
|
|
4519
|
+
INSERT INTO held_message_drafts (id, chat_id, agent, sender, content, mentions_json, base_message_id, latest_message_id_at_hold, status, hold_reason)
|
|
4520
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'held', ?)
|
|
4521
|
+
`).run(
|
|
4522
|
+
id,
|
|
4523
|
+
room.id,
|
|
4524
|
+
agent,
|
|
4525
|
+
sender,
|
|
4526
|
+
content,
|
|
4527
|
+
JSON.stringify(normalizeMentionsInput(input.mentions)),
|
|
4528
|
+
input.baseMessageId,
|
|
4529
|
+
input.latestMessageIdAtHold,
|
|
4530
|
+
input.holdReason,
|
|
4531
|
+
);
|
|
4532
|
+
return this.getHeldMessageDraft(id)!;
|
|
4533
|
+
}
|
|
2213
4534
|
|
|
2214
|
-
|
|
4535
|
+
getHeldMessageDraft(id: string): HeldMessageDraft | null {
|
|
4536
|
+
const row = this.db.query(`
|
|
4537
|
+
SELECT hmd.*, c.name AS chat_name, c.display_name AS chat_display_name,
|
|
4538
|
+
(
|
|
4539
|
+
SELECT COUNT(*)
|
|
4540
|
+
FROM messages m
|
|
4541
|
+
WHERE m.chat_id = hmd.chat_id
|
|
4542
|
+
AND m.id > hmd.base_message_id
|
|
4543
|
+
AND m.id <= hmd.latest_message_id_at_hold
|
|
4544
|
+
AND m.sender != hmd.sender
|
|
4545
|
+
) AS intervening_message_count
|
|
4546
|
+
FROM held_message_drafts hmd
|
|
4547
|
+
JOIN chats c ON c.id = hmd.chat_id
|
|
4548
|
+
WHERE hmd.id = ?
|
|
4549
|
+
LIMIT 1
|
|
4550
|
+
`).get(id.trim()) as Record<string, unknown> | null;
|
|
4551
|
+
return row ? rowToHeldMessageDraft(row) : null;
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
listHeldMessageDrafts(agent: string, status: HeldDraftStatus = 'held', limit = 50): HeldMessageDraft[] {
|
|
2215
4555
|
const rows = this.db.query(`
|
|
2216
|
-
SELECT
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
4556
|
+
SELECT hmd.*, c.name AS chat_name, c.display_name AS chat_display_name,
|
|
4557
|
+
(
|
|
4558
|
+
SELECT COUNT(*)
|
|
4559
|
+
FROM messages m
|
|
4560
|
+
WHERE m.chat_id = hmd.chat_id
|
|
4561
|
+
AND m.id > hmd.base_message_id
|
|
4562
|
+
AND m.id <= hmd.latest_message_id_at_hold
|
|
4563
|
+
AND m.sender != hmd.sender
|
|
4564
|
+
) AS intervening_message_count
|
|
4565
|
+
FROM held_message_drafts hmd
|
|
4566
|
+
JOIN chats c ON c.id = hmd.chat_id
|
|
4567
|
+
WHERE hmd.agent = ? AND hmd.status = ?
|
|
4568
|
+
ORDER BY datetime(hmd.created_at) DESC, hmd.id ASC
|
|
2221
4569
|
LIMIT ?
|
|
2222
|
-
`).all(
|
|
4570
|
+
`).all(agent.trim(), status, limitValue(limit, 50)) as Record<string, unknown>[];
|
|
4571
|
+
return rows.map(rowToHeldMessageDraft);
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
abandonHeldMessageDraft(id: string): HeldMessageDraft | null {
|
|
4575
|
+
const existing = this.getHeldMessageDraft(id);
|
|
4576
|
+
if (!existing) return null;
|
|
4577
|
+
if (existing.status !== 'held') throw new Error('HELD_DRAFT_ALREADY_RESOLVED');
|
|
4578
|
+
const result = this.db.query(`
|
|
4579
|
+
UPDATE held_message_drafts
|
|
4580
|
+
SET status = 'abandoned', updated_at = datetime('now'), resolved_at = datetime('now')
|
|
4581
|
+
WHERE id = ? AND status = 'held'
|
|
4582
|
+
`).run(existing.id);
|
|
4583
|
+
if (result.changes === 0) throw new Error('HELD_DRAFT_ALREADY_RESOLVED');
|
|
4584
|
+
return this.getHeldMessageDraft(existing.id)!;
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4587
|
+
sendHeldMessageDraftAnyway(id: string): { draft: HeldMessageDraft; message: Message } | null {
|
|
4588
|
+
return this.db.transaction(() => {
|
|
4589
|
+
const existing = this.getHeldMessageDraft(id);
|
|
4590
|
+
if (!existing) return null;
|
|
4591
|
+
if (existing.status !== 'held') throw new Error('HELD_DRAFT_ALREADY_RESOLVED');
|
|
4592
|
+
const message = this.createMessage({
|
|
4593
|
+
chatId: existing.chat_id,
|
|
4594
|
+
sender: existing.sender,
|
|
4595
|
+
content: existing.content,
|
|
4596
|
+
mentions: existing.mentions,
|
|
4597
|
+
});
|
|
4598
|
+
const result = this.db.query(`
|
|
4599
|
+
UPDATE held_message_drafts
|
|
4600
|
+
SET status = 'sent_anyway',
|
|
4601
|
+
resolved_message_id = ?,
|
|
4602
|
+
updated_at = datetime('now'),
|
|
4603
|
+
resolved_at = datetime('now')
|
|
4604
|
+
WHERE id = ? AND status = 'held'
|
|
4605
|
+
`).run(message.id, existing.id);
|
|
4606
|
+
if (result.changes === 0) throw new Error('HELD_DRAFT_ALREADY_RESOLVED');
|
|
4607
|
+
return { draft: this.getHeldMessageDraft(existing.id)!, message };
|
|
4608
|
+
})();
|
|
4609
|
+
}
|
|
2223
4610
|
|
|
2224
|
-
|
|
4611
|
+
reviseHeldMessageDraft(id: string, content: string): { draft: HeldMessageDraft; message: Message } | null {
|
|
4612
|
+
const revisedContent = content.trim();
|
|
4613
|
+
if (!revisedContent) throw new Error('content is required');
|
|
4614
|
+
return this.db.transaction(() => {
|
|
4615
|
+
const existing = this.getHeldMessageDraft(id);
|
|
4616
|
+
if (!existing) return null;
|
|
4617
|
+
if (existing.status !== 'held') throw new Error('HELD_DRAFT_ALREADY_RESOLVED');
|
|
4618
|
+
const message = this.createMessage({
|
|
4619
|
+
chatId: existing.chat_id,
|
|
4620
|
+
sender: existing.sender,
|
|
4621
|
+
content: revisedContent,
|
|
4622
|
+
mentions: existing.mentions,
|
|
4623
|
+
});
|
|
4624
|
+
const result = this.db.query(`
|
|
4625
|
+
UPDATE held_message_drafts
|
|
4626
|
+
SET status = 'sent_anyway',
|
|
4627
|
+
resolved_message_id = ?,
|
|
4628
|
+
updated_at = datetime('now'),
|
|
4629
|
+
resolved_at = datetime('now')
|
|
4630
|
+
WHERE id = ? AND status = 'held'
|
|
4631
|
+
`).run(message.id, existing.id);
|
|
4632
|
+
if (result.changes === 0) throw new Error('HELD_DRAFT_ALREADY_RESOLVED');
|
|
4633
|
+
return { draft: this.getHeldMessageDraft(existing.id)!, message };
|
|
4634
|
+
})();
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
getDeliveryContext(input: { agent: string; chatId: string; messageId: number; limit?: number }): DeliveryContext {
|
|
4638
|
+
const room = this.getChatById(input.chatId);
|
|
4639
|
+
if (!room) throw new Error(`room ${input.chatId} was not found`);
|
|
4640
|
+
const trigger = this.getMessage(input.messageId);
|
|
4641
|
+
if (!trigger) throw new Error(`message ${input.messageId} was not found`);
|
|
4642
|
+
if (trigger.chat_id !== room.id) throw new Error(`message ${input.messageId} does not belong to room ${room.id}`);
|
|
4643
|
+
|
|
4644
|
+
const participant = this.getRoomParticipant(room.id, input.agent);
|
|
4645
|
+
const cursor = participant?.delivery_cursor_message_id ?? 0;
|
|
4646
|
+
const previous = this.db.query(`
|
|
4647
|
+
SELECT COALESCE(MAX(message_id), 0) AS message_id
|
|
4648
|
+
FROM message_deliveries
|
|
4649
|
+
WHERE agent = ? AND chat_id = ? AND message_id < ?
|
|
4650
|
+
`).get(input.agent, room.id, trigger.id) as { message_id: number } | null;
|
|
4651
|
+
const startMessageId = Math.max(Number(cursor ?? 0), Number(previous?.message_id ?? 0), 0);
|
|
4652
|
+
const total = this.db.query(`
|
|
4653
|
+
SELECT COUNT(*) AS count
|
|
4654
|
+
FROM messages
|
|
4655
|
+
WHERE chat_id = ? AND id > ? AND id <= ?
|
|
4656
|
+
`).get(room.id, startMessageId, trigger.id) as { count: number };
|
|
4657
|
+
const limit = limitValue(input.limit, 50);
|
|
4658
|
+
const rows = this.db.query(`
|
|
4659
|
+
SELECT *
|
|
4660
|
+
FROM (
|
|
4661
|
+
SELECT m.*, c.name AS chat_name
|
|
4662
|
+
FROM messages m
|
|
4663
|
+
JOIN chats c ON c.id = m.chat_id
|
|
4664
|
+
WHERE m.chat_id = ? AND m.id > ? AND m.id <= ?
|
|
4665
|
+
ORDER BY m.id DESC
|
|
4666
|
+
LIMIT ?
|
|
4667
|
+
)
|
|
4668
|
+
ORDER BY id ASC
|
|
4669
|
+
`).all(room.id, startMessageId, trigger.id, limit) as Record<string, unknown>[];
|
|
4670
|
+
const totalCount = Number(total.count ?? 0);
|
|
4671
|
+
return {
|
|
4672
|
+
startMessageId,
|
|
4673
|
+
totalCount,
|
|
4674
|
+
omittedCount: Math.max(0, totalCount - rows.length),
|
|
4675
|
+
messages: this.attachMessageAttachments(rows.map(rowToMessage)),
|
|
4676
|
+
savedMessages: this.listRoomSavedMessages({ roomId: room.id, limit: 10 }),
|
|
4677
|
+
};
|
|
2225
4678
|
}
|
|
2226
4679
|
|
|
2227
4680
|
listInbox(agent: string, after = 0, limit = 50): Message[] {
|
|
@@ -2236,7 +4689,7 @@ export class MessageStore {
|
|
|
2236
4689
|
LIMIT ?
|
|
2237
4690
|
`).all(after, agent, agent, limitValue(limit, 50)) as Record<string, unknown>[];
|
|
2238
4691
|
|
|
2239
|
-
return rows.map(rowToMessage);
|
|
4692
|
+
return this.attachMessageAttachments(rows.map(rowToMessage));
|
|
2240
4693
|
}
|
|
2241
4694
|
|
|
2242
4695
|
getComputer(id: string): Computer | null {
|
|
@@ -2253,6 +4706,79 @@ export class MessageStore {
|
|
|
2253
4706
|
return rows.map(rowToComputer);
|
|
2254
4707
|
}
|
|
2255
4708
|
|
|
4709
|
+
updateComputerName(id: string, name: string): Computer {
|
|
4710
|
+
const computerId = id.trim();
|
|
4711
|
+
const displayName = name.trim();
|
|
4712
|
+
if (!computerId) throw new Error('computer id is required');
|
|
4713
|
+
if (!displayName) throw new Error('computer name is required');
|
|
4714
|
+
this.db.query(`
|
|
4715
|
+
UPDATE computers
|
|
4716
|
+
SET name = ?, updated_at = datetime('now')
|
|
4717
|
+
WHERE id = ?
|
|
4718
|
+
`).run(displayName, computerId);
|
|
4719
|
+
const computer = this.getComputer(computerId);
|
|
4720
|
+
if (!computer) throw new Error('computer not found');
|
|
4721
|
+
return computer;
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4724
|
+
regenerateComputerCommand(id: string, input: RegenerateComputerCommandInput = {}): ProvisionedComputer {
|
|
4725
|
+
const computer = this.getComputer(id);
|
|
4726
|
+
if (!computer) throw new Error('computer not found');
|
|
4727
|
+
const apiKey = `sk_machine_${randomBytes(32).toString('hex')}`;
|
|
4728
|
+
this.db.query(`
|
|
4729
|
+
UPDATE computers
|
|
4730
|
+
SET credential_hash = ?, updated_at = datetime('now')
|
|
4731
|
+
WHERE id = ?
|
|
4732
|
+
`).run(hashSecret(apiKey), computer.id);
|
|
4733
|
+
const serverUrl = input.serverUrl?.trim() || 'http://127.0.0.1:4127';
|
|
4734
|
+
const command = daemonStartCommand({ commandPrefix: input.packageName, serverUrl, apiKey, name: computer.name });
|
|
4735
|
+
return { computer: this.getComputer(computer.id)!, api_key: apiKey, command };
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
getComputerDeletionImpact(id: string): ComputerDeletionImpact | null {
|
|
4739
|
+
const computer = this.getComputer(id);
|
|
4740
|
+
if (!computer) return null;
|
|
4741
|
+
return {
|
|
4742
|
+
computer,
|
|
4743
|
+
projects: this.listProjectsForComputer(computer.id),
|
|
4744
|
+
assignments: this.listComputerAgentAssignments(computer.id),
|
|
4745
|
+
};
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
deleteComputer(id: string, options: { removeProjects?: boolean; removeAssignments?: boolean } = {}): ComputerDeletionResult {
|
|
4749
|
+
const computer = this.getComputer(id);
|
|
4750
|
+
if (!computer) throw new Error('computer not found');
|
|
4751
|
+
const impact = this.getComputerDeletionImpact(computer.id)!;
|
|
4752
|
+
if (impact.projects.length > 0 && options.removeProjects !== true) throw new Error('COMPUTER_HAS_PROJECTS');
|
|
4753
|
+
if (impact.assignments.length > 0 && options.removeAssignments !== true) throw new Error('COMPUTER_HAS_ASSIGNMENTS');
|
|
4754
|
+
let projectsRemoved = 0;
|
|
4755
|
+
let assignmentsRemoved = 0;
|
|
4756
|
+
let connectionsRemoved = 0;
|
|
4757
|
+
this.db.transaction(() => {
|
|
4758
|
+
if (options.removeProjects === true) {
|
|
4759
|
+
const projectIds = impact.projects.map((project) => project.id);
|
|
4760
|
+
for (const projectId of projectIds) {
|
|
4761
|
+
this.db.query('DELETE FROM chats WHERE project_id = ?').run(projectId);
|
|
4762
|
+
const result = this.db.query('DELETE FROM projects WHERE id = ?').run(projectId);
|
|
4763
|
+
projectsRemoved += result.changes;
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
if (options.removeAssignments === true) {
|
|
4767
|
+
const result = this.db.query('DELETE FROM computer_agent_assignments WHERE computer_id = ?').run(computer.id);
|
|
4768
|
+
assignmentsRemoved += result.changes;
|
|
4769
|
+
}
|
|
4770
|
+
const connectionsResult = this.db.query('DELETE FROM computer_connections WHERE computer_id = ?').run(computer.id);
|
|
4771
|
+
connectionsRemoved += connectionsResult.changes;
|
|
4772
|
+
this.db.query('DELETE FROM computers WHERE id = ?').run(computer.id);
|
|
4773
|
+
})();
|
|
4774
|
+
return {
|
|
4775
|
+
computer,
|
|
4776
|
+
projects_removed: projectsRemoved,
|
|
4777
|
+
assignments_removed: assignmentsRemoved,
|
|
4778
|
+
connections_removed: connectionsRemoved,
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
|
|
2256
4782
|
private getComputerByCredential(secret: string): Computer | null {
|
|
2257
4783
|
const row = this.db.query('SELECT * FROM computers WHERE credential_hash = ? ORDER BY created_at DESC LIMIT 1').get(hashSecret(secret.trim())) as Record<string, unknown> | null;
|
|
2258
4784
|
return row ? rowToComputer(row) : null;
|
|
@@ -2263,6 +4789,17 @@ export class MessageStore {
|
|
|
2263
4789
|
return row ? rowToComputerConnection(row) : null;
|
|
2264
4790
|
}
|
|
2265
4791
|
|
|
4792
|
+
listActiveComputerConnections(): ComputerConnection[] {
|
|
4793
|
+
const rows = this.db.query(`
|
|
4794
|
+
SELECT cc.*
|
|
4795
|
+
FROM computer_connections cc
|
|
4796
|
+
JOIN computers c ON c.active_connection_id = cc.id
|
|
4797
|
+
WHERE cc.status = 'active' AND c.status = 'online'
|
|
4798
|
+
ORDER BY datetime(cc.last_heartbeat_at) DESC, cc.id ASC
|
|
4799
|
+
`).all() as Record<string, unknown>[];
|
|
4800
|
+
return rows.map(rowToComputerConnection);
|
|
4801
|
+
}
|
|
4802
|
+
|
|
2266
4803
|
provisionComputer(input: ProvisionComputerInput = {}): ProvisionedComputer {
|
|
2267
4804
|
const id = `machine_${randomBytes(8).toString('hex')}`;
|
|
2268
4805
|
const apiKey = `sk_machine_${randomBytes(32).toString('hex')}`;
|
|
@@ -2272,9 +4809,8 @@ export class MessageStore {
|
|
|
2272
4809
|
VALUES (?, ?, ?, 'offline', NULL, NULL, datetime('now'))
|
|
2273
4810
|
`).run(id, name, hashSecret(apiKey));
|
|
2274
4811
|
|
|
2275
|
-
const packageName = input.packageName?.trim() || '@controlflow-ai/daemon@latest';
|
|
2276
4812
|
const serverUrl = input.serverUrl?.trim() || 'http://127.0.0.1:4127';
|
|
2277
|
-
const command =
|
|
4813
|
+
const command = daemonStartCommand({ commandPrefix: input.packageName, serverUrl, apiKey, name });
|
|
2278
4814
|
return { computer: this.getComputer(id)!, api_key: apiKey, command };
|
|
2279
4815
|
}
|
|
2280
4816
|
|
|
@@ -2286,7 +4822,7 @@ export class MessageStore {
|
|
|
2286
4822
|
if (!computerId) throw new Error('computer_id is required');
|
|
2287
4823
|
if (!secret) throw new Error('computer secret is required');
|
|
2288
4824
|
const credentialHash = hashSecret(secret);
|
|
2289
|
-
const existing = this.db.query('SELECT credential_hash FROM computers WHERE id = ?').get(computerId) as { credential_hash: string } | null;
|
|
4825
|
+
const existing = this.db.query('SELECT credential_hash, name FROM computers WHERE id = ?').get(computerId) as { credential_hash: string; name: string } | null;
|
|
2290
4826
|
if (existing && existing.credential_hash !== credentialHash) {
|
|
2291
4827
|
throw new Error('invalid computer credential');
|
|
2292
4828
|
}
|
|
@@ -2302,7 +4838,8 @@ export class MessageStore {
|
|
|
2302
4838
|
const revokedConnectionIds = revokedRows.map((row) => row.id);
|
|
2303
4839
|
const epochRow = this.db.query('SELECT COALESCE(MAX(epoch), 0) + 1 AS epoch FROM computer_connections WHERE computer_id = ?').get(computerId) as { epoch: number };
|
|
2304
4840
|
const epoch = Number(epochRow.epoch);
|
|
2305
|
-
const
|
|
4841
|
+
const providedName = input.name?.trim() || null;
|
|
4842
|
+
const name = providedName ?? existing?.name ?? computerId;
|
|
2306
4843
|
|
|
2307
4844
|
this.db.transaction(() => {
|
|
2308
4845
|
if (!existing) {
|
|
@@ -2313,9 +4850,13 @@ export class MessageStore {
|
|
|
2313
4850
|
} else {
|
|
2314
4851
|
this.db.query(`
|
|
2315
4852
|
UPDATE computers
|
|
2316
|
-
SET name =
|
|
4853
|
+
SET name = CASE WHEN ? IS NOT NULL THEN ? ELSE name END,
|
|
4854
|
+
status = 'online',
|
|
4855
|
+
active_connection_id = ?,
|
|
4856
|
+
last_seen_at = datetime('now'),
|
|
4857
|
+
updated_at = datetime('now')
|
|
2317
4858
|
WHERE id = ?
|
|
2318
|
-
`).run(
|
|
4859
|
+
`).run(providedName, providedName, connectionId, computerId);
|
|
2319
4860
|
}
|
|
2320
4861
|
this.db.query(`
|
|
2321
4862
|
UPDATE computer_connections
|
|
@@ -2405,6 +4946,21 @@ export class MessageStore {
|
|
|
2405
4946
|
return { computer, connection, daemon, token };
|
|
2406
4947
|
}
|
|
2407
4948
|
|
|
4949
|
+
getComputerWorkload(computerId: string): ComputerWorkload | null {
|
|
4950
|
+
const computer = this.getComputer(computerId);
|
|
4951
|
+
if (!computer) return null;
|
|
4952
|
+
const connection = computer.active_connection_id ? this.getComputerConnection(computer.active_connection_id) : null;
|
|
4953
|
+
const daemon = connection ? this.getDaemon(connection.id) : null;
|
|
4954
|
+
return {
|
|
4955
|
+
computer,
|
|
4956
|
+
connection,
|
|
4957
|
+
daemon,
|
|
4958
|
+
assignments: this.listComputerAgentAssignments(computer.id),
|
|
4959
|
+
runs: this.listRunsForComputer(computer.id, 20),
|
|
4960
|
+
deliveries: this.listActiveDeliveriesForComputer(computer.id, 20),
|
|
4961
|
+
};
|
|
4962
|
+
}
|
|
4963
|
+
|
|
2408
4964
|
closeStaleComputerConnections(timeoutMs: number, now = new Date()): number {
|
|
2409
4965
|
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new Error('timeoutMs must be non-negative');
|
|
2410
4966
|
const cutoff = new Date(now.getTime() - timeoutMs).toISOString();
|
|
@@ -2414,7 +4970,6 @@ export class MessageStore {
|
|
|
2414
4970
|
JOIN computers c ON c.active_connection_id = cc.id
|
|
2415
4971
|
WHERE cc.status = 'active' AND datetime(cc.last_heartbeat_at) < datetime(?)
|
|
2416
4972
|
`).all(cutoff) as Array<{ id: string; computer_id: string }>;
|
|
2417
|
-
if (staleRows.length === 0) return 0;
|
|
2418
4973
|
this.db.transaction(() => {
|
|
2419
4974
|
for (const row of staleRows) {
|
|
2420
4975
|
this.db.query(`
|
|
@@ -2427,7 +4982,103 @@ export class MessageStore {
|
|
|
2427
4982
|
SET status = 'offline', active_connection_id = NULL, updated_at = datetime('now')
|
|
2428
4983
|
WHERE id = ? AND active_connection_id = ?
|
|
2429
4984
|
`).run(row.computer_id, row.id);
|
|
4985
|
+
this.db.query(`
|
|
4986
|
+
UPDATE agent_runs
|
|
4987
|
+
SET status = 'killed',
|
|
4988
|
+
action = NULL,
|
|
4989
|
+
ended_at = COALESCE(ended_at, datetime('now')),
|
|
4990
|
+
updated_at = datetime('now'),
|
|
4991
|
+
output = CASE WHEN output = '' THEN 'computer connection heartbeat timed out' ELSE output END
|
|
4992
|
+
WHERE computer_id = ? AND status = 'running'
|
|
4993
|
+
`).run(row.computer_id);
|
|
4994
|
+
this.db.query(`
|
|
4995
|
+
UPDATE message_deliveries
|
|
4996
|
+
SET status = 'pending',
|
|
4997
|
+
daemon_id = NULL,
|
|
4998
|
+
connection_id = NULL,
|
|
4999
|
+
claim_token = NULL,
|
|
5000
|
+
lease_until = NULL,
|
|
5001
|
+
run_id = NULL,
|
|
5002
|
+
last_error = '',
|
|
5003
|
+
claimed_at = NULL
|
|
5004
|
+
WHERE status IN ('claimed', 'processing_completed')
|
|
5005
|
+
AND (
|
|
5006
|
+
connection_id = ?
|
|
5007
|
+
OR (connection_id IS NULL AND daemon_id = ?)
|
|
5008
|
+
)
|
|
5009
|
+
`).run(row.id, row.id);
|
|
2430
5010
|
}
|
|
5011
|
+
this.db.query(`
|
|
5012
|
+
UPDATE agent_runs
|
|
5013
|
+
SET status = 'killed',
|
|
5014
|
+
action = NULL,
|
|
5015
|
+
ended_at = COALESCE(ended_at, datetime('now')),
|
|
5016
|
+
updated_at = datetime('now'),
|
|
5017
|
+
output = CASE WHEN output = '' THEN 'computer connection is no longer active' ELSE output END
|
|
5018
|
+
WHERE status = 'running'
|
|
5019
|
+
AND (
|
|
5020
|
+
(
|
|
5021
|
+
connection_id IS NOT NULL
|
|
5022
|
+
AND NOT EXISTS (
|
|
5023
|
+
SELECT 1
|
|
5024
|
+
FROM computer_connections cc
|
|
5025
|
+
JOIN computers c ON c.active_connection_id = cc.id
|
|
5026
|
+
WHERE cc.id = agent_runs.connection_id
|
|
5027
|
+
AND cc.status = 'active'
|
|
5028
|
+
AND c.status = 'online'
|
|
5029
|
+
)
|
|
5030
|
+
)
|
|
5031
|
+
OR (
|
|
5032
|
+
connection_id IS NULL
|
|
5033
|
+
AND computer_id IS NOT NULL
|
|
5034
|
+
AND NOT EXISTS (
|
|
5035
|
+
SELECT 1
|
|
5036
|
+
FROM computers c
|
|
5037
|
+
JOIN computer_connections cc ON cc.id = c.active_connection_id
|
|
5038
|
+
WHERE c.id = agent_runs.computer_id
|
|
5039
|
+
AND c.status = 'online'
|
|
5040
|
+
AND cc.status = 'active'
|
|
5041
|
+
)
|
|
5042
|
+
)
|
|
5043
|
+
)
|
|
5044
|
+
`).run();
|
|
5045
|
+
this.db.query(`
|
|
5046
|
+
UPDATE message_deliveries
|
|
5047
|
+
SET status = 'pending',
|
|
5048
|
+
daemon_id = NULL,
|
|
5049
|
+
connection_id = NULL,
|
|
5050
|
+
claim_token = NULL,
|
|
5051
|
+
lease_until = NULL,
|
|
5052
|
+
run_id = NULL,
|
|
5053
|
+
last_error = '',
|
|
5054
|
+
claimed_at = NULL
|
|
5055
|
+
WHERE status IN ('claimed', 'processing_completed')
|
|
5056
|
+
AND (
|
|
5057
|
+
(
|
|
5058
|
+
connection_id IS NOT NULL
|
|
5059
|
+
AND NOT EXISTS (
|
|
5060
|
+
SELECT 1
|
|
5061
|
+
FROM computer_connections cc
|
|
5062
|
+
JOIN computers c ON c.active_connection_id = cc.id
|
|
5063
|
+
WHERE cc.id = message_deliveries.connection_id
|
|
5064
|
+
AND cc.status = 'active'
|
|
5065
|
+
AND c.status = 'online'
|
|
5066
|
+
)
|
|
5067
|
+
)
|
|
5068
|
+
OR (
|
|
5069
|
+
connection_id IS NULL
|
|
5070
|
+
AND daemon_id IN (SELECT id FROM computer_connections)
|
|
5071
|
+
AND NOT EXISTS (
|
|
5072
|
+
SELECT 1
|
|
5073
|
+
FROM computer_connections cc
|
|
5074
|
+
JOIN computers c ON c.active_connection_id = cc.id
|
|
5075
|
+
WHERE cc.id = message_deliveries.daemon_id
|
|
5076
|
+
AND cc.status = 'active'
|
|
5077
|
+
AND c.status = 'online'
|
|
5078
|
+
)
|
|
5079
|
+
)
|
|
5080
|
+
)
|
|
5081
|
+
`).run();
|
|
2431
5082
|
})();
|
|
2432
5083
|
return staleRows.length;
|
|
2433
5084
|
}
|
|
@@ -2517,6 +5168,114 @@ export class MessageStore {
|
|
|
2517
5168
|
return row !== null;
|
|
2518
5169
|
}
|
|
2519
5170
|
|
|
5171
|
+
listDaemonAgents(daemonId: string): string[] {
|
|
5172
|
+
const rows = this.db.query(`
|
|
5173
|
+
SELECT agent FROM daemon_agents
|
|
5174
|
+
WHERE daemon_id = ? AND status = 'online'
|
|
5175
|
+
ORDER BY agent ASC
|
|
5176
|
+
`).all(daemonId) as Array<{ agent: string }>;
|
|
5177
|
+
return rows.map((row) => row.agent);
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
listPendingDeliveryAgentsForConnection(connectionId: string): Array<{ agent: string; pending: number }> {
|
|
5181
|
+
this.releaseExpiredDeliveries();
|
|
5182
|
+
const rows = this.db.query(`
|
|
5183
|
+
SELECT md.agent AS agent, COUNT(*) AS pending
|
|
5184
|
+
FROM message_deliveries md
|
|
5185
|
+
JOIN daemon_agents da ON da.agent = md.agent
|
|
5186
|
+
JOIN computer_connections cc ON cc.id = da.daemon_id
|
|
5187
|
+
JOIN computers c ON c.active_connection_id = cc.id
|
|
5188
|
+
WHERE da.daemon_id = ?
|
|
5189
|
+
AND da.status = 'online'
|
|
5190
|
+
AND cc.status = 'active'
|
|
5191
|
+
AND c.status = 'online'
|
|
5192
|
+
AND md.status = 'pending'
|
|
5193
|
+
GROUP BY md.agent
|
|
5194
|
+
ORDER BY MIN(datetime(md.created_at)) ASC, md.agent ASC
|
|
5195
|
+
`).all(connectionId.trim()) as Array<{ agent: string; pending: number }>;
|
|
5196
|
+
return rows.map((row) => ({ agent: row.agent, pending: Number(row.pending) }));
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5199
|
+
listDeliveryBacklogForConnection(connectionId: string): DeliveryBacklogSummary[] {
|
|
5200
|
+
this.releaseExpiredDeliveries();
|
|
5201
|
+
const rows = this.db.query(`
|
|
5202
|
+
SELECT
|
|
5203
|
+
md.agent AS agent,
|
|
5204
|
+
SUM(CASE WHEN md.status = 'pending' THEN 1 ELSE 0 END) AS pending,
|
|
5205
|
+
SUM(CASE WHEN md.status = 'claimed' THEN 1 ELSE 0 END) AS claimed,
|
|
5206
|
+
SUM(CASE WHEN md.status = 'processing_completed' THEN 1 ELSE 0 END) AS processing_completed,
|
|
5207
|
+
SUM(CASE
|
|
5208
|
+
WHEN md.status IN ('claimed', 'processing_completed')
|
|
5209
|
+
AND md.lease_until IS NOT NULL
|
|
5210
|
+
AND datetime(md.lease_until) <= datetime('now')
|
|
5211
|
+
THEN 1 ELSE 0
|
|
5212
|
+
END) AS expired_active
|
|
5213
|
+
FROM message_deliveries md
|
|
5214
|
+
JOIN daemon_agents da ON da.agent = md.agent
|
|
5215
|
+
JOIN computer_connections cc ON cc.id = da.daemon_id
|
|
5216
|
+
JOIN computers c ON c.active_connection_id = cc.id
|
|
5217
|
+
WHERE da.daemon_id = ?
|
|
5218
|
+
AND da.status = 'online'
|
|
5219
|
+
AND cc.status = 'active'
|
|
5220
|
+
AND c.status = 'online'
|
|
5221
|
+
AND md.status IN ('pending', 'claimed', 'processing_completed')
|
|
5222
|
+
GROUP BY md.agent
|
|
5223
|
+
ORDER BY MIN(datetime(md.created_at)) ASC, md.agent ASC
|
|
5224
|
+
`).all(connectionId.trim()) as Array<{
|
|
5225
|
+
agent: string;
|
|
5226
|
+
pending: number;
|
|
5227
|
+
claimed: number;
|
|
5228
|
+
processing_completed: number;
|
|
5229
|
+
expired_active: number;
|
|
5230
|
+
}>;
|
|
5231
|
+
return rows.map((row) => ({
|
|
5232
|
+
agent: row.agent,
|
|
5233
|
+
pending: Number(row.pending),
|
|
5234
|
+
claimed: Number(row.claimed),
|
|
5235
|
+
processing_completed: Number(row.processing_completed),
|
|
5236
|
+
expired_active: Number(row.expired_active),
|
|
5237
|
+
}));
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
releaseExpiredDeliveries(now = new Date()): number {
|
|
5241
|
+
const result = this.db.query(`
|
|
5242
|
+
UPDATE message_deliveries
|
|
5243
|
+
SET status = 'pending',
|
|
5244
|
+
daemon_id = NULL,
|
|
5245
|
+
connection_id = NULL,
|
|
5246
|
+
claim_token = NULL,
|
|
5247
|
+
lease_until = NULL,
|
|
5248
|
+
run_id = NULL,
|
|
5249
|
+
last_error = '',
|
|
5250
|
+
claimed_at = NULL
|
|
5251
|
+
WHERE status IN ('claimed', 'processing_completed')
|
|
5252
|
+
AND lease_until IS NOT NULL
|
|
5253
|
+
AND datetime(lease_until) <= datetime(?)
|
|
5254
|
+
AND NOT EXISTS (
|
|
5255
|
+
SELECT 1
|
|
5256
|
+
FROM agent_runs r
|
|
5257
|
+
WHERE r.delivery_id = message_deliveries.id
|
|
5258
|
+
AND r.status = 'running'
|
|
5259
|
+
)
|
|
5260
|
+
`).run(now.toISOString());
|
|
5261
|
+
return result.changes;
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
listOnlineDaemonConnectionsForAgent(agent: string): string[] {
|
|
5265
|
+
const rows = this.db.query(`
|
|
5266
|
+
SELECT da.daemon_id AS connection_id
|
|
5267
|
+
FROM daemon_agents da
|
|
5268
|
+
JOIN computer_connections cc ON cc.id = da.daemon_id
|
|
5269
|
+
JOIN computers c ON c.active_connection_id = cc.id
|
|
5270
|
+
WHERE da.agent = ?
|
|
5271
|
+
AND da.status = 'online'
|
|
5272
|
+
AND cc.status = 'active'
|
|
5273
|
+
AND c.status = 'online'
|
|
5274
|
+
ORDER BY datetime(da.last_seen_at) DESC
|
|
5275
|
+
`).all(agent.trim()) as Array<{ connection_id: string }>;
|
|
5276
|
+
return rows.map((row) => row.connection_id);
|
|
5277
|
+
}
|
|
5278
|
+
|
|
2520
5279
|
hasDaemonAgent(agent: string): boolean {
|
|
2521
5280
|
const row = this.db.query(`
|
|
2522
5281
|
SELECT 1 FROM daemon_agents
|
|
@@ -2526,13 +5285,90 @@ export class MessageStore {
|
|
|
2526
5285
|
return row !== null;
|
|
2527
5286
|
}
|
|
2528
5287
|
|
|
2529
|
-
listDeliveries(agent: string, status = 'pending', limit = 50): MessageDelivery[] {
|
|
5288
|
+
listDeliveries(agent: string, status = 'pending', limit = 50, options: { distinctChat?: boolean; excludeRunningComputerId?: string | null; connectionId?: string | null } = {}): MessageDelivery[] {
|
|
5289
|
+
if (status === 'pending') this.releaseExpiredDeliveries();
|
|
5290
|
+
const excludeRunningComputerId = options.excludeRunningComputerId?.trim() || null;
|
|
5291
|
+
const rows = options.distinctChat
|
|
5292
|
+
? excludeRunningComputerId
|
|
5293
|
+
? this.db.query(`
|
|
5294
|
+
SELECT md.*
|
|
5295
|
+
FROM message_deliveries md
|
|
5296
|
+
INNER JOIN (
|
|
5297
|
+
SELECT pending.chat_id, MIN(pending.rowid) AS first_rowid
|
|
5298
|
+
FROM message_deliveries pending
|
|
5299
|
+
WHERE pending.agent = ? AND pending.status = ?
|
|
5300
|
+
AND NOT EXISTS (
|
|
5301
|
+
SELECT 1
|
|
5302
|
+
FROM agent_runs ar
|
|
5303
|
+
WHERE ar.computer_id = ?
|
|
5304
|
+
AND ar.agent = pending.agent
|
|
5305
|
+
AND ar.status = 'running'
|
|
5306
|
+
AND EXISTS (
|
|
5307
|
+
SELECT 1
|
|
5308
|
+
FROM messages running_message
|
|
5309
|
+
WHERE running_message.id = ar.message_id
|
|
5310
|
+
AND running_message.chat_id = pending.chat_id
|
|
5311
|
+
)
|
|
5312
|
+
)
|
|
5313
|
+
GROUP BY pending.chat_id
|
|
5314
|
+
) first ON md.rowid = first.first_rowid
|
|
5315
|
+
WHERE md.agent = ? AND md.status = ?
|
|
5316
|
+
ORDER BY datetime(md.created_at) ASC, md.rowid ASC
|
|
5317
|
+
LIMIT ?
|
|
5318
|
+
`).all(agent, status, excludeRunningComputerId, agent, status, limitValue(limit, 50)) as Record<string, unknown>[]
|
|
5319
|
+
: this.db.query(`
|
|
5320
|
+
SELECT md.*
|
|
5321
|
+
FROM message_deliveries md
|
|
5322
|
+
INNER JOIN (
|
|
5323
|
+
SELECT chat_id, MIN(rowid) AS first_rowid
|
|
5324
|
+
FROM message_deliveries
|
|
5325
|
+
WHERE agent = ? AND status = ?
|
|
5326
|
+
GROUP BY chat_id
|
|
5327
|
+
) first ON md.rowid = first.first_rowid
|
|
5328
|
+
WHERE md.agent = ? AND md.status = ?
|
|
5329
|
+
ORDER BY datetime(md.created_at) ASC, md.rowid ASC
|
|
5330
|
+
LIMIT ?
|
|
5331
|
+
`).all(agent, status, agent, status, limitValue(limit, 50)) as Record<string, unknown>[]
|
|
5332
|
+
: this.db.query(`
|
|
5333
|
+
SELECT * FROM message_deliveries
|
|
5334
|
+
WHERE agent = ? AND status = ?
|
|
5335
|
+
ORDER BY datetime(created_at) ASC, rowid ASC
|
|
5336
|
+
LIMIT ?
|
|
5337
|
+
`).all(agent, status, limitValue(limit, 50)) as Record<string, unknown>[];
|
|
5338
|
+
return rows.map(rowToDelivery);
|
|
5339
|
+
}
|
|
5340
|
+
|
|
5341
|
+
listRecentDeliveriesForAgent(agent: string, limit = 20): MessageDelivery[] {
|
|
5342
|
+
this.releaseExpiredDeliveries();
|
|
2530
5343
|
const rows = this.db.query(`
|
|
2531
|
-
SELECT *
|
|
2532
|
-
|
|
2533
|
-
|
|
5344
|
+
SELECT *
|
|
5345
|
+
FROM message_deliveries
|
|
5346
|
+
WHERE agent = ?
|
|
5347
|
+
ORDER BY datetime(created_at) DESC, rowid DESC
|
|
5348
|
+
LIMIT ?
|
|
5349
|
+
`).all(agent.trim(), limitValue(limit, 20)) as Record<string, unknown>[];
|
|
5350
|
+
return rows.map(rowToDelivery);
|
|
5351
|
+
}
|
|
5352
|
+
|
|
5353
|
+
listActiveDeliveriesForComputer(computerId: string, limit = 20): MessageDelivery[] {
|
|
5354
|
+
this.releaseExpiredDeliveries();
|
|
5355
|
+
const rows = this.db.query(`
|
|
5356
|
+
SELECT md.*, c.name AS chat_name
|
|
5357
|
+
FROM message_deliveries md
|
|
5358
|
+
JOIN chats c ON c.id = md.chat_id
|
|
5359
|
+
WHERE md.status IN ('pending', 'claimed', 'processing_completed')
|
|
5360
|
+
AND (
|
|
5361
|
+
md.connection_id IN (SELECT id FROM computer_connections WHERE computer_id = ?)
|
|
5362
|
+
OR md.daemon_id IN (SELECT id FROM computer_connections WHERE computer_id = ?)
|
|
5363
|
+
OR (md.status = 'pending' AND md.agent IN (
|
|
5364
|
+
SELECT agent
|
|
5365
|
+
FROM computer_agent_assignments
|
|
5366
|
+
WHERE computer_id = ? AND status = 'active'
|
|
5367
|
+
))
|
|
5368
|
+
)
|
|
5369
|
+
ORDER BY datetime(md.created_at) DESC, md.rowid DESC
|
|
2534
5370
|
LIMIT ?
|
|
2535
|
-
`).all(
|
|
5371
|
+
`).all(computerId.trim(), computerId.trim(), computerId.trim(), limitValue(limit, 20)) as Record<string, unknown>[];
|
|
2536
5372
|
return rows.map(rowToDelivery);
|
|
2537
5373
|
}
|
|
2538
5374
|
|
|
@@ -2664,11 +5500,22 @@ export class MessageStore {
|
|
|
2664
5500
|
}
|
|
2665
5501
|
|
|
2666
5502
|
canSendWebMessageToRoom(input: { room: Chat; sender: string }): boolean {
|
|
5503
|
+
if (input.room.status === 'archived') return false;
|
|
2667
5504
|
if (input.room.provider === 'web') return true;
|
|
2668
5505
|
if (!this.getAgent(input.sender)) return false;
|
|
2669
5506
|
if (input.room.provider === 'lark') {
|
|
2670
|
-
const
|
|
2671
|
-
|
|
5507
|
+
const row = this.db.query(`
|
|
5508
|
+
SELECT 1
|
|
5509
|
+
FROM channel_conversations cc
|
|
5510
|
+
JOIN channel_accounts ca ON ca.id = cc.channel_account_id
|
|
5511
|
+
WHERE cc.lock_chat_id = ?
|
|
5512
|
+
AND cc.audit_only = 0
|
|
5513
|
+
AND ca.channel = 'lark'
|
|
5514
|
+
AND ca.status = 'active'
|
|
5515
|
+
AND ca.agent = ?
|
|
5516
|
+
LIMIT 1
|
|
5517
|
+
`).get(input.room.id, input.sender) as Record<string, unknown> | null;
|
|
5518
|
+
return row !== null;
|
|
2672
5519
|
}
|
|
2673
5520
|
return false;
|
|
2674
5521
|
}
|
|
@@ -2752,6 +5599,17 @@ export class MessageStore {
|
|
|
2752
5599
|
return rows.map(rowToSession);
|
|
2753
5600
|
}
|
|
2754
5601
|
|
|
5602
|
+
listSessionsForAgent(agent: string, limit = 10): AgentSession[] {
|
|
5603
|
+
const rows = this.db.query(`
|
|
5604
|
+
SELECT *
|
|
5605
|
+
FROM agent_sessions
|
|
5606
|
+
WHERE agent = ?
|
|
5607
|
+
ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC
|
|
5608
|
+
LIMIT ?
|
|
5609
|
+
`).all(agent.trim(), limitValue(limit, 10)) as Record<string, unknown>[];
|
|
5610
|
+
return rows.map(rowToSession);
|
|
5611
|
+
}
|
|
5612
|
+
|
|
2755
5613
|
listRunsForSession(sessionId: string, limit = 50): AgentRun[] {
|
|
2756
5614
|
const rows = this.db.query(`
|
|
2757
5615
|
SELECT * FROM agent_runs
|
|
@@ -2762,7 +5620,108 @@ export class MessageStore {
|
|
|
2762
5620
|
return rows.map(rowToRun);
|
|
2763
5621
|
}
|
|
2764
5622
|
|
|
5623
|
+
recordAgentActivity(input: RecordAgentActivityInput): AgentActivityEvent {
|
|
5624
|
+
const run = this.getRun(input.runId);
|
|
5625
|
+
if (!run) throw new Error(`run ${input.runId} was not found`);
|
|
5626
|
+
const message = this.getMessage(run.message_id);
|
|
5627
|
+
if (!message) throw new Error(`message ${run.message_id} was not found`);
|
|
5628
|
+
const id = crypto.randomUUID();
|
|
5629
|
+
this.db.query(`
|
|
5630
|
+
INSERT INTO agent_activity_events (id, run_id, session_id, agent, room_id, kind, title, detail, metadata)
|
|
5631
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5632
|
+
`).run(
|
|
5633
|
+
id,
|
|
5634
|
+
run.id,
|
|
5635
|
+
run.session_id,
|
|
5636
|
+
run.agent,
|
|
5637
|
+
message.chat_id,
|
|
5638
|
+
input.kind,
|
|
5639
|
+
input.title.trim().slice(0, 160) || input.kind,
|
|
5640
|
+
(input.detail ?? '').trim().slice(0, 4000),
|
|
5641
|
+
JSON.stringify(input.metadata ?? {}),
|
|
5642
|
+
);
|
|
5643
|
+
return this.getAgentActivityEvent(id)!;
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5646
|
+
getAgentActivityEvent(id: string): AgentActivityEvent | null {
|
|
5647
|
+
const row = this.db.query('SELECT * FROM agent_activity_events WHERE id = ?').get(id) as Record<string, unknown> | null;
|
|
5648
|
+
return row ? rowToActivityEvent(row) : null;
|
|
5649
|
+
}
|
|
5650
|
+
|
|
5651
|
+
listActivityForRun(runId: string, limit = 100): AgentActivityEvent[] {
|
|
5652
|
+
const rows = this.db.query(`
|
|
5653
|
+
SELECT * FROM agent_activity_events
|
|
5654
|
+
WHERE run_id = ?
|
|
5655
|
+
ORDER BY datetime(created_at) ASC, rowid ASC
|
|
5656
|
+
LIMIT ?
|
|
5657
|
+
`).all(runId, limitValue(limit, 100)) as Record<string, unknown>[];
|
|
5658
|
+
return rows.map(rowToActivityEvent);
|
|
5659
|
+
}
|
|
5660
|
+
|
|
5661
|
+
listActivityForAgent(agent: string, limit = 100): AgentActivityEvent[] {
|
|
5662
|
+
const rows = this.db.query(`
|
|
5663
|
+
SELECT * FROM agent_activity_events
|
|
5664
|
+
WHERE agent = ?
|
|
5665
|
+
ORDER BY datetime(created_at) DESC, rowid DESC
|
|
5666
|
+
LIMIT ?
|
|
5667
|
+
`).all(agent.trim(), limitValue(limit, 100)) as Record<string, unknown>[];
|
|
5668
|
+
return rows.map(rowToActivityEvent);
|
|
5669
|
+
}
|
|
5670
|
+
|
|
5671
|
+
listActivityForRoomAgent(roomId: string, agent: string, limit = 100): AgentActivityEvent[] {
|
|
5672
|
+
const room = this.getChatById(roomId);
|
|
5673
|
+
if (!room) return [];
|
|
5674
|
+
const rows = this.db.query(`
|
|
5675
|
+
SELECT *
|
|
5676
|
+
FROM agent_activity_events
|
|
5677
|
+
WHERE room_id = ? AND agent = ?
|
|
5678
|
+
ORDER BY datetime(created_at) DESC, rowid DESC
|
|
5679
|
+
LIMIT ?
|
|
5680
|
+
`).all(room.id, agent.trim(), limitValue(limit, 100)) as Record<string, unknown>[];
|
|
5681
|
+
return rows.map(rowToActivityEvent);
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5684
|
+
listRoomAgentActivity(roomId: string): RoomAgentActivityStatus[] {
|
|
5685
|
+
const room = this.getChatById(roomId);
|
|
5686
|
+
if (!room) return [];
|
|
5687
|
+
const rows = this.db.query(`
|
|
5688
|
+
SELECT
|
|
5689
|
+
ar.id AS run_id,
|
|
5690
|
+
ar.agent,
|
|
5691
|
+
COALESCE(ae.kind, 'running') AS kind,
|
|
5692
|
+
COALESCE(ae.title, 'Working') AS title,
|
|
5693
|
+
COALESCE(ae.created_at, ar.updated_at) AS updated_at
|
|
5694
|
+
FROM agent_runs ar
|
|
5695
|
+
INNER JOIN messages m ON m.id = ar.message_id
|
|
5696
|
+
LEFT JOIN agent_activity_events ae ON ae.id = (
|
|
5697
|
+
SELECT latest.id
|
|
5698
|
+
FROM agent_activity_events latest
|
|
5699
|
+
WHERE latest.run_id = ar.id AND latest.room_id = m.chat_id
|
|
5700
|
+
ORDER BY datetime(latest.created_at) DESC, latest.rowid DESC
|
|
5701
|
+
LIMIT 1
|
|
5702
|
+
)
|
|
5703
|
+
WHERE m.chat_id = ? AND ar.status = 'running'
|
|
5704
|
+
AND (
|
|
5705
|
+
ae.id IS NULL
|
|
5706
|
+
OR NOT (
|
|
5707
|
+
ae.kind = 'lifecycle'
|
|
5708
|
+
AND lower(trim(ae.title)) IN ('step completed', 'runtime completed', 'run completed')
|
|
5709
|
+
)
|
|
5710
|
+
)
|
|
5711
|
+
ORDER BY datetime(updated_at) DESC, ar.agent ASC
|
|
5712
|
+
`).all(room.id) as Record<string, unknown>[];
|
|
5713
|
+
return rows.map((row) => ({
|
|
5714
|
+
agent: String(row.agent),
|
|
5715
|
+
run_id: String(row.run_id),
|
|
5716
|
+
status: 'working',
|
|
5717
|
+
kind: String(row.kind) as RoomAgentActivityStatus['kind'],
|
|
5718
|
+
title: String(row.title),
|
|
5719
|
+
updated_at: String(row.updated_at),
|
|
5720
|
+
}));
|
|
5721
|
+
}
|
|
5722
|
+
|
|
2765
5723
|
claimDelivery(id: string, input: ClaimDeliveryInput): MessageDelivery {
|
|
5724
|
+
this.releaseExpiredDeliveries();
|
|
2766
5725
|
const claimToken = crypto.randomUUID();
|
|
2767
5726
|
const leaseMs = input.leaseMs ?? 30_000;
|
|
2768
5727
|
const leaseUntil = new Date(Date.now() + leaseMs).toISOString();
|
|
@@ -2770,13 +5729,19 @@ export class MessageStore {
|
|
|
2770
5729
|
if (!delivery) throw new Error(`pending delivery ${id} was not found`);
|
|
2771
5730
|
if (input.computerId) {
|
|
2772
5731
|
const busy = this.db.query(`
|
|
2773
|
-
SELECT
|
|
5732
|
+
SELECT r.id, r.connection_id
|
|
2774
5733
|
FROM agent_runs r
|
|
2775
5734
|
JOIN agent_sessions s ON s.id = r.session_id
|
|
2776
5735
|
WHERE r.computer_id = ? AND r.agent = ? AND s.chat_id = ? AND r.status = 'running'
|
|
2777
5736
|
LIMIT 1
|
|
2778
5737
|
`).get(input.computerId, delivery.agent, delivery.chat_id) as Record<string, unknown> | null;
|
|
2779
|
-
if (busy)
|
|
5738
|
+
if (busy) {
|
|
5739
|
+
const matchesSteerRun = input.steerRunId && String(busy.id) === input.steerRunId;
|
|
5740
|
+
const matchesConnection = !input.connectionId || !busy.connection_id || String(busy.connection_id) === input.connectionId;
|
|
5741
|
+
if (!matchesSteerRun || !matchesConnection) {
|
|
5742
|
+
throw new Error(`runtime invocation already running for computer ${input.computerId} agent ${delivery.agent} chat ${delivery.chat_id}`);
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
2780
5745
|
}
|
|
2781
5746
|
const connectionId = input.connectionId ?? null;
|
|
2782
5747
|
const result = this.db.query(`
|
|
@@ -2900,6 +5865,25 @@ export class MessageStore {
|
|
|
2900
5865
|
return this.getRun(runId)!;
|
|
2901
5866
|
}
|
|
2902
5867
|
|
|
5868
|
+
requestRoomAgentRunAction(input: { roomId: string; agent: string; action: RunAction }): AgentRun {
|
|
5869
|
+
const room = this.getChatById(input.roomId);
|
|
5870
|
+
if (!room) throw new Error(`room ${input.roomId} was not found`);
|
|
5871
|
+
const agent = input.agent.trim();
|
|
5872
|
+
if (!agent) throw new Error('agent is required');
|
|
5873
|
+
const row = this.db.query(`
|
|
5874
|
+
SELECT ar.*
|
|
5875
|
+
FROM agent_runs ar
|
|
5876
|
+
JOIN messages m ON m.id = ar.message_id
|
|
5877
|
+
WHERE ar.status = 'running'
|
|
5878
|
+
AND ar.agent = ?
|
|
5879
|
+
AND m.chat_id = ?
|
|
5880
|
+
ORDER BY datetime(ar.updated_at) DESC, datetime(ar.started_at) DESC, ar.rowid DESC
|
|
5881
|
+
LIMIT 1
|
|
5882
|
+
`).get(agent, room.id) as Record<string, unknown> | null;
|
|
5883
|
+
if (!row) throw new Error(`running run for agent ${agent} in room ${room.id} was not found`);
|
|
5884
|
+
return this.requestRunAction(String(row.id), input.action);
|
|
5885
|
+
}
|
|
5886
|
+
|
|
2903
5887
|
clearRunAction(runId: string): void {
|
|
2904
5888
|
this.db.query(`
|
|
2905
5889
|
UPDATE agent_runs
|
|
@@ -2922,6 +5906,28 @@ export class MessageStore {
|
|
|
2922
5906
|
return rows.map(rowToRun);
|
|
2923
5907
|
}
|
|
2924
5908
|
|
|
5909
|
+
listRunsForAgent(agent: string, limit = 10): AgentRun[] {
|
|
5910
|
+
const rows = this.db.query(`
|
|
5911
|
+
SELECT *
|
|
5912
|
+
FROM agent_runs
|
|
5913
|
+
WHERE agent = ?
|
|
5914
|
+
ORDER BY datetime(updated_at) DESC, datetime(started_at) DESC
|
|
5915
|
+
LIMIT ?
|
|
5916
|
+
`).all(agent.trim(), limitValue(limit, 10)) as Record<string, unknown>[];
|
|
5917
|
+
return rows.map(rowToRun);
|
|
5918
|
+
}
|
|
5919
|
+
|
|
5920
|
+
listRunsForComputer(computerId: string, limit = 20): AgentRun[] {
|
|
5921
|
+
const rows = this.db.query(`
|
|
5922
|
+
SELECT *
|
|
5923
|
+
FROM agent_runs
|
|
5924
|
+
WHERE computer_id = ?
|
|
5925
|
+
ORDER BY datetime(updated_at) DESC, datetime(started_at) DESC
|
|
5926
|
+
LIMIT ?
|
|
5927
|
+
`).all(computerId.trim(), limitValue(limit, 20)) as Record<string, unknown>[];
|
|
5928
|
+
return rows.map(rowToRun);
|
|
5929
|
+
}
|
|
5930
|
+
|
|
2925
5931
|
createArtifact(input: CreateArtifactInput): { artifact: ArtifactMetadata; token: string } {
|
|
2926
5932
|
validateArtifactContent(input.mimeType, input.content);
|
|
2927
5933
|
const id = crypto.randomUUID();
|