@arinova-ai/agent-sdk 0.0.15 → 0.0.16-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const DEFAULT_RECONNECT_INTERVAL = 5_000;
2
2
  const DEFAULT_PING_INTERVAL = 30_000;
3
3
  const TASK_HEARTBEAT_INTERVAL = 60_000;
4
+ const MAX_QUEUE_SIZE = 10;
4
5
  export class ArinovaAgent {
5
6
  serverUrl;
6
7
  botToken;
@@ -9,10 +10,14 @@ export class ArinovaAgent {
9
10
  pingInterval;
10
11
  ws = null;
11
12
  pingTimer = null;
13
+ commandHeartbeatTimer = null;
12
14
  reconnectTimer = null;
13
15
  stopped = false;
16
+ agentId = null;
14
17
  taskHandler = null;
15
18
  taskAbortControllers = new Map();
19
+ activeConversationTasks = new Map(); // conversationId → taskId
20
+ conversationQueues = new Map(); // conversationId → queued task data
16
21
  listeners = {
17
22
  connected: [],
18
23
  disconnected: [],
@@ -69,7 +74,7 @@ export class ArinovaAgent {
69
74
  const httpUrl = this.serverUrl
70
75
  .replace(/^ws:/, "http:")
71
76
  .replace(/^wss:/, "https:");
72
- const res = await fetch(`${httpUrl}/api/agent/send`, {
77
+ const res = await fetch(`${httpUrl}/api/v1/messages/send`, {
73
78
  method: "POST",
74
79
  headers: {
75
80
  "Authorization": `Bearer ${this.botToken}`,
@@ -82,6 +87,20 @@ export class ArinovaAgent {
82
87
  throw new Error(`sendMessage failed (${res.status}): ${body}`);
83
88
  }
84
89
  }
90
+ /**
91
+ * Send a telemetry event to the server.
92
+ * Silently no-ops if WebSocket is not connected.
93
+ */
94
+ sendTelemetry(event, data) {
95
+ this.send({ type: "agent_telemetry", event, data });
96
+ }
97
+ /**
98
+ * Send HUD data to the server for display in the office HUD bar.
99
+ * The server forwards this to the agent owner's frontend.
100
+ */
101
+ sendHud(data) {
102
+ this.send({ type: "hud_update", data });
103
+ }
85
104
  emit(event, ...args) {
86
105
  for (const listener of this.listeners[event] ?? []) {
87
106
  listener(...args);
@@ -97,6 +116,10 @@ export class ArinovaAgent {
97
116
  clearInterval(this.pingTimer);
98
117
  this.pingTimer = null;
99
118
  }
119
+ if (this.commandHeartbeatTimer) {
120
+ clearInterval(this.commandHeartbeatTimer);
121
+ this.commandHeartbeatTimer = null;
122
+ }
100
123
  if (this.reconnectTimer) {
101
124
  clearTimeout(this.reconnectTimer);
102
125
  this.reconnectTimer = null;
@@ -108,6 +131,14 @@ export class ArinovaAgent {
108
131
  catch { }
109
132
  this.ws = null;
110
133
  }
134
+ // Clear queues BEFORE aborting — abort triggers markFinished → processNextTask,
135
+ // which would dequeue and start tasks during disconnect if queues aren't empty.
136
+ this.conversationQueues.clear();
137
+ this.activeConversationTasks.clear();
138
+ for (const controller of this.taskAbortControllers.values()) {
139
+ controller.abort();
140
+ }
141
+ this.taskAbortControllers.clear();
111
142
  }
112
143
  scheduleReconnect() {
113
144
  if (this.stopped)
@@ -145,7 +176,27 @@ export class ArinovaAgent {
145
176
  try {
146
177
  const data = JSON.parse(String(event.data));
147
178
  if (data.type === "auth_ok") {
179
+ this.agentId = data.agentId ?? null;
148
180
  this.emit("connected");
181
+ // Register SDK runtime commands from skills
182
+ if (this.skills.length > 0 && this.agentId) {
183
+ this.send({
184
+ type: "register_commands",
185
+ agentId: this.agentId,
186
+ commands: this.skills.map((s) => ({
187
+ name: s.id ?? s.name,
188
+ description: s.description ?? "",
189
+ })),
190
+ });
191
+ }
192
+ // Start heartbeat to extend Redis TTL every 60s
193
+ if (this.commandHeartbeatTimer)
194
+ clearInterval(this.commandHeartbeatTimer);
195
+ if (this.skills.length > 0 && this.agentId) {
196
+ this.commandHeartbeatTimer = setInterval(() => {
197
+ this.send({ type: "heartbeat_commands", agentId: this.agentId });
198
+ }, 60_000);
199
+ }
149
200
  // Resolve the connect() promise on first successful auth
150
201
  if (this.connectResolve) {
151
202
  this.connectResolve();
@@ -177,6 +228,17 @@ export class ArinovaAgent {
177
228
  }
178
229
  if (data.type === "cancel_task") {
179
230
  const taskId = data.taskId;
231
+ // Check if the task is still queued (not yet started)
232
+ for (const [convId, queue] of this.conversationQueues) {
233
+ const idx = queue.findIndex((t) => t.taskId === taskId);
234
+ if (idx !== -1) {
235
+ queue.splice(idx, 1);
236
+ if (queue.length === 0)
237
+ this.conversationQueues.delete(convId);
238
+ return;
239
+ }
240
+ }
241
+ // Active task — abort it (processNextTask will be called via markFinished)
180
242
  const controller = this.taskAbortControllers.get(taskId);
181
243
  if (controller) {
182
244
  controller.abort();
@@ -215,7 +277,7 @@ export class ArinovaAgent {
215
277
  formData.append("conversationId", conversationId);
216
278
  const blob = new Blob([new Uint8Array(file)], { type: mime });
217
279
  formData.append("file", blob, fileName);
218
- const res = await fetch(`${httpUrl}/api/agent/upload`, {
280
+ const res = await fetch(`${httpUrl}/api/v1/files/upload`, {
219
281
  method: "POST",
220
282
  headers: {
221
283
  Authorization: `Bearer ${this.botToken}`,
@@ -247,7 +309,7 @@ export class ArinovaAgent {
247
309
  if (options?.limit != null)
248
310
  params.set("limit", String(options.limit));
249
311
  const qs = params.toString();
250
- const url = `${httpUrl}/api/agent/messages/${conversationId}${qs ? `?${qs}` : ""}`;
312
+ const url = `${httpUrl}/api/v1/messages/${conversationId}${qs ? `?${qs}` : ""}`;
251
313
  const res = await fetch(url, {
252
314
  method: "GET",
253
315
  headers: {
@@ -274,8 +336,12 @@ export class ArinovaAgent {
274
336
  params.set("before", options.before);
275
337
  if (options?.limit != null)
276
338
  params.set("limit", String(options.limit));
339
+ if (options?.tags?.length)
340
+ params.set("tags", options.tags.join(","));
341
+ if (options?.archived)
342
+ params.set("archived", "true");
277
343
  const qs = params.toString();
278
- const url = `${httpUrl}/api/agent/conversations/${conversationId}/notes${qs ? `?${qs}` : ""}`;
344
+ const url = `${httpUrl}/api/v1/notes${qs ? `?${qs}` : ""}`;
279
345
  const res = await fetch(url, {
280
346
  method: "GET",
281
347
  headers: { Authorization: `Bearer ${this.botToken}` },
@@ -295,7 +361,7 @@ export class ArinovaAgent {
295
361
  const httpUrl = this.serverUrl
296
362
  .replace(/^ws:/, "http:")
297
363
  .replace(/^wss:/, "https:");
298
- const res = await fetch(`${httpUrl}/api/agent/conversations/${conversationId}/notes`, {
364
+ const res = await fetch(`${httpUrl}/api/v1/notes`, {
299
365
  method: "POST",
300
366
  headers: {
301
367
  Authorization: `Bearer ${this.botToken}`,
@@ -319,7 +385,7 @@ export class ArinovaAgent {
319
385
  const httpUrl = this.serverUrl
320
386
  .replace(/^ws:/, "http:")
321
387
  .replace(/^wss:/, "https:");
322
- const res = await fetch(`${httpUrl}/api/agent/conversations/${conversationId}/notes/${noteId}`, {
388
+ const res = await fetch(`${httpUrl}/api/v1/notes/${noteId}`, {
323
389
  method: "PATCH",
324
390
  headers: {
325
391
  Authorization: `Bearer ${this.botToken}`,
@@ -342,7 +408,7 @@ export class ArinovaAgent {
342
408
  const httpUrl = this.serverUrl
343
409
  .replace(/^ws:/, "http:")
344
410
  .replace(/^wss:/, "https:");
345
- const res = await fetch(`${httpUrl}/api/agent/conversations/${conversationId}/notes/${noteId}`, {
411
+ const res = await fetch(`${httpUrl}/api/v1/notes/${noteId}`, {
346
412
  method: "DELETE",
347
413
  headers: { Authorization: `Bearer ${this.botToken}` },
348
414
  });
@@ -351,12 +417,621 @@ export class ArinovaAgent {
351
417
  throw new Error(`deleteNote failed (${res.status}): ${text}`);
352
418
  }
353
419
  }
420
+ // ── Kanban API ────────────────────────────────────────────────
421
+ /**
422
+ * List the owner's kanban boards.
423
+ * Returns an array of boards with id, name, and createdAt.
424
+ */
425
+ async listBoards() {
426
+ const httpUrl = this.serverUrl
427
+ .replace(/^ws:/, "http:")
428
+ .replace(/^wss:/, "https:");
429
+ const res = await fetch(`${httpUrl}/api/v1/kanban/boards`, {
430
+ method: "GET",
431
+ headers: { Authorization: `Bearer ${this.botToken}` },
432
+ });
433
+ if (!res.ok) {
434
+ const body = await res.text();
435
+ throw new Error(`listBoards failed (${res.status}): ${body}`);
436
+ }
437
+ return res.json();
438
+ }
439
+ /**
440
+ * Create a kanban card on the owner's board.
441
+ * The card is automatically assigned to the calling agent.
442
+ * @param body - Card title and optional description, priority, column.
443
+ */
444
+ async createCard(body) {
445
+ const httpUrl = this.serverUrl
446
+ .replace(/^ws:/, "http:")
447
+ .replace(/^wss:/, "https:");
448
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards`, {
449
+ method: "POST",
450
+ headers: {
451
+ Authorization: `Bearer ${this.botToken}`,
452
+ "Content-Type": "application/json",
453
+ },
454
+ body: JSON.stringify(body),
455
+ });
456
+ if (!res.ok) {
457
+ const text = await res.text();
458
+ throw new Error(`createCard failed (${res.status}): ${text}`);
459
+ }
460
+ return res.json();
461
+ }
462
+ /**
463
+ * Update a kanban card.
464
+ * @param cardId - The card ID to update.
465
+ * @param body - Fields to update (title, description, priority, columnId, sortOrder).
466
+ */
467
+ async updateCard(cardId, body) {
468
+ const httpUrl = this.serverUrl
469
+ .replace(/^ws:/, "http:")
470
+ .replace(/^wss:/, "https:");
471
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}`, {
472
+ method: "PATCH",
473
+ headers: {
474
+ Authorization: `Bearer ${this.botToken}`,
475
+ "Content-Type": "application/json",
476
+ },
477
+ body: JSON.stringify(body),
478
+ });
479
+ if (!res.ok) {
480
+ const text = await res.text();
481
+ throw new Error(`updateCard failed (${res.status}): ${text}`);
482
+ }
483
+ return res.json();
484
+ }
485
+ /**
486
+ * Create a new kanban board.
487
+ * @param body - Board name and optional initial columns.
488
+ */
489
+ async createBoard(body) {
490
+ const httpUrl = this.serverUrl
491
+ .replace(/^ws:/, "http:")
492
+ .replace(/^wss:/, "https:");
493
+ const res = await fetch(`${httpUrl}/api/v1/kanban/boards`, {
494
+ method: "POST",
495
+ headers: {
496
+ Authorization: `Bearer ${this.botToken}`,
497
+ "Content-Type": "application/json",
498
+ },
499
+ body: JSON.stringify(body),
500
+ });
501
+ if (!res.ok) {
502
+ const text = await res.text();
503
+ throw new Error(`createBoard failed (${res.status}): ${text}`);
504
+ }
505
+ return res.json();
506
+ }
507
+ /**
508
+ * Update a kanban board.
509
+ * @param boardId - The board ID to update.
510
+ * @param body - Fields to update.
511
+ */
512
+ async updateBoard(boardId, body) {
513
+ const httpUrl = this.serverUrl
514
+ .replace(/^ws:/, "http:")
515
+ .replace(/^wss:/, "https:");
516
+ const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}`, {
517
+ method: "PATCH",
518
+ headers: {
519
+ Authorization: `Bearer ${this.botToken}`,
520
+ "Content-Type": "application/json",
521
+ },
522
+ body: JSON.stringify(body),
523
+ });
524
+ if (!res.ok) {
525
+ const text = await res.text();
526
+ throw new Error(`updateBoard failed (${res.status}): ${text}`);
527
+ }
528
+ return res.json();
529
+ }
530
+ /**
531
+ * Archive a kanban board.
532
+ * @param boardId - The board ID to archive.
533
+ */
534
+ async archiveBoard(boardId) {
535
+ const httpUrl = this.serverUrl
536
+ .replace(/^ws:/, "http:")
537
+ .replace(/^wss:/, "https:");
538
+ const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/archive`, {
539
+ method: "POST",
540
+ headers: { Authorization: `Bearer ${this.botToken}` },
541
+ });
542
+ if (!res.ok) {
543
+ const text = await res.text();
544
+ throw new Error(`archiveBoard failed (${res.status}): ${text}`);
545
+ }
546
+ }
547
+ /**
548
+ * List columns for a board.
549
+ * @param boardId - The board ID.
550
+ */
551
+ async listColumns(boardId) {
552
+ const httpUrl = this.serverUrl
553
+ .replace(/^ws:/, "http:")
554
+ .replace(/^wss:/, "https:");
555
+ const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/columns`, {
556
+ method: "GET",
557
+ headers: { Authorization: `Bearer ${this.botToken}` },
558
+ });
559
+ if (!res.ok) {
560
+ const text = await res.text();
561
+ throw new Error(`listColumns failed (${res.status}): ${text}`);
562
+ }
563
+ return res.json();
564
+ }
565
+ /**
566
+ * Create a column in a board.
567
+ * @param boardId - The board ID.
568
+ * @param body - Column name and optional sort order.
569
+ */
570
+ async createColumn(boardId, body) {
571
+ const httpUrl = this.serverUrl
572
+ .replace(/^ws:/, "http:")
573
+ .replace(/^wss:/, "https:");
574
+ const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/columns`, {
575
+ method: "POST",
576
+ headers: {
577
+ Authorization: `Bearer ${this.botToken}`,
578
+ "Content-Type": "application/json",
579
+ },
580
+ body: JSON.stringify(body),
581
+ });
582
+ if (!res.ok) {
583
+ const text = await res.text();
584
+ throw new Error(`createColumn failed (${res.status}): ${text}`);
585
+ }
586
+ return res.json();
587
+ }
588
+ /**
589
+ * Update a column.
590
+ * @param columnId - The column ID to update.
591
+ * @param body - Fields to update (name, sortOrder).
592
+ */
593
+ async updateColumn(columnId, body) {
594
+ const httpUrl = this.serverUrl
595
+ .replace(/^ws:/, "http:")
596
+ .replace(/^wss:/, "https:");
597
+ const res = await fetch(`${httpUrl}/api/v1/kanban/columns/${columnId}`, {
598
+ method: "PATCH",
599
+ headers: {
600
+ Authorization: `Bearer ${this.botToken}`,
601
+ "Content-Type": "application/json",
602
+ },
603
+ body: JSON.stringify(body),
604
+ });
605
+ if (!res.ok) {
606
+ const text = await res.text();
607
+ throw new Error(`updateColumn failed (${res.status}): ${text}`);
608
+ }
609
+ return res.json();
610
+ }
611
+ /**
612
+ * Delete a column.
613
+ * @param columnId - The column ID to delete.
614
+ */
615
+ async deleteColumn(columnId) {
616
+ const httpUrl = this.serverUrl
617
+ .replace(/^ws:/, "http:")
618
+ .replace(/^wss:/, "https:");
619
+ const res = await fetch(`${httpUrl}/api/v1/kanban/columns/${columnId}`, {
620
+ method: "DELETE",
621
+ headers: { Authorization: `Bearer ${this.botToken}` },
622
+ });
623
+ if (!res.ok) {
624
+ const text = await res.text();
625
+ throw new Error(`deleteColumn failed (${res.status}): ${text}`);
626
+ }
627
+ }
628
+ /**
629
+ * Reorder columns in a board.
630
+ * @param boardId - The board ID.
631
+ * @param columnIds - Ordered array of column IDs.
632
+ */
633
+ async reorderColumns(boardId, columnIds) {
634
+ const httpUrl = this.serverUrl
635
+ .replace(/^ws:/, "http:")
636
+ .replace(/^wss:/, "https:");
637
+ const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/columns/reorder`, {
638
+ method: "POST",
639
+ headers: {
640
+ Authorization: `Bearer ${this.botToken}`,
641
+ "Content-Type": "application/json",
642
+ },
643
+ body: JSON.stringify({ columnIds }),
644
+ });
645
+ if (!res.ok) {
646
+ const text = await res.text();
647
+ throw new Error(`reorderColumns failed (${res.status}): ${text}`);
648
+ }
649
+ }
650
+ /**
651
+ * List all kanban cards for the agent's owner.
652
+ */
653
+ async listCards() {
654
+ const httpUrl = this.serverUrl
655
+ .replace(/^ws:/, "http:")
656
+ .replace(/^wss:/, "https:");
657
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards`, {
658
+ method: "GET",
659
+ headers: { Authorization: `Bearer ${this.botToken}` },
660
+ });
661
+ if (!res.ok) {
662
+ const text = await res.text();
663
+ throw new Error(`listCards failed (${res.status}): ${text}`);
664
+ }
665
+ return res.json();
666
+ }
667
+ /**
668
+ * Mark a card as complete (moves it to the Done column).
669
+ * @param cardId - The card ID to complete.
670
+ */
671
+ async completeCard(cardId) {
672
+ const httpUrl = this.serverUrl
673
+ .replace(/^ws:/, "http:")
674
+ .replace(/^wss:/, "https:");
675
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/complete`, {
676
+ method: "POST",
677
+ headers: { Authorization: `Bearer ${this.botToken}` },
678
+ });
679
+ if (!res.ok) {
680
+ const text = await res.text();
681
+ throw new Error(`completeCard failed (${res.status}): ${text}`);
682
+ }
683
+ return res.json();
684
+ }
685
+ /**
686
+ * List archived cards for a board.
687
+ * @param boardId - The board ID.
688
+ * @param options - Pagination options (page, limit).
689
+ */
690
+ async listArchivedCards(boardId, options) {
691
+ const httpUrl = this.serverUrl
692
+ .replace(/^ws:/, "http:")
693
+ .replace(/^wss:/, "https:");
694
+ const params = new URLSearchParams();
695
+ if (options?.page != null)
696
+ params.set("page", String(options.page));
697
+ if (options?.limit != null)
698
+ params.set("limit", String(options.limit));
699
+ const qs = params.toString();
700
+ const url = `${httpUrl}/api/v1/kanban/boards/${boardId}/archived-cards${qs ? `?${qs}` : ""}`;
701
+ const res = await fetch(url, {
702
+ method: "GET",
703
+ headers: { Authorization: `Bearer ${this.botToken}` },
704
+ });
705
+ if (!res.ok) {
706
+ const text = await res.text();
707
+ throw new Error(`listArchivedCards failed (${res.status}): ${text}`);
708
+ }
709
+ return res.json();
710
+ }
711
+ /**
712
+ * Add a commit link to a card.
713
+ * @param cardId - The card ID.
714
+ * @param body - Commit hash and optional message.
715
+ */
716
+ async addCardCommit(cardId, body) {
717
+ const httpUrl = this.serverUrl
718
+ .replace(/^ws:/, "http:")
719
+ .replace(/^wss:/, "https:");
720
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/commits`, {
721
+ method: "POST",
722
+ headers: {
723
+ Authorization: `Bearer ${this.botToken}`,
724
+ "Content-Type": "application/json",
725
+ },
726
+ body: JSON.stringify(body),
727
+ });
728
+ if (!res.ok) {
729
+ const text = await res.text();
730
+ throw new Error(`addCardCommit failed (${res.status}): ${text}`);
731
+ }
732
+ return res.json();
733
+ }
734
+ /**
735
+ * List commits linked to a card.
736
+ * @param cardId - The card ID.
737
+ */
738
+ async listCardCommits(cardId) {
739
+ const httpUrl = this.serverUrl
740
+ .replace(/^ws:/, "http:")
741
+ .replace(/^wss:/, "https:");
742
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/commits`, {
743
+ method: "GET",
744
+ headers: { Authorization: `Bearer ${this.botToken}` },
745
+ });
746
+ if (!res.ok) {
747
+ const text = await res.text();
748
+ throw new Error(`listCardCommits failed (${res.status}): ${text}`);
749
+ }
750
+ return res.json();
751
+ }
752
+ /**
753
+ * Link a note to a card.
754
+ * @param cardId - The card ID.
755
+ * @param noteId - The note ID to link.
756
+ */
757
+ async linkCardNote(cardId, noteId) {
758
+ const httpUrl = this.serverUrl
759
+ .replace(/^ws:/, "http:")
760
+ .replace(/^wss:/, "https:");
761
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/notes`, {
762
+ method: "POST",
763
+ headers: {
764
+ Authorization: `Bearer ${this.botToken}`,
765
+ "Content-Type": "application/json",
766
+ },
767
+ body: JSON.stringify({ noteId }),
768
+ });
769
+ if (!res.ok) {
770
+ const text = await res.text();
771
+ throw new Error(`linkCardNote failed (${res.status}): ${text}`);
772
+ }
773
+ }
774
+ /**
775
+ * Unlink a note from a card.
776
+ * @param cardId - The card ID.
777
+ * @param noteId - The note ID to unlink.
778
+ */
779
+ async unlinkCardNote(cardId, noteId) {
780
+ const httpUrl = this.serverUrl
781
+ .replace(/^ws:/, "http:")
782
+ .replace(/^wss:/, "https:");
783
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/notes/${noteId}`, {
784
+ method: "DELETE",
785
+ headers: { Authorization: `Bearer ${this.botToken}` },
786
+ });
787
+ if (!res.ok) {
788
+ const text = await res.text();
789
+ throw new Error(`unlinkCardNote failed (${res.status}): ${text}`);
790
+ }
791
+ }
792
+ /**
793
+ * List notes linked to a card.
794
+ * @param cardId - The card ID.
795
+ */
796
+ async listCardNotes(cardId) {
797
+ const httpUrl = this.serverUrl
798
+ .replace(/^ws:/, "http:")
799
+ .replace(/^wss:/, "https:");
800
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/notes`, {
801
+ method: "GET",
802
+ headers: { Authorization: `Bearer ${this.botToken}` },
803
+ });
804
+ if (!res.ok) {
805
+ const text = await res.text();
806
+ throw new Error(`listCardNotes failed (${res.status}): ${text}`);
807
+ }
808
+ return res.json();
809
+ }
810
+ // ── Label API ────────────────────────────────────────────────
811
+ /**
812
+ * List labels for a board.
813
+ * @param boardId - The board ID.
814
+ */
815
+ async listLabels(boardId) {
816
+ const httpUrl = this.serverUrl
817
+ .replace(/^ws:/, "http:")
818
+ .replace(/^wss:/, "https:");
819
+ const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/labels`, {
820
+ method: "GET",
821
+ headers: { Authorization: `Bearer ${this.botToken}` },
822
+ });
823
+ if (!res.ok) {
824
+ const text = await res.text();
825
+ throw new Error(`listLabels failed (${res.status}): ${text}`);
826
+ }
827
+ return res.json();
828
+ }
829
+ /**
830
+ * Create a label on a board.
831
+ * @param boardId - The board ID.
832
+ * @param body - Label name and optional color.
833
+ */
834
+ async createLabel(boardId, body) {
835
+ const httpUrl = this.serverUrl
836
+ .replace(/^ws:/, "http:")
837
+ .replace(/^wss:/, "https:");
838
+ const res = await fetch(`${httpUrl}/api/v1/kanban/boards/${boardId}/labels`, {
839
+ method: "POST",
840
+ headers: {
841
+ Authorization: `Bearer ${this.botToken}`,
842
+ "Content-Type": "application/json",
843
+ },
844
+ body: JSON.stringify(body),
845
+ });
846
+ if (!res.ok) {
847
+ const text = await res.text();
848
+ throw new Error(`createLabel failed (${res.status}): ${text}`);
849
+ }
850
+ return res.json();
851
+ }
852
+ /**
853
+ * Update a label.
854
+ * @param labelId - The label ID to update.
855
+ * @param body - Fields to update (name, color).
856
+ */
857
+ async updateLabel(labelId, body) {
858
+ const httpUrl = this.serverUrl
859
+ .replace(/^ws:/, "http:")
860
+ .replace(/^wss:/, "https:");
861
+ const res = await fetch(`${httpUrl}/api/v1/kanban/labels/${labelId}`, {
862
+ method: "PATCH",
863
+ headers: {
864
+ Authorization: `Bearer ${this.botToken}`,
865
+ "Content-Type": "application/json",
866
+ },
867
+ body: JSON.stringify(body),
868
+ });
869
+ if (!res.ok) {
870
+ const text = await res.text();
871
+ throw new Error(`updateLabel failed (${res.status}): ${text}`);
872
+ }
873
+ return res.json();
874
+ }
875
+ /**
876
+ * Delete a label.
877
+ * @param labelId - The label ID to delete.
878
+ */
879
+ async deleteLabel(labelId) {
880
+ const httpUrl = this.serverUrl
881
+ .replace(/^ws:/, "http:")
882
+ .replace(/^wss:/, "https:");
883
+ const res = await fetch(`${httpUrl}/api/v1/kanban/labels/${labelId}`, {
884
+ method: "DELETE",
885
+ headers: { Authorization: `Bearer ${this.botToken}` },
886
+ });
887
+ if (!res.ok) {
888
+ const text = await res.text();
889
+ throw new Error(`deleteLabel failed (${res.status}): ${text}`);
890
+ }
891
+ }
892
+ /**
893
+ * Add a label to a card.
894
+ * @param cardId - The card ID.
895
+ * @param labelId - The label ID to add.
896
+ */
897
+ async addCardLabel(cardId, labelId) {
898
+ const httpUrl = this.serverUrl
899
+ .replace(/^ws:/, "http:")
900
+ .replace(/^wss:/, "https:");
901
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/labels`, {
902
+ method: "POST",
903
+ headers: {
904
+ Authorization: `Bearer ${this.botToken}`,
905
+ "Content-Type": "application/json",
906
+ },
907
+ body: JSON.stringify({ labelId }),
908
+ });
909
+ if (!res.ok) {
910
+ const text = await res.text();
911
+ throw new Error(`addCardLabel failed (${res.status}): ${text}`);
912
+ }
913
+ }
914
+ /**
915
+ * Remove a label from a card.
916
+ * @param cardId - The card ID.
917
+ * @param labelId - The label ID to remove.
918
+ */
919
+ async removeCardLabel(cardId, labelId) {
920
+ const httpUrl = this.serverUrl
921
+ .replace(/^ws:/, "http:")
922
+ .replace(/^wss:/, "https:");
923
+ const res = await fetch(`${httpUrl}/api/v1/kanban/cards/${cardId}/labels/${labelId}`, {
924
+ method: "DELETE",
925
+ headers: { Authorization: `Bearer ${this.botToken}` },
926
+ });
927
+ if (!res.ok) {
928
+ const text = await res.text();
929
+ throw new Error(`removeCardLabel failed (${res.status}): ${text}`);
930
+ }
931
+ }
932
+ // ── Memory API ───────────────────────────────────────────────
933
+ /**
934
+ * Search memories across all memory capsules granted to this agent.
935
+ * Uses hybrid search (embedding + text) to find relevant memories.
936
+ * @param options - Query string and optional limit.
937
+ */
938
+ async queryMemory(options) {
939
+ const httpUrl = this.serverUrl
940
+ .replace(/^ws:/, "http:")
941
+ .replace(/^wss:/, "https:");
942
+ const params = new URLSearchParams();
943
+ params.set("query", options.query);
944
+ if (options.limit != null)
945
+ params.set("limit", String(options.limit));
946
+ const res = await fetch(`${httpUrl}/api/v1/capsules?${params}`, {
947
+ method: "GET",
948
+ headers: { Authorization: `Bearer ${this.botToken}` },
949
+ });
950
+ if (!res.ok) {
951
+ const body = await res.text();
952
+ throw new Error(`queryMemory failed (${res.status}): ${body}`);
953
+ }
954
+ // Server returns snake_case, map to camelCase
955
+ const raw = (await res.json());
956
+ return raw.map((r) => ({
957
+ content: r.content,
958
+ capsuleName: r.capsule_name,
959
+ capsuleId: r.capsule_id,
960
+ score: r.score,
961
+ importance: r.importance,
962
+ }));
963
+ }
964
+ // ── Skill Prompt API ─────────────────────────────────────────
965
+ /**
966
+ * Fetch the full prompt content for an installed skill by slug.
967
+ * Use this when the agent decides to trigger a skill from availableSkills.
968
+ * @param skillSlug - The skill slug (e.g. "draw", "proactive-agent").
969
+ */
970
+ async fetchSkillPrompt(skillSlug) {
971
+ const httpUrl = this.serverUrl
972
+ .replace(/^ws:/, "http:")
973
+ .replace(/^wss:/, "https:");
974
+ const res = await fetch(`${httpUrl}/api/v1/skills/${encodeURIComponent(skillSlug)}/prompt`, {
975
+ method: "GET",
976
+ headers: { Authorization: `Bearer ${this.botToken}` },
977
+ });
978
+ if (!res.ok) {
979
+ const body = await res.text();
980
+ throw new Error(`fetchSkillPrompt failed (${res.status}): ${body}`);
981
+ }
982
+ return (await res.json());
983
+ }
984
+ // ── Note Share API ───────────────────────────────────────────
985
+ /**
986
+ * Share a note as a message in a conversation.
987
+ * Creates a rich preview card visible to all conversation members.
988
+ * @param conversationId - The conversation to share into.
989
+ * @param noteId - The note ID to share.
990
+ */
991
+ async shareNote(conversationId, noteId) {
992
+ const httpUrl = this.serverUrl
993
+ .replace(/^ws:/, "http:")
994
+ .replace(/^wss:/, "https:");
995
+ const res = await fetch(`${httpUrl}/api/v1/notes/${noteId}/share`, {
996
+ method: "POST",
997
+ headers: { Authorization: `Bearer ${this.botToken}` },
998
+ });
999
+ if (!res.ok) {
1000
+ const text = await res.text();
1001
+ throw new Error(`shareNote failed (${res.status}): ${text}`);
1002
+ }
1003
+ return res.json();
1004
+ }
354
1005
  handleTask(data) {
1006
+ if (!this.taskHandler)
1007
+ return;
1008
+ const conversationId = data.conversationId;
1009
+ const activeTaskId = this.activeConversationTasks.get(conversationId);
1010
+ // If this conversation already has an active task, queue the new one
1011
+ if (activeTaskId && this.taskAbortControllers.has(activeTaskId)) {
1012
+ let queue = this.conversationQueues.get(conversationId);
1013
+ if (!queue) {
1014
+ queue = [];
1015
+ this.conversationQueues.set(conversationId, queue);
1016
+ }
1017
+ // Overflow: drop oldest queued task when queue is full
1018
+ if (queue.length >= MAX_QUEUE_SIZE) {
1019
+ const dropped = queue.shift();
1020
+ this.send({ type: "agent_error", taskId: dropped.taskId, error: "queue_overflow" });
1021
+ }
1022
+ queue.push(data);
1023
+ return;
1024
+ }
1025
+ this.executeTask(data);
1026
+ }
1027
+ executeTask(data) {
355
1028
  if (!this.taskHandler)
356
1029
  return;
357
1030
  const taskId = data.taskId;
1031
+ const conversationId = data.conversationId;
358
1032
  const abortController = new AbortController();
359
1033
  this.taskAbortControllers.set(taskId, abortController);
1034
+ this.activeConversationTasks.set(conversationId, taskId);
360
1035
  // Auto heartbeat: keep task alive while processing
361
1036
  const heartbeatTimer = setInterval(() => {
362
1037
  this.send({ type: "agent_heartbeat", taskId });
@@ -372,11 +1047,13 @@ export class ArinovaAgent {
372
1047
  taskFinished = true;
373
1048
  stopHeartbeat();
374
1049
  this.taskAbortControllers.delete(taskId);
1050
+ this.activeConversationTasks.delete(conversationId);
1051
+ this.processNextTask(conversationId);
375
1052
  return true;
376
1053
  };
377
1054
  const ctx = {
378
1055
  taskId,
379
- conversationId: data.conversationId,
1056
+ conversationId,
380
1057
  content: data.content,
381
1058
  conversationType: data.conversationType,
382
1059
  senderUserId: data.senderUserId,
@@ -406,8 +1083,8 @@ export class ArinovaAgent {
406
1083
  this.send({ type: "agent_error", taskId, error });
407
1084
  },
408
1085
  signal: abortController.signal,
409
- uploadFile: (file, fileName, fileType) => this.uploadFile(data.conversationId, file, fileName, fileType),
410
- fetchHistory: (options) => this.fetchHistory(data.conversationId, options),
1086
+ uploadFile: (file, fileName, fileType) => this.uploadFile(conversationId, file, fileName, fileType),
1087
+ fetchHistory: (options) => this.fetchHistory(conversationId, options),
411
1088
  };
412
1089
  // When task is aborted (user cancelled), immediately send cancellation
413
1090
  // error so the server knows this agent is free for new tasks.
@@ -421,6 +1098,17 @@ export class ArinovaAgent {
421
1098
  ctx.sendError(errorMsg);
422
1099
  });
423
1100
  }
1101
+ processNextTask(conversationId) {
1102
+ const queue = this.conversationQueues.get(conversationId);
1103
+ if (!queue || queue.length === 0) {
1104
+ this.conversationQueues.delete(conversationId);
1105
+ return;
1106
+ }
1107
+ const nextTask = queue.shift();
1108
+ if (queue.length === 0)
1109
+ this.conversationQueues.delete(conversationId);
1110
+ this.executeTask(nextTask);
1111
+ }
424
1112
  }
425
1113
  const MIME_TYPES = {
426
1114
  jpg: "image/jpeg",