agent-comm-hub 0.8.0 → 0.9.0
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 +27 -16
- package/README.zh.md +11 -10
- package/agents/README.md +8 -8
- package/agents/SKILL.md +37 -22
- package/agents/dsh/cordis.patch.yml +5 -1
- package/agents/install-all.ps1 +0 -4
- package/agents/minimax-code/SKILL.md +1 -1
- package/agents/registry.json +3 -1
- package/assets/admin.html +190 -1743
- package/lib/cli.js +388 -84
- package/lib/index.js +386 -82
- package/lib/setup.js +6 -2
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -422,7 +422,40 @@ import { randomUUID } from "node:crypto";
|
|
|
422
422
|
var KINDS = ["chat", "task", "notice", "ack"];
|
|
423
423
|
var PEER_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
424
424
|
var BROADCAST = "all";
|
|
425
|
-
var
|
|
425
|
+
var MAX_HOP = 5;
|
|
426
|
+
var TERMINAL_TASK_STATUSES = ["rejected", "done", "failed", "timeout"];
|
|
427
|
+
function ackToTaskStatus(ack) {
|
|
428
|
+
return ack;
|
|
429
|
+
}
|
|
430
|
+
function nextTaskStatus(current, ack) {
|
|
431
|
+
if (TERMINAL_TASK_STATUSES.includes(current)) return null;
|
|
432
|
+
const next = ackToTaskStatus(ack);
|
|
433
|
+
switch (current) {
|
|
434
|
+
case "pending":
|
|
435
|
+
return next;
|
|
436
|
+
case "accepted":
|
|
437
|
+
return next;
|
|
438
|
+
case "working":
|
|
439
|
+
if (next === "accepted") return "working";
|
|
440
|
+
return next;
|
|
441
|
+
default:
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function aggregateTaskStatus(assignees) {
|
|
446
|
+
if (assignees.length === 0) return "pending";
|
|
447
|
+
const statuses = assignees.map((a) => a.status);
|
|
448
|
+
const active = statuses.filter((s) => s === "pending" || s === "accepted" || s === "working");
|
|
449
|
+
if (active.length > 0) {
|
|
450
|
+
if (active.some((s) => s === "working")) return "working";
|
|
451
|
+
if (active.some((s) => s === "accepted")) return "accepted";
|
|
452
|
+
return "pending";
|
|
453
|
+
}
|
|
454
|
+
if (statuses.every((s) => s === "done")) return "done";
|
|
455
|
+
if (statuses.some((s) => s === "failed")) return "failed";
|
|
456
|
+
if (statuses.some((s) => s === "timeout")) return "timeout";
|
|
457
|
+
return "rejected";
|
|
458
|
+
}
|
|
426
459
|
function encodeContent(payload) {
|
|
427
460
|
return JSON.stringify(payload);
|
|
428
461
|
}
|
|
@@ -459,10 +492,14 @@ var AgentHub = class {
|
|
|
459
492
|
groups = /* @__PURE__ */ new Map();
|
|
460
493
|
/** Task ledger: task message id → record (status + ack timeline). */
|
|
461
494
|
taskLedger = /* @__PURE__ */ new Map();
|
|
495
|
+
/** SLA timers keyed by task id (cleared when the task turns terminal). */
|
|
496
|
+
taskTimers = /* @__PURE__ */ new Map();
|
|
462
497
|
gcTimer;
|
|
463
498
|
/** Stop the idle GC (call when the hub shuts down). */
|
|
464
499
|
dispose() {
|
|
465
500
|
if (this.gcTimer !== void 0) clearInterval(this.gcTimer);
|
|
501
|
+
for (const timer of this.taskTimers.values()) clearTimeout(timer);
|
|
502
|
+
this.taskTimers.clear();
|
|
466
503
|
}
|
|
467
504
|
gcTick(idleTimeoutMs) {
|
|
468
505
|
const now = Date.now();
|
|
@@ -626,29 +663,49 @@ var AgentHub = class {
|
|
|
626
663
|
return last !== void 0 && Date.now() - last < (this.options.connectedWindowMs ?? 3e4);
|
|
627
664
|
}
|
|
628
665
|
/** Send a chat/notice text message to `to` (or {@link BROADCAST}). */
|
|
629
|
-
send(from, to, kind, content) {
|
|
630
|
-
return this.route(from, to, kind, content);
|
|
666
|
+
send(from, to, kind, content, opts) {
|
|
667
|
+
return this.route(from, to, kind, content, opts?.ref, opts?.threadId);
|
|
631
668
|
}
|
|
632
|
-
/** Send a structured task message to `to` (
|
|
633
|
-
* a ledger entry so the sender can later query status /
|
|
669
|
+
/** Send a structured task message to `to` (peer, {@link BROADCAST}, or a
|
|
670
|
+
* group id). Records a ledger entry so the sender can later query status /
|
|
671
|
+
* wait for acks. */
|
|
634
672
|
sendTask(from, to, task) {
|
|
635
|
-
const
|
|
673
|
+
const threadId = task.threadId;
|
|
674
|
+
const message = this.route(from, to, "task", JSON.stringify(task), void 0, threadId);
|
|
636
675
|
this.recordTask(message, task);
|
|
637
676
|
return message;
|
|
638
677
|
}
|
|
639
678
|
/** Send an acknowledgement back to the sender of `ref`. Updates the task
|
|
640
|
-
* ledger when `ref` is a known task
|
|
679
|
+
* ledger when `ref` is a known task (looked up in the ledger first, so acks
|
|
680
|
+
* survive history-ring eviction); chat/notice refs still route via history. */
|
|
641
681
|
sendAck(from, ref, ack) {
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
682
|
+
const task = this.taskLedger.get(ref);
|
|
683
|
+
let target;
|
|
684
|
+
if (task !== void 0) {
|
|
685
|
+
target = task.from;
|
|
686
|
+
} else {
|
|
687
|
+
const original = this.historyRing.findLast((message2) => message2.id === ref);
|
|
688
|
+
if (!original) throw new Error(`cannot ack unknown message: ${ref}`);
|
|
689
|
+
target = original.from;
|
|
690
|
+
}
|
|
691
|
+
const message = this.route(from, target, "ack", JSON.stringify(ack), ref);
|
|
645
692
|
this.applyAckToLedger(message, from, ref, ack);
|
|
646
693
|
return message;
|
|
647
694
|
}
|
|
695
|
+
/** Resolve the peer ids a task is assigned to (broadcast / group fan-out). */
|
|
696
|
+
resolveAssignees(from, to) {
|
|
697
|
+
if (to === BROADCAST) return this.peers().filter((peer) => peer !== from);
|
|
698
|
+
const group = this.groups.get(to);
|
|
699
|
+
if (group !== void 0) return group.members.filter((member) => member !== from);
|
|
700
|
+
return [to];
|
|
701
|
+
}
|
|
648
702
|
/** Create or refresh the ledger row for a newly sent task. */
|
|
649
703
|
recordTask(message, task) {
|
|
650
704
|
const now = message.ts;
|
|
651
|
-
this.
|
|
705
|
+
const assigneeIds = this.resolveAssignees(message.from, message.to);
|
|
706
|
+
const assignees = assigneeIds.map((peerId) => ({ peerId, status: "pending", acks: [] }));
|
|
707
|
+
const timeoutMs = typeof task.timeoutMs === "number" && Number.isFinite(task.timeoutMs) && task.timeoutMs > 0 ? Math.floor(task.timeoutMs) : void 0;
|
|
708
|
+
const record = {
|
|
652
709
|
id: message.id,
|
|
653
710
|
from: message.from,
|
|
654
711
|
to: message.to,
|
|
@@ -657,25 +714,105 @@ var AgentHub = class {
|
|
|
657
714
|
...task.deliverable !== void 0 ? { deliverable: task.deliverable } : {},
|
|
658
715
|
status: "pending",
|
|
659
716
|
acks: [],
|
|
717
|
+
assignees,
|
|
718
|
+
...timeoutMs !== void 0 ? { timeoutMs, deadlineAt: now + timeoutMs } : {},
|
|
719
|
+
...message.threadId !== void 0 ? { threadId: message.threadId } : {},
|
|
660
720
|
createdAt: now,
|
|
661
721
|
updatedAt: now
|
|
662
|
-
}
|
|
722
|
+
};
|
|
723
|
+
this.taskLedger.set(message.id, record);
|
|
724
|
+
this.scheduleTaskTimeout(record);
|
|
663
725
|
this.evictOverflowTasks();
|
|
726
|
+
this.options.onTaskChanged?.(this.taskOf(record.id));
|
|
727
|
+
}
|
|
728
|
+
/** Arm the SLA timer for a task that supplied `timeoutMs`. */
|
|
729
|
+
scheduleTaskTimeout(record) {
|
|
730
|
+
if (record.deadlineAt === void 0) return;
|
|
731
|
+
const delay = Math.max(0, record.deadlineAt - Date.now());
|
|
732
|
+
const timer = setTimeout(() => this.fireTaskTimeout(record.id), delay);
|
|
733
|
+
timer.unref?.();
|
|
734
|
+
this.taskTimers.set(record.id, timer);
|
|
735
|
+
}
|
|
736
|
+
/** Mark a non-terminal task `timeout` and leave a system timeline event. */
|
|
737
|
+
fireTaskTimeout(taskId) {
|
|
738
|
+
this.taskTimers.delete(taskId);
|
|
739
|
+
const record = this.taskLedger.get(taskId);
|
|
740
|
+
if (record === void 0) return;
|
|
741
|
+
if (TERMINAL_TASK_STATUSES.includes(record.status)) return;
|
|
742
|
+
const now = Date.now();
|
|
743
|
+
const event = {
|
|
744
|
+
from: record.to,
|
|
745
|
+
status: "failed",
|
|
746
|
+
note: `task timed out after ${record.timeoutMs ?? 0}ms without a terminal ack`,
|
|
747
|
+
ts: now,
|
|
748
|
+
messageId: `timeout-${record.id}`,
|
|
749
|
+
applied: true,
|
|
750
|
+
system: true
|
|
751
|
+
};
|
|
752
|
+
record.acks.push(event);
|
|
753
|
+
for (const assignee of record.assignees) {
|
|
754
|
+
if (!TERMINAL_TASK_STATUSES.includes(assignee.status)) {
|
|
755
|
+
assignee.status = "timeout";
|
|
756
|
+
assignee.acks.push({ ...event });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
record.status = "timeout";
|
|
760
|
+
record.updatedAt = now;
|
|
761
|
+
this.clearTaskTimer(taskId);
|
|
762
|
+
this.options.onTaskChanged?.(this.taskOf(taskId));
|
|
664
763
|
}
|
|
665
|
-
|
|
666
|
-
|
|
764
|
+
clearTaskTimer(taskId) {
|
|
765
|
+
const timer = this.taskTimers.get(taskId);
|
|
766
|
+
if (timer !== void 0) {
|
|
767
|
+
clearTimeout(timer);
|
|
768
|
+
this.taskTimers.delete(taskId);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/** Note that `target` has taken delivery of a task (waiter handoff / queue). */
|
|
772
|
+
markTaskDelivered(taskId, target, ts) {
|
|
773
|
+
const record = this.taskLedger.get(taskId);
|
|
774
|
+
if (record === void 0) return;
|
|
775
|
+
const assignee = record.assignees.find((a) => a.peerId === target);
|
|
776
|
+
if (assignee === void 0 || assignee.deliveredMs !== void 0) return;
|
|
777
|
+
assignee.deliveredMs = ts;
|
|
778
|
+
record.updatedAt = ts;
|
|
779
|
+
this.options.onTaskChanged?.(this.taskOf(taskId));
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Fold an ack into the task's timeline and (when legal) its status.
|
|
783
|
+
* - Unknown refs are a no-op (chat/notice can be acked for routing without a ledger row).
|
|
784
|
+
* - Terminal tasks keep the event on the timeline with `applied: false`.
|
|
785
|
+
* - Broadcast / group tasks update the acking assignee first, then re-aggregate.
|
|
786
|
+
*/
|
|
667
787
|
applyAckToLedger(message, from, ref, ack) {
|
|
668
788
|
const record = this.taskLedger.get(ref);
|
|
669
789
|
if (record === void 0) return;
|
|
670
|
-
record.
|
|
790
|
+
const next = nextTaskStatus(record.status, ack.status);
|
|
791
|
+
const assignee = record.assignees.find((a) => a.peerId === from);
|
|
792
|
+
const assigneeNext = assignee === void 0 ? null : nextTaskStatus(assignee.status, ack.status);
|
|
793
|
+
const applied = record.assignees.length <= 1 ? next !== null : assigneeNext !== null;
|
|
794
|
+
const event = {
|
|
671
795
|
from,
|
|
672
796
|
status: ack.status,
|
|
673
797
|
...ack.note !== void 0 ? { note: ack.note } : {},
|
|
798
|
+
...typeof ack.progress === "number" && Number.isFinite(ack.progress) ? { progress: Math.max(0, Math.min(100, ack.progress)) } : {},
|
|
674
799
|
ts: message.ts,
|
|
675
|
-
messageId: message.id
|
|
676
|
-
|
|
677
|
-
|
|
800
|
+
messageId: message.id,
|
|
801
|
+
applied
|
|
802
|
+
};
|
|
803
|
+
record.acks.push(event);
|
|
804
|
+
if (assignee !== void 0 && assigneeNext !== null) {
|
|
805
|
+
assignee.status = assigneeNext;
|
|
806
|
+
assignee.acks.push({ ...event });
|
|
807
|
+
}
|
|
808
|
+
if (record.assignees.length <= 1) {
|
|
809
|
+
if (next !== null) record.status = next;
|
|
810
|
+
} else {
|
|
811
|
+
record.status = aggregateTaskStatus(record.assignees);
|
|
812
|
+
}
|
|
678
813
|
record.updatedAt = message.ts;
|
|
814
|
+
if (TERMINAL_TASK_STATUSES.includes(record.status)) this.clearTaskTimer(record.id);
|
|
815
|
+
this.options.onTaskChanged?.(this.taskOf(record.id));
|
|
679
816
|
}
|
|
680
817
|
/** Keep the ledger bounded: drop oldest terminal tasks first. */
|
|
681
818
|
evictOverflowTasks() {
|
|
@@ -684,19 +821,25 @@ var AgentHub = class {
|
|
|
684
821
|
let overflow = this.taskLedger.size - MAX_TASKS;
|
|
685
822
|
for (const record of evictable) {
|
|
686
823
|
if (overflow <= 0) break;
|
|
824
|
+
this.clearTaskTimer(record.id);
|
|
687
825
|
this.taskLedger.delete(record.id);
|
|
688
826
|
overflow--;
|
|
689
827
|
}
|
|
690
828
|
}
|
|
691
|
-
/**
|
|
829
|
+
/** Deep-ish copy of one task by message id (the ack `ref`). */
|
|
692
830
|
taskOf(ref) {
|
|
693
831
|
const record = this.taskLedger.get(ref);
|
|
694
|
-
|
|
832
|
+
if (record === void 0) return void 0;
|
|
833
|
+
return {
|
|
834
|
+
...record,
|
|
835
|
+
acks: record.acks.map((a) => ({ ...a })),
|
|
836
|
+
assignees: record.assignees.map((a) => ({ ...a, acks: a.acks.map((e) => ({ ...e })) }))
|
|
837
|
+
};
|
|
695
838
|
}
|
|
696
839
|
/**
|
|
697
840
|
* Tasks involving `peer`. `role`:
|
|
698
841
|
* - `sent` — tasks this peer delegated (`from`)
|
|
699
|
-
* - `received` — tasks addressed to this peer
|
|
842
|
+
* - `received` — tasks addressed to this peer, a group they are in, or broadcast
|
|
700
843
|
* - `all` (default) — either side
|
|
701
844
|
* Optional `status` filter matches the ledger's current status.
|
|
702
845
|
*/
|
|
@@ -705,17 +848,94 @@ var AgentHub = class {
|
|
|
705
848
|
const limit = Math.max(1, options?.limit ?? 50);
|
|
706
849
|
const out = [];
|
|
707
850
|
for (const record of this.taskLedger.values()) {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
851
|
+
if (peerId !== "all") {
|
|
852
|
+
const isSent = record.from === peerId;
|
|
853
|
+
const isReceived = record.to === peerId || record.to === BROADCAST || record.assignees.some((a) => a.peerId === peerId) || (this.groups.get(record.to)?.members.includes(peerId) ?? false);
|
|
854
|
+
if (role === "sent" && !isSent) continue;
|
|
855
|
+
if (role === "received" && !isReceived) continue;
|
|
856
|
+
if (role === "all" && !isSent && !isReceived) continue;
|
|
857
|
+
}
|
|
713
858
|
if (options?.status !== void 0 && record.status !== options.status) continue;
|
|
714
|
-
|
|
859
|
+
const copy = this.taskOf(record.id);
|
|
860
|
+
if (copy !== void 0) out.push(copy);
|
|
715
861
|
}
|
|
716
862
|
out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
717
863
|
return out.slice(0, limit);
|
|
718
864
|
}
|
|
865
|
+
/** Snapshot of every task (persistence). */
|
|
866
|
+
exportTasks() {
|
|
867
|
+
return [...this.taskLedger.values()].map((record) => this.taskOf(record.id)).filter(Boolean);
|
|
868
|
+
}
|
|
869
|
+
/** Restore the task ledger at boot (and re-arm SLA timers). */
|
|
870
|
+
importTasks(tasks) {
|
|
871
|
+
if (!Array.isArray(tasks)) return 0;
|
|
872
|
+
let count = 0;
|
|
873
|
+
for (const raw of tasks) {
|
|
874
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
875
|
+
const t = raw;
|
|
876
|
+
if (typeof t.id !== "string" || typeof t.from !== "string" || typeof t.to !== "string" || typeof t.prompt !== "string") continue;
|
|
877
|
+
if (typeof t.status !== "string" || !TERMINAL_TASK_STATUSES.includes(t.status) && t.status !== "pending" && t.status !== "accepted" && t.status !== "working") continue;
|
|
878
|
+
const acks = Array.isArray(t.acks) ? t.acks.flatMap((rawAck) => {
|
|
879
|
+
if (typeof rawAck !== "object" || rawAck === null) return [];
|
|
880
|
+
const a = rawAck;
|
|
881
|
+
if (typeof a.from !== "string" || typeof a.status !== "string" || typeof a.ts !== "number" || typeof a.messageId !== "string") return [];
|
|
882
|
+
return [{
|
|
883
|
+
from: a.from,
|
|
884
|
+
status: a.status,
|
|
885
|
+
...typeof a.note === "string" ? { note: a.note } : {},
|
|
886
|
+
...typeof a.progress === "number" ? { progress: a.progress } : {},
|
|
887
|
+
ts: a.ts,
|
|
888
|
+
messageId: a.messageId,
|
|
889
|
+
applied: a.applied !== false,
|
|
890
|
+
...a.system === true ? { system: true } : {}
|
|
891
|
+
}];
|
|
892
|
+
}) : [];
|
|
893
|
+
const assignees = Array.isArray(t.assignees) && t.assignees.length > 0 ? t.assignees.flatMap((rawA) => {
|
|
894
|
+
if (typeof rawA !== "object" || rawA === null) return [];
|
|
895
|
+
const a = rawA;
|
|
896
|
+
if (typeof a.peerId !== "string" || typeof a.status !== "string") return [];
|
|
897
|
+
return [{
|
|
898
|
+
peerId: a.peerId,
|
|
899
|
+
status: a.status,
|
|
900
|
+
...typeof a.deliveredMs === "number" ? { deliveredMs: a.deliveredMs } : {},
|
|
901
|
+
acks: Array.isArray(a.acks) ? a.acks.flatMap((rawE) => {
|
|
902
|
+
if (typeof rawE !== "object" || rawE === null) return [];
|
|
903
|
+
const e = rawE;
|
|
904
|
+
if (typeof e.from !== "string" || typeof e.status !== "string" || typeof e.ts !== "number" || typeof e.messageId !== "string") return [];
|
|
905
|
+
return [{
|
|
906
|
+
from: e.from,
|
|
907
|
+
status: e.status,
|
|
908
|
+
...typeof e.note === "string" ? { note: e.note } : {},
|
|
909
|
+
ts: e.ts,
|
|
910
|
+
messageId: e.messageId,
|
|
911
|
+
applied: e.applied !== false
|
|
912
|
+
}];
|
|
913
|
+
}) : []
|
|
914
|
+
}];
|
|
915
|
+
}) : [{ peerId: t.to, status: t.status, acks: [] }];
|
|
916
|
+
const record = {
|
|
917
|
+
id: t.id,
|
|
918
|
+
from: t.from,
|
|
919
|
+
to: t.to,
|
|
920
|
+
prompt: t.prompt,
|
|
921
|
+
...typeof t.context === "string" ? { context: t.context } : {},
|
|
922
|
+
...typeof t.deliverable === "string" ? { deliverable: t.deliverable } : {},
|
|
923
|
+
status: t.status,
|
|
924
|
+
acks,
|
|
925
|
+
assignees,
|
|
926
|
+
...typeof t.timeoutMs === "number" ? { timeoutMs: t.timeoutMs } : {},
|
|
927
|
+
...typeof t.deadlineAt === "number" ? { deadlineAt: t.deadlineAt } : {},
|
|
928
|
+
...typeof t.threadId === "string" ? { threadId: t.threadId } : {},
|
|
929
|
+
createdAt: typeof t.createdAt === "number" ? t.createdAt : Date.now(),
|
|
930
|
+
updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : Date.now()
|
|
931
|
+
};
|
|
932
|
+
this.taskLedger.set(record.id, record);
|
|
933
|
+
this.scheduleTaskTimeout(record);
|
|
934
|
+
count++;
|
|
935
|
+
}
|
|
936
|
+
this.evictOverflowTasks();
|
|
937
|
+
return count;
|
|
938
|
+
}
|
|
719
939
|
/** Recent messages involving `peer` (inbound and outbound, plus group
|
|
720
940
|
* channels the peer belongs to), newest first. */
|
|
721
941
|
history(peerId, limit) {
|
|
@@ -799,20 +1019,7 @@ var AgentHub = class {
|
|
|
799
1019
|
const group = this.groups.get(groupId);
|
|
800
1020
|
if (group === void 0) throw new Error(`unknown group: ${groupId}`);
|
|
801
1021
|
if (!group.members.includes(from)) throw new Error(`peer '${from}' is not a member of group '${groupId}'`);
|
|
802
|
-
|
|
803
|
-
id: randomUUID(),
|
|
804
|
-
from,
|
|
805
|
-
to: groupId,
|
|
806
|
-
kind: "chat",
|
|
807
|
-
content,
|
|
808
|
-
channel: groupId,
|
|
809
|
-
ts: Date.now()
|
|
810
|
-
};
|
|
811
|
-
this.lastSeen.set(from, message.ts);
|
|
812
|
-
for (const member of group.members) {
|
|
813
|
-
if (member !== from && this.lastSeen.has(member)) this.deliver(member, message);
|
|
814
|
-
}
|
|
815
|
-
return message;
|
|
1022
|
+
return this.route(from, groupId, "chat", content);
|
|
816
1023
|
}
|
|
817
1024
|
/** Snapshot of every group for persistence. */
|
|
818
1025
|
exportGroups() {
|
|
@@ -860,6 +1067,7 @@ var AgentHub = class {
|
|
|
860
1067
|
content: m.content,
|
|
861
1068
|
...typeof m.ref === "string" ? { ref: m.ref } : {},
|
|
862
1069
|
...typeof m.channel === "string" ? { channel: m.channel } : {},
|
|
1070
|
+
...typeof m.threadId === "string" ? { threadId: m.threadId } : {},
|
|
863
1071
|
ts: m.ts
|
|
864
1072
|
});
|
|
865
1073
|
}
|
|
@@ -893,6 +1101,7 @@ var AgentHub = class {
|
|
|
893
1101
|
content: m.content,
|
|
894
1102
|
...typeof m.ref === "string" ? { ref: m.ref } : {},
|
|
895
1103
|
...typeof m.channel === "string" ? { channel: m.channel } : {},
|
|
1104
|
+
...typeof m.threadId === "string" ? { threadId: m.threadId } : {},
|
|
896
1105
|
ts: m.ts
|
|
897
1106
|
});
|
|
898
1107
|
}
|
|
@@ -944,11 +1153,12 @@ var AgentHub = class {
|
|
|
944
1153
|
* a multi-message burst (or a reconnect with a full queue) would silently
|
|
945
1154
|
* lose messages that never reach wait/poll again.
|
|
946
1155
|
*/
|
|
947
|
-
wait(peerId, timeoutMs, from, signal, ref) {
|
|
1156
|
+
wait(peerId, timeoutMs, from, signal, ref, threadId) {
|
|
948
1157
|
const startedAt = Date.now();
|
|
949
1158
|
const matches = (message) => {
|
|
950
1159
|
if (from !== void 0 && message.from !== from) return false;
|
|
951
1160
|
if (ref !== void 0 && !(message.kind === "ack" && message.ref === ref)) return false;
|
|
1161
|
+
if (threadId !== void 0 && message.threadId !== threadId) return false;
|
|
952
1162
|
return true;
|
|
953
1163
|
};
|
|
954
1164
|
const queue = this.queues.get(peerId);
|
|
@@ -993,15 +1203,28 @@ var AgentHub = class {
|
|
|
993
1203
|
timer,
|
|
994
1204
|
onAbort,
|
|
995
1205
|
...from !== void 0 ? { from } : {},
|
|
996
|
-
...ref !== void 0 ? { ref } : {}
|
|
1206
|
+
...ref !== void 0 ? { ref } : {},
|
|
1207
|
+
...threadId !== void 0 ? { threadId } : {}
|
|
997
1208
|
});
|
|
998
1209
|
this.waiters.set(peerId, list);
|
|
999
1210
|
});
|
|
1000
1211
|
}
|
|
1001
|
-
/** Create a message from `from` addressed to `to` and deliver it.
|
|
1002
|
-
|
|
1212
|
+
/** Create a message from `from` addressed to `to` and deliver it. `to` may
|
|
1213
|
+
* be a peer id, {@link BROADCAST}, or a group id. */
|
|
1214
|
+
route(from, to, kind, content, ref, threadId) {
|
|
1003
1215
|
if (!this.lastSeen.has(from)) throw new Error(`sender not registered: ${from}`);
|
|
1004
|
-
|
|
1216
|
+
const group = this.groups.get(to);
|
|
1217
|
+
if (to !== BROADCAST && group === void 0 && !this.lastSeen.has(to)) {
|
|
1218
|
+
throw new Error(`unknown recipient: ${to} (registered peers: ${this.peers().join(", ") || "none"})`);
|
|
1219
|
+
}
|
|
1220
|
+
let hop = 0;
|
|
1221
|
+
if (ref !== void 0) {
|
|
1222
|
+
const parent = this.historyRing.findLast((m) => m.id === ref);
|
|
1223
|
+
hop = (parent?.hop ?? 0) + 1;
|
|
1224
|
+
if (hop > MAX_HOP) {
|
|
1225
|
+
throw new Error(`hop limit exceeded (max ${MAX_HOP}): start a new task/thread instead of extending this chain`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1005
1228
|
const message = {
|
|
1006
1229
|
id: randomUUID(),
|
|
1007
1230
|
from,
|
|
@@ -1009,31 +1232,42 @@ var AgentHub = class {
|
|
|
1009
1232
|
kind,
|
|
1010
1233
|
content,
|
|
1011
1234
|
...ref !== void 0 ? { ref } : {},
|
|
1235
|
+
...threadId !== void 0 ? { threadId } : {},
|
|
1236
|
+
...group !== void 0 ? { channel: group.id } : {},
|
|
1237
|
+
...hop > 0 ? { hop } : {},
|
|
1012
1238
|
ts: Date.now()
|
|
1013
1239
|
};
|
|
1014
1240
|
this.lastSeen.set(from, message.ts);
|
|
1241
|
+
this.remember(message);
|
|
1015
1242
|
if (to === BROADCAST) {
|
|
1016
1243
|
for (const peer of this.peers()) {
|
|
1017
1244
|
if (peer !== from) this.deliver(peer, message);
|
|
1018
1245
|
}
|
|
1246
|
+
} else if (group !== void 0) {
|
|
1247
|
+
for (const member of group.members) {
|
|
1248
|
+
if (member !== from) this.deliver(member, message);
|
|
1249
|
+
}
|
|
1019
1250
|
} else {
|
|
1020
1251
|
this.deliver(to, message);
|
|
1021
1252
|
}
|
|
1022
1253
|
return message;
|
|
1023
1254
|
}
|
|
1024
|
-
/** Queue or hand off a message; wake the first matching waiter for its target.
|
|
1255
|
+
/** Queue or hand off a message; wake the first matching waiter for its target.
|
|
1256
|
+
* History / onMessage are recorded by the caller (route / sendToGroup) —
|
|
1257
|
+
* exactly once per message, never per fan-out target. */
|
|
1025
1258
|
deliver(target, message) {
|
|
1026
1259
|
const list = this.waiters.get(target) ?? [];
|
|
1027
1260
|
const index = list.findIndex((waiter) => {
|
|
1028
1261
|
if (waiter.from !== void 0 && waiter.from !== message.from) return false;
|
|
1029
1262
|
if (waiter.ref !== void 0 && !(message.kind === "ack" && message.ref === waiter.ref)) return false;
|
|
1263
|
+
if (waiter.threadId !== void 0 && message.threadId !== waiter.threadId) return false;
|
|
1030
1264
|
return true;
|
|
1031
1265
|
});
|
|
1266
|
+
if (message.kind === "task") this.markTaskDelivered(message.id, target, message.ts);
|
|
1032
1267
|
if (index >= 0) {
|
|
1033
1268
|
const [waiter] = list.splice(index, 1);
|
|
1034
1269
|
this.waiters.set(target, list);
|
|
1035
1270
|
clearTimeout(waiter.timer);
|
|
1036
|
-
this.remember(message);
|
|
1037
1271
|
waiter.resolve({ type: "message", message });
|
|
1038
1272
|
return;
|
|
1039
1273
|
}
|
|
@@ -1041,11 +1275,10 @@ var AgentHub = class {
|
|
|
1041
1275
|
if (queue.length >= this.options.maxQueue) queue.shift();
|
|
1042
1276
|
queue.push(message);
|
|
1043
1277
|
this.queues.set(target, queue);
|
|
1044
|
-
this.remember(message);
|
|
1045
1278
|
this.options.onQueued?.(message, target);
|
|
1046
1279
|
}
|
|
1047
|
-
/** Append a
|
|
1048
|
-
*
|
|
1280
|
+
/** Append a sent message to the history ring (and fire the persistence /
|
|
1281
|
+
* relay hooks). Exactly one call per message — see route() / sendToGroup(). */
|
|
1049
1282
|
remember(message) {
|
|
1050
1283
|
this.historyRing.push(message);
|
|
1051
1284
|
if (this.historyRing.length > this.options.historyLimit) this.historyRing.splice(0, this.historyRing.length - this.options.historyLimit);
|
|
@@ -1215,11 +1448,13 @@ function loadTokenFileVoid(rows) {
|
|
|
1215
1448
|
// src/hub-tools.ts
|
|
1216
1449
|
var DEFAULT_WAIT_MS = 3e4;
|
|
1217
1450
|
function present(message) {
|
|
1218
|
-
const { ref, channel, ...rest } = message;
|
|
1451
|
+
const { ref, channel, threadId, hop, ...rest } = message;
|
|
1219
1452
|
return {
|
|
1220
1453
|
...rest,
|
|
1221
1454
|
...ref !== void 0 ? { ref } : {},
|
|
1222
1455
|
...channel !== void 0 ? { channel } : {},
|
|
1456
|
+
...threadId !== void 0 ? { threadId } : {},
|
|
1457
|
+
...hop !== void 0 ? { hop } : {},
|
|
1223
1458
|
content: decodeContent(message.kind, message.content)
|
|
1224
1459
|
};
|
|
1225
1460
|
}
|
|
@@ -1320,7 +1555,10 @@ function hubTools(hub, registry, options) {
|
|
|
1320
1555
|
from: message.from,
|
|
1321
1556
|
to: message.to,
|
|
1322
1557
|
kind: message.kind,
|
|
1558
|
+
...message.ref !== void 0 ? { ref: message.ref } : {},
|
|
1323
1559
|
...message.channel !== void 0 ? { channel: message.channel } : {},
|
|
1560
|
+
...message.threadId !== void 0 ? { threadId: message.threadId } : {},
|
|
1561
|
+
...message.hop !== void 0 ? { hop: message.hop } : {},
|
|
1324
1562
|
ts: message.ts
|
|
1325
1563
|
});
|
|
1326
1564
|
const presentWait = (result) => result.type === "timeout" ? result : { type: "message", message: present(result.message) };
|
|
@@ -1462,70 +1700,93 @@ function hubTools(hub, registry, options) {
|
|
|
1462
1700
|
},
|
|
1463
1701
|
{
|
|
1464
1702
|
name: "bridge_chat",
|
|
1465
|
-
description: 'Send a chat message to another agent on the hub. Use bridge_wait (long-poll) or bridge_poll to receive replies. `to` is the target peerId, or "all" to broadcast.',
|
|
1703
|
+
description: 'Send a chat message to another agent on the hub. Use bridge_wait (long-poll) or bridge_poll to receive replies. `to` is the target peerId, a group id, or "all" to broadcast. Optional `threadId` groups multi-turn dialogs; optional `ref` marks a reply-to.',
|
|
1466
1704
|
inputSchema: schema(
|
|
1467
|
-
{
|
|
1705
|
+
{
|
|
1706
|
+
to: str('Target peerId, group id, or "all" to broadcast.'),
|
|
1707
|
+
message: str("The message text."),
|
|
1708
|
+
threadId: optStr("Optional conversation / workflow thread id."),
|
|
1709
|
+
ref: optStr("Optional reply-to message id.")
|
|
1710
|
+
},
|
|
1468
1711
|
["to", "message"]
|
|
1469
1712
|
),
|
|
1470
|
-
handler: wrap(true, async (args, peer) => receipt(hub.send(peer, String(args.to), "chat", String(args.message)
|
|
1713
|
+
handler: wrap(true, async (args, peer) => receipt(hub.send(peer, String(args.to), "chat", String(args.message), {
|
|
1714
|
+
...args.threadId !== void 0 ? { threadId: String(args.threadId) } : {},
|
|
1715
|
+
...args.ref !== void 0 ? { ref: String(args.ref) } : {}
|
|
1716
|
+
})))
|
|
1471
1717
|
},
|
|
1472
1718
|
{
|
|
1473
1719
|
name: "bridge_task",
|
|
1474
|
-
description:
|
|
1720
|
+
description: 'Delegate a structured task to another agent (peer, group, or "all"). The assignee acks with accepted | working | rejected | done | failed (bridge_ack). Track with bridge_task_status(ref) / bridge_tasks; wait for the ack with bridge_wait({ ref }). Optional timeoutMs marks the task `timeout` when the SLA expires without a terminal ack.',
|
|
1475
1721
|
inputSchema: schema(
|
|
1476
1722
|
{
|
|
1477
|
-
to: str('Target peerId, or "all" to broadcast.'),
|
|
1723
|
+
to: str('Target peerId, group id, or "all" to broadcast.'),
|
|
1478
1724
|
prompt: str("What the receiving agent should do."),
|
|
1479
1725
|
context: optStr("Optional background information for the task."),
|
|
1480
|
-
deliverable: optStr("Optional expected deliverable description.")
|
|
1726
|
+
deliverable: optStr("Optional expected deliverable description."),
|
|
1727
|
+
timeoutMs: int("Optional soft SLA in ms; hub marks the task timeout when it expires."),
|
|
1728
|
+
threadId: optStr("Optional conversation / workflow thread id.")
|
|
1481
1729
|
},
|
|
1482
1730
|
["to", "prompt"]
|
|
1483
1731
|
),
|
|
1484
1732
|
handler: wrap(true, async (args, peer) => receipt(hub.sendTask(peer, String(args.to), {
|
|
1485
1733
|
prompt: String(args.prompt),
|
|
1486
1734
|
...args.context !== void 0 ? { context: String(args.context) } : {},
|
|
1487
|
-
...args.deliverable !== void 0 ? { deliverable: String(args.deliverable) } : {}
|
|
1735
|
+
...args.deliverable !== void 0 ? { deliverable: String(args.deliverable) } : {},
|
|
1736
|
+
...args.timeoutMs !== void 0 ? { timeoutMs: Number(args.timeoutMs) } : {},
|
|
1737
|
+
...args.threadId !== void 0 ? { threadId: String(args.threadId) } : {}
|
|
1488
1738
|
})))
|
|
1489
1739
|
},
|
|
1490
1740
|
{
|
|
1491
1741
|
name: "bridge_ack",
|
|
1492
|
-
description: "Acknowledge a message received from another agent (usually a delegated task): accepted | rejected | done | failed. The ack is routed back to the original sender of `ref` and updates the hub task ledger (see bridge_task_status / bridge_tasks).",
|
|
1742
|
+
description: "Acknowledge a message received from another agent (usually a delegated task): accepted | working | rejected | done | failed. The ack is routed back to the original sender of `ref` and updates the hub task ledger (see bridge_task_status / bridge_tasks). `working` + `progress` (0-100) report intermediate state without finishing the task. Terminal statuses lock the task; later acks are recorded but do not reopen it.",
|
|
1493
1743
|
inputSchema: schema(
|
|
1494
1744
|
{
|
|
1495
1745
|
ref: str("The id of the message being acknowledged."),
|
|
1496
|
-
status: { type: "string", enum: ["accepted", "rejected", "done", "failed"], description: "acknowledgement status." },
|
|
1497
|
-
note: optStr("Optional explanation for the acknowledgement.")
|
|
1746
|
+
status: { type: "string", enum: ["accepted", "working", "rejected", "done", "failed"], description: "acknowledgement status." },
|
|
1747
|
+
note: optStr("Optional explanation for the acknowledgement."),
|
|
1748
|
+
progress: int("Optional 0-100 progress (meaningful with status=working).")
|
|
1498
1749
|
},
|
|
1499
1750
|
["ref", "status"]
|
|
1500
1751
|
),
|
|
1501
1752
|
handler: wrap(true, async (args, peer) => {
|
|
1502
1753
|
const status = String(args.status);
|
|
1503
|
-
const valid = ["accepted", "rejected", "done", "failed"];
|
|
1754
|
+
const valid = ["accepted", "working", "rejected", "done", "failed"];
|
|
1504
1755
|
if (!valid.includes(status)) {
|
|
1505
1756
|
throw new Error(`invalid ack status: ${status} (expected ${valid.join(" | ")})`);
|
|
1506
1757
|
}
|
|
1507
|
-
return receipt(hub.sendAck(peer, String(args.ref), {
|
|
1758
|
+
return receipt(hub.sendAck(peer, String(args.ref), {
|
|
1759
|
+
status,
|
|
1760
|
+
...args.note !== void 0 ? { note: String(args.note) } : {},
|
|
1761
|
+
...args.progress !== void 0 ? { progress: Number(args.progress) } : {}
|
|
1762
|
+
}));
|
|
1508
1763
|
})
|
|
1509
1764
|
},
|
|
1510
1765
|
{
|
|
1511
1766
|
name: "bridge_tasks",
|
|
1512
|
-
description: "List delegated tasks involving you, newest first. role=sent = tasks you assigned; role=received = tasks assigned to you (or broadcast); status filters the ledger state (pending/accepted/rejected/done/failed). Each row carries the full ack timeline.",
|
|
1767
|
+
description: "List delegated tasks involving you, newest first. role=sent = tasks you assigned; role=received = tasks assigned to you (or broadcast/group); status filters the ledger state (pending/accepted/working/rejected/done/failed/timeout). Each row carries the full ack timeline and per-assignee state.",
|
|
1513
1768
|
inputSchema: schema({
|
|
1514
1769
|
role: { type: "string", enum: ["sent", "received", "all"], description: "Which side of the task to list (default all)." },
|
|
1515
|
-
status: { type: "string", enum: ["pending", "accepted", "rejected", "done", "failed"], description: "Only tasks whose current ledger status matches." },
|
|
1770
|
+
status: { type: "string", enum: ["pending", "accepted", "working", "rejected", "done", "failed", "timeout"], description: "Only tasks whose current ledger status matches." },
|
|
1771
|
+
peer: optStr('Manager only: "all" or another peerId for the admin board.'),
|
|
1516
1772
|
limit: int("How many tasks to return (default 50).")
|
|
1517
1773
|
}),
|
|
1518
|
-
handler: wrap(true, async (args, peer) => {
|
|
1774
|
+
handler: wrap(true, async (args, peer, sessionId) => {
|
|
1519
1775
|
const role = args.role === void 0 ? void 0 : String(args.role);
|
|
1520
1776
|
if (role !== void 0 && role !== "sent" && role !== "received" && role !== "all") {
|
|
1521
1777
|
throw new Error(`invalid role: ${role} (expected sent | received | all)`);
|
|
1522
1778
|
}
|
|
1523
1779
|
const status = args.status === void 0 ? void 0 : String(args.status);
|
|
1524
|
-
const validStatus = ["pending", "accepted", "rejected", "done", "failed"];
|
|
1780
|
+
const validStatus = ["pending", "accepted", "working", "rejected", "done", "failed", "timeout"];
|
|
1525
1781
|
if (status !== void 0 && !validStatus.includes(status)) {
|
|
1526
1782
|
throw new Error(`invalid status: ${status} (expected ${validStatus.join(" | ")})`);
|
|
1527
1783
|
}
|
|
1528
|
-
|
|
1784
|
+
let target = peer;
|
|
1785
|
+
if (args.peer !== void 0) {
|
|
1786
|
+
target = String(args.peer);
|
|
1787
|
+
if (target !== peer) requireManager(peer, `listing tasks of '${target}'`, sessionId);
|
|
1788
|
+
}
|
|
1789
|
+
const tasks = hub.listTasks(target === "all" ? "all" : target, {
|
|
1529
1790
|
...role !== void 0 ? { role } : {},
|
|
1530
1791
|
...status !== void 0 ? { status } : {},
|
|
1531
1792
|
...args.limit !== void 0 ? { limit: Number(args.limit) } : {}
|
|
@@ -1535,7 +1796,7 @@ function hubTools(hub, registry, options) {
|
|
|
1535
1796
|
},
|
|
1536
1797
|
{
|
|
1537
1798
|
name: "bridge_task_status",
|
|
1538
|
-
description: "Status of one delegated task: current ledger state (pending \u2192 accepted \u2192 done/failed, or rejected), the original prompt/context/deliverable, and every ack event (who, status, note, when). Use the task message id (the same id you pass as bridge_ack ref).",
|
|
1799
|
+
description: "Status of one delegated task: current ledger state (pending \u2192 accepted/working \u2192 done/failed/timeout, or rejected), the original prompt/context/deliverable, per-assignee state, and every ack event (who, status, note, progress, when). Use the task message id (the same id you pass as bridge_ack ref).",
|
|
1539
1800
|
inputSchema: schema({ ref: str("Task message id (the id returned by bridge_task, also used as ack ref).") }, ["ref"]),
|
|
1540
1801
|
handler: wrap(true, async (args, peer, sessionId) => {
|
|
1541
1802
|
const ref = String(args.ref);
|
|
@@ -1549,10 +1810,11 @@ function hubTools(hub, registry, options) {
|
|
|
1549
1810
|
},
|
|
1550
1811
|
{
|
|
1551
1812
|
name: "bridge_wait",
|
|
1552
|
-
description: "Wait (long-poll) for the next message addressed to you. Resolves immediately when one is queued; otherwise blocks until one arrives or the timeout fires. `from` narrows to one sender. `ref` waits specifically for the ack of that task id. Loop this tool to hold a real-time conversation.",
|
|
1813
|
+
description: "Wait (long-poll) for the next message addressed to you. Resolves immediately when one is queued; otherwise blocks until one arrives or the timeout fires. `from` narrows to one sender. `ref` waits specifically for the ack of that task id. `threadId` narrows to one conversation thread. Loop this tool to hold a real-time conversation.",
|
|
1553
1814
|
inputSchema: schema({
|
|
1554
1815
|
from: optStr("Only wait for messages from this peerId."),
|
|
1555
1816
|
ref: optStr("Only wait for the ack of this task message id (bridge_task id)."),
|
|
1817
|
+
threadId: optStr("Only wait for messages carrying this threadId."),
|
|
1556
1818
|
timeoutMs: int(`Max wait in milliseconds (default ${DEFAULT_WAIT_MS}, ceiling is server waitTimeoutMs).`)
|
|
1557
1819
|
}),
|
|
1558
1820
|
handler: wrap(true, async (args, peer) => presentWait(await hub.wait(
|
|
@@ -1560,7 +1822,8 @@ function hubTools(hub, registry, options) {
|
|
|
1560
1822
|
args.timeoutMs === void 0 ? options.defaultWaitMs ?? DEFAULT_WAIT_MS : Number(args.timeoutMs),
|
|
1561
1823
|
args.from === void 0 ? void 0 : String(args.from),
|
|
1562
1824
|
void 0,
|
|
1563
|
-
args.ref === void 0 ? void 0 : String(args.ref)
|
|
1825
|
+
args.ref === void 0 ? void 0 : String(args.ref),
|
|
1826
|
+
args.threadId === void 0 ? void 0 : String(args.threadId)
|
|
1564
1827
|
)))
|
|
1565
1828
|
},
|
|
1566
1829
|
{
|
|
@@ -2380,7 +2643,7 @@ function readBody(req) {
|
|
|
2380
2643
|
import { DatabaseSync } from "node:sqlite";
|
|
2381
2644
|
var KIND_NAMES = ["chat", "task", "notice", "ack"];
|
|
2382
2645
|
function messageToRow(message) {
|
|
2383
|
-
return [message.id, message.from, message.to, message.kind, message.content, message.ref ?? null, message.channel ?? null, message.ts];
|
|
2646
|
+
return [message.id, message.from, message.to, message.kind, message.content, message.ref ?? null, message.channel ?? null, message.threadId ?? null, message.ts];
|
|
2384
2647
|
}
|
|
2385
2648
|
function rowToMessage(row) {
|
|
2386
2649
|
const id = row.id;
|
|
@@ -2399,6 +2662,7 @@ function rowToMessage(row) {
|
|
|
2399
2662
|
content,
|
|
2400
2663
|
...typeof row.ref === "string" && row.ref !== "" ? { ref: row.ref } : {},
|
|
2401
2664
|
...typeof row.channel === "string" && row.channel !== "" ? { channel: row.channel } : {},
|
|
2665
|
+
...typeof row.thread_id === "string" && row.thread_id !== "" ? { threadId: row.thread_id } : {},
|
|
2402
2666
|
ts
|
|
2403
2667
|
};
|
|
2404
2668
|
}
|
|
@@ -2413,16 +2677,25 @@ var SQLiteStateStore = class {
|
|
|
2413
2677
|
);
|
|
2414
2678
|
CREATE TABLE IF NOT EXISTS messages (
|
|
2415
2679
|
seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL, from_peer TEXT NOT NULL,
|
|
2416
|
-
to_peer TEXT NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL, ref TEXT, channel TEXT, ts INTEGER NOT NULL
|
|
2680
|
+
to_peer TEXT NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL, ref TEXT, channel TEXT, thread_id TEXT, ts INTEGER NOT NULL
|
|
2417
2681
|
);
|
|
2418
2682
|
CREATE TABLE IF NOT EXISTS mailboxes (
|
|
2419
2683
|
peer TEXT NOT NULL, seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL, from_peer TEXT NOT NULL,
|
|
2420
|
-
to_peer TEXT NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL, ref TEXT, channel TEXT, ts INTEGER NOT NULL
|
|
2684
|
+
to_peer TEXT NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL, ref TEXT, channel TEXT, thread_id TEXT, ts INTEGER NOT NULL
|
|
2421
2685
|
);
|
|
2422
2686
|
CREATE TABLE IF NOT EXISTS groups (
|
|
2423
2687
|
id TEXT PRIMARY KEY, name TEXT, members TEXT NOT NULL, created_by TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
2424
2688
|
);
|
|
2689
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
2690
|
+
id TEXT PRIMARY KEY, body TEXT NOT NULL, updated_at INTEGER NOT NULL
|
|
2691
|
+
);
|
|
2425
2692
|
`);
|
|
2693
|
+
for (const table of ["messages", "mailboxes"]) {
|
|
2694
|
+
try {
|
|
2695
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN thread_id TEXT`);
|
|
2696
|
+
} catch {
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2426
2699
|
log.info(`sqlite state open: ${file}`);
|
|
2427
2700
|
}
|
|
2428
2701
|
db;
|
|
@@ -2431,17 +2704,17 @@ var SQLiteStateStore = class {
|
|
|
2431
2704
|
appendMessage(message) {
|
|
2432
2705
|
this.buffer.push(message);
|
|
2433
2706
|
}
|
|
2434
|
-
/** Insert buffered messages and re-snapshot mailboxes/profiles/groups. */
|
|
2707
|
+
/** Insert buffered messages and re-snapshot mailboxes/profiles/groups/tasks. */
|
|
2435
2708
|
flush(snapshot) {
|
|
2436
2709
|
const insert = this.db.prepare(
|
|
2437
|
-
"INSERT INTO messages (id, from_peer, to_peer, kind, content, ref, channel, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
2710
|
+
"INSERT INTO messages (id, from_peer, to_peer, kind, content, ref, channel, thread_id, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
2438
2711
|
);
|
|
2439
2712
|
for (const message of this.buffer.splice(0, this.buffer.length)) {
|
|
2440
2713
|
insert.run(...messageToRow(message));
|
|
2441
2714
|
}
|
|
2442
2715
|
this.db.exec("DELETE FROM mailboxes");
|
|
2443
2716
|
const insertMail = this.db.prepare(
|
|
2444
|
-
"INSERT INTO mailboxes (peer, id, from_peer, to_peer, kind, content, ref, channel, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
2717
|
+
"INSERT INTO mailboxes (peer, id, from_peer, to_peer, kind, content, ref, channel, thread_id, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
2445
2718
|
);
|
|
2446
2719
|
for (const [peer, queue] of Object.entries(snapshot.mailboxes)) {
|
|
2447
2720
|
for (const message of queue) insertMail.run(peer, ...messageToRow(message));
|
|
@@ -2460,6 +2733,13 @@ var SQLiteStateStore = class {
|
|
|
2460
2733
|
for (const group of snapshot.groups) {
|
|
2461
2734
|
insertGroup.run(group.id, group.name ?? null, JSON.stringify(group.members), group.createdBy, group.createdAt);
|
|
2462
2735
|
}
|
|
2736
|
+
this.db.exec("DELETE FROM tasks");
|
|
2737
|
+
const insertTask = this.db.prepare(
|
|
2738
|
+
"INSERT INTO tasks (id, body, updated_at) VALUES (?, ?, ?)"
|
|
2739
|
+
);
|
|
2740
|
+
for (const task of snapshot.tasks) {
|
|
2741
|
+
insertTask.run(task.id, JSON.stringify(task), task.updatedAt);
|
|
2742
|
+
}
|
|
2463
2743
|
const row = this.db.prepare("SELECT COUNT(*) AS n FROM messages").get();
|
|
2464
2744
|
const total = Number(row.n);
|
|
2465
2745
|
if (total > this.historyLimit * 2) {
|
|
@@ -2507,7 +2787,16 @@ var SQLiteStateStore = class {
|
|
|
2507
2787
|
} catch {
|
|
2508
2788
|
}
|
|
2509
2789
|
}
|
|
2510
|
-
|
|
2790
|
+
const tasks = [];
|
|
2791
|
+
for (const row of this.db.prepare("SELECT * FROM tasks").all()) {
|
|
2792
|
+
if (typeof row.body !== "string") continue;
|
|
2793
|
+
try {
|
|
2794
|
+
const parsed = JSON.parse(row.body);
|
|
2795
|
+
if (typeof parsed === "object" && parsed !== null) tasks.push(parsed);
|
|
2796
|
+
} catch {
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
return { profiles, messages, mailboxes, groups, tasks };
|
|
2511
2800
|
}
|
|
2512
2801
|
/** Final flush + close (hub shutdown). */
|
|
2513
2802
|
close(snapshot) {
|
|
@@ -2518,7 +2807,7 @@ var SQLiteStateStore = class {
|
|
|
2518
2807
|
|
|
2519
2808
|
// src/index.ts
|
|
2520
2809
|
var SERVER_NAME = "agent-comm-hub";
|
|
2521
|
-
var SERVER_VERSION = "0.
|
|
2810
|
+
var SERVER_VERSION = "0.9.0";
|
|
2522
2811
|
var DEFAULT_HOST = "127.0.0.1";
|
|
2523
2812
|
var DEFAULT_PORT = 18764;
|
|
2524
2813
|
var DEFAULT_PATH = "/mcp";
|
|
@@ -2621,7 +2910,7 @@ function startHub(config = {}, log = console) {
|
|
|
2621
2910
|
if (dbDirty && dbStore !== void 0) {
|
|
2622
2911
|
dbDirty = false;
|
|
2623
2912
|
try {
|
|
2624
|
-
dbStore.flush({ mailboxes: hub.exportMailboxes(), profiles: hub.exportProfiles(), groups: hub.exportGroups() });
|
|
2913
|
+
dbStore.flush({ mailboxes: hub.exportMailboxes(), profiles: hub.exportProfiles(), groups: hub.exportGroups(), tasks: hub.exportTasks() });
|
|
2625
2914
|
} catch (error) {
|
|
2626
2915
|
log.warn(`sqlite flush failed: ${error.message}`);
|
|
2627
2916
|
}
|
|
@@ -2651,6 +2940,7 @@ function startHub(config = {}, log = console) {
|
|
|
2651
2940
|
if (dbStore !== void 0) {
|
|
2652
2941
|
dbStore.appendMessage(message);
|
|
2653
2942
|
dbDirty = true;
|
|
2943
|
+
scheduleSave();
|
|
2654
2944
|
}
|
|
2655
2945
|
try {
|
|
2656
2946
|
pushRelay(message);
|
|
@@ -2661,6 +2951,15 @@ function startHub(config = {}, log = console) {
|
|
|
2661
2951
|
onMailboxesChanged: () => {
|
|
2662
2952
|
if (dbStore === void 0) return;
|
|
2663
2953
|
dbDirty = true;
|
|
2954
|
+
scheduleSave();
|
|
2955
|
+
},
|
|
2956
|
+
// Task ledger transitions (ack folding, SLA timeout, first delivery) are
|
|
2957
|
+
// their own persistence trigger: without it a crash between two roster
|
|
2958
|
+
// events loses the ledger state the admin board reads back.
|
|
2959
|
+
onTaskChanged: () => {
|
|
2960
|
+
if (dbStore === void 0) return;
|
|
2961
|
+
dbDirty = true;
|
|
2962
|
+
scheduleSave();
|
|
2664
2963
|
},
|
|
2665
2964
|
onGroupsChanged: () => {
|
|
2666
2965
|
if (dbStore === void 0) return;
|
|
@@ -2711,6 +3010,7 @@ function startHub(config = {}, log = console) {
|
|
|
2711
3010
|
const restoredMessages = hub.importHistory(state.messages);
|
|
2712
3011
|
const restoredQueued = hub.importMailboxes(state.mailboxes);
|
|
2713
3012
|
const restoredGroups = hub.importGroups(state.groups);
|
|
3013
|
+
const restoredTasks = hub.importTasks(state.tasks);
|
|
2714
3014
|
if (restoredMessages + restoredQueued + restoredGroups > 0) {
|
|
2715
3015
|
log.info(`state restored from ${resolved.db}: ${restoredMessages} message(s), ${restoredQueued} queued, ${restoredGroups} group(s)`);
|
|
2716
3016
|
dbDirty = true;
|
|
@@ -2813,7 +3113,7 @@ function startHub(config = {}, log = console) {
|
|
|
2813
3113
|
flushSaves();
|
|
2814
3114
|
if (dbStore !== void 0) {
|
|
2815
3115
|
try {
|
|
2816
|
-
dbStore.close({ mailboxes: hub.exportMailboxes(), profiles: hub.exportProfiles(), groups: hub.exportGroups() });
|
|
3116
|
+
dbStore.close({ mailboxes: hub.exportMailboxes(), profiles: hub.exportProfiles(), groups: hub.exportGroups(), tasks: hub.exportTasks() });
|
|
2817
3117
|
} catch (error) {
|
|
2818
3118
|
log.warn(`sqlite close failed: ${error.message}`);
|
|
2819
3119
|
}
|
|
@@ -2834,6 +3134,7 @@ export {
|
|
|
2834
3134
|
DEFAULT_PORT,
|
|
2835
3135
|
HerdrCtl,
|
|
2836
3136
|
KINDS,
|
|
3137
|
+
MAX_HOP,
|
|
2837
3138
|
McpStreamableHttpServer,
|
|
2838
3139
|
PEER_ID_PATTERN,
|
|
2839
3140
|
SERVER_NAME,
|
|
@@ -2841,13 +3142,16 @@ export {
|
|
|
2841
3142
|
SQLiteStateStore,
|
|
2842
3143
|
SessionRegistry,
|
|
2843
3144
|
TERMINAL_TASK_STATUSES,
|
|
3145
|
+
ackToTaskStatus,
|
|
2844
3146
|
addToken,
|
|
3147
|
+
aggregateTaskStatus,
|
|
2845
3148
|
autoRegisterPeer,
|
|
2846
3149
|
decodeContent,
|
|
2847
3150
|
encodeContent,
|
|
2848
3151
|
generateToken,
|
|
2849
3152
|
hubTools,
|
|
2850
3153
|
loadTokenFile,
|
|
3154
|
+
nextTaskStatus,
|
|
2851
3155
|
present,
|
|
2852
3156
|
readTokens,
|
|
2853
3157
|
removeToken,
|