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/cli.js
CHANGED
|
@@ -424,7 +424,40 @@ import { randomUUID } from "node:crypto";
|
|
|
424
424
|
var KINDS = ["chat", "task", "notice", "ack"];
|
|
425
425
|
var PEER_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
426
426
|
var BROADCAST = "all";
|
|
427
|
-
var
|
|
427
|
+
var MAX_HOP = 5;
|
|
428
|
+
var TERMINAL_TASK_STATUSES = ["rejected", "done", "failed", "timeout"];
|
|
429
|
+
function ackToTaskStatus(ack) {
|
|
430
|
+
return ack;
|
|
431
|
+
}
|
|
432
|
+
function nextTaskStatus(current, ack) {
|
|
433
|
+
if (TERMINAL_TASK_STATUSES.includes(current)) return null;
|
|
434
|
+
const next = ackToTaskStatus(ack);
|
|
435
|
+
switch (current) {
|
|
436
|
+
case "pending":
|
|
437
|
+
return next;
|
|
438
|
+
case "accepted":
|
|
439
|
+
return next;
|
|
440
|
+
case "working":
|
|
441
|
+
if (next === "accepted") return "working";
|
|
442
|
+
return next;
|
|
443
|
+
default:
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function aggregateTaskStatus(assignees) {
|
|
448
|
+
if (assignees.length === 0) return "pending";
|
|
449
|
+
const statuses = assignees.map((a) => a.status);
|
|
450
|
+
const active = statuses.filter((s) => s === "pending" || s === "accepted" || s === "working");
|
|
451
|
+
if (active.length > 0) {
|
|
452
|
+
if (active.some((s) => s === "working")) return "working";
|
|
453
|
+
if (active.some((s) => s === "accepted")) return "accepted";
|
|
454
|
+
return "pending";
|
|
455
|
+
}
|
|
456
|
+
if (statuses.every((s) => s === "done")) return "done";
|
|
457
|
+
if (statuses.some((s) => s === "failed")) return "failed";
|
|
458
|
+
if (statuses.some((s) => s === "timeout")) return "timeout";
|
|
459
|
+
return "rejected";
|
|
460
|
+
}
|
|
428
461
|
function decodeContent(kind, content) {
|
|
429
462
|
if (kind !== "task" && kind !== "ack") return content;
|
|
430
463
|
try {
|
|
@@ -458,10 +491,14 @@ var AgentHub = class {
|
|
|
458
491
|
groups = /* @__PURE__ */ new Map();
|
|
459
492
|
/** Task ledger: task message id → record (status + ack timeline). */
|
|
460
493
|
taskLedger = /* @__PURE__ */ new Map();
|
|
494
|
+
/** SLA timers keyed by task id (cleared when the task turns terminal). */
|
|
495
|
+
taskTimers = /* @__PURE__ */ new Map();
|
|
461
496
|
gcTimer;
|
|
462
497
|
/** Stop the idle GC (call when the hub shuts down). */
|
|
463
498
|
dispose() {
|
|
464
499
|
if (this.gcTimer !== void 0) clearInterval(this.gcTimer);
|
|
500
|
+
for (const timer of this.taskTimers.values()) clearTimeout(timer);
|
|
501
|
+
this.taskTimers.clear();
|
|
465
502
|
}
|
|
466
503
|
gcTick(idleTimeoutMs) {
|
|
467
504
|
const now = Date.now();
|
|
@@ -625,29 +662,49 @@ var AgentHub = class {
|
|
|
625
662
|
return last !== void 0 && Date.now() - last < (this.options.connectedWindowMs ?? 3e4);
|
|
626
663
|
}
|
|
627
664
|
/** Send a chat/notice text message to `to` (or {@link BROADCAST}). */
|
|
628
|
-
send(from, to, kind, content) {
|
|
629
|
-
return this.route(from, to, kind, content);
|
|
665
|
+
send(from, to, kind, content, opts) {
|
|
666
|
+
return this.route(from, to, kind, content, opts?.ref, opts?.threadId);
|
|
630
667
|
}
|
|
631
|
-
/** Send a structured task message to `to` (
|
|
632
|
-
* a ledger entry so the sender can later query status /
|
|
668
|
+
/** Send a structured task message to `to` (peer, {@link BROADCAST}, or a
|
|
669
|
+
* group id). Records a ledger entry so the sender can later query status /
|
|
670
|
+
* wait for acks. */
|
|
633
671
|
sendTask(from, to, task) {
|
|
634
|
-
const
|
|
672
|
+
const threadId = task.threadId;
|
|
673
|
+
const message = this.route(from, to, "task", JSON.stringify(task), void 0, threadId);
|
|
635
674
|
this.recordTask(message, task);
|
|
636
675
|
return message;
|
|
637
676
|
}
|
|
638
677
|
/** Send an acknowledgement back to the sender of `ref`. Updates the task
|
|
639
|
-
* ledger when `ref` is a known task
|
|
678
|
+
* ledger when `ref` is a known task (looked up in the ledger first, so acks
|
|
679
|
+
* survive history-ring eviction); chat/notice refs still route via history. */
|
|
640
680
|
sendAck(from, ref, ack) {
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
681
|
+
const task = this.taskLedger.get(ref);
|
|
682
|
+
let target;
|
|
683
|
+
if (task !== void 0) {
|
|
684
|
+
target = task.from;
|
|
685
|
+
} else {
|
|
686
|
+
const original = this.historyRing.findLast((message2) => message2.id === ref);
|
|
687
|
+
if (!original) throw new Error(`cannot ack unknown message: ${ref}`);
|
|
688
|
+
target = original.from;
|
|
689
|
+
}
|
|
690
|
+
const message = this.route(from, target, "ack", JSON.stringify(ack), ref);
|
|
644
691
|
this.applyAckToLedger(message, from, ref, ack);
|
|
645
692
|
return message;
|
|
646
693
|
}
|
|
694
|
+
/** Resolve the peer ids a task is assigned to (broadcast / group fan-out). */
|
|
695
|
+
resolveAssignees(from, to) {
|
|
696
|
+
if (to === BROADCAST) return this.peers().filter((peer) => peer !== from);
|
|
697
|
+
const group = this.groups.get(to);
|
|
698
|
+
if (group !== void 0) return group.members.filter((member) => member !== from);
|
|
699
|
+
return [to];
|
|
700
|
+
}
|
|
647
701
|
/** Create or refresh the ledger row for a newly sent task. */
|
|
648
702
|
recordTask(message, task) {
|
|
649
703
|
const now = message.ts;
|
|
650
|
-
this.
|
|
704
|
+
const assigneeIds = this.resolveAssignees(message.from, message.to);
|
|
705
|
+
const assignees = assigneeIds.map((peerId) => ({ peerId, status: "pending", acks: [] }));
|
|
706
|
+
const timeoutMs = typeof task.timeoutMs === "number" && Number.isFinite(task.timeoutMs) && task.timeoutMs > 0 ? Math.floor(task.timeoutMs) : void 0;
|
|
707
|
+
const record = {
|
|
651
708
|
id: message.id,
|
|
652
709
|
from: message.from,
|
|
653
710
|
to: message.to,
|
|
@@ -656,25 +713,105 @@ var AgentHub = class {
|
|
|
656
713
|
...task.deliverable !== void 0 ? { deliverable: task.deliverable } : {},
|
|
657
714
|
status: "pending",
|
|
658
715
|
acks: [],
|
|
716
|
+
assignees,
|
|
717
|
+
...timeoutMs !== void 0 ? { timeoutMs, deadlineAt: now + timeoutMs } : {},
|
|
718
|
+
...message.threadId !== void 0 ? { threadId: message.threadId } : {},
|
|
659
719
|
createdAt: now,
|
|
660
720
|
updatedAt: now
|
|
661
|
-
}
|
|
721
|
+
};
|
|
722
|
+
this.taskLedger.set(message.id, record);
|
|
723
|
+
this.scheduleTaskTimeout(record);
|
|
662
724
|
this.evictOverflowTasks();
|
|
725
|
+
this.options.onTaskChanged?.(this.taskOf(record.id));
|
|
726
|
+
}
|
|
727
|
+
/** Arm the SLA timer for a task that supplied `timeoutMs`. */
|
|
728
|
+
scheduleTaskTimeout(record) {
|
|
729
|
+
if (record.deadlineAt === void 0) return;
|
|
730
|
+
const delay = Math.max(0, record.deadlineAt - Date.now());
|
|
731
|
+
const timer = setTimeout(() => this.fireTaskTimeout(record.id), delay);
|
|
732
|
+
timer.unref?.();
|
|
733
|
+
this.taskTimers.set(record.id, timer);
|
|
734
|
+
}
|
|
735
|
+
/** Mark a non-terminal task `timeout` and leave a system timeline event. */
|
|
736
|
+
fireTaskTimeout(taskId) {
|
|
737
|
+
this.taskTimers.delete(taskId);
|
|
738
|
+
const record = this.taskLedger.get(taskId);
|
|
739
|
+
if (record === void 0) return;
|
|
740
|
+
if (TERMINAL_TASK_STATUSES.includes(record.status)) return;
|
|
741
|
+
const now = Date.now();
|
|
742
|
+
const event = {
|
|
743
|
+
from: record.to,
|
|
744
|
+
status: "failed",
|
|
745
|
+
note: `task timed out after ${record.timeoutMs ?? 0}ms without a terminal ack`,
|
|
746
|
+
ts: now,
|
|
747
|
+
messageId: `timeout-${record.id}`,
|
|
748
|
+
applied: true,
|
|
749
|
+
system: true
|
|
750
|
+
};
|
|
751
|
+
record.acks.push(event);
|
|
752
|
+
for (const assignee of record.assignees) {
|
|
753
|
+
if (!TERMINAL_TASK_STATUSES.includes(assignee.status)) {
|
|
754
|
+
assignee.status = "timeout";
|
|
755
|
+
assignee.acks.push({ ...event });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
record.status = "timeout";
|
|
759
|
+
record.updatedAt = now;
|
|
760
|
+
this.clearTaskTimer(taskId);
|
|
761
|
+
this.options.onTaskChanged?.(this.taskOf(taskId));
|
|
663
762
|
}
|
|
664
|
-
|
|
665
|
-
|
|
763
|
+
clearTaskTimer(taskId) {
|
|
764
|
+
const timer = this.taskTimers.get(taskId);
|
|
765
|
+
if (timer !== void 0) {
|
|
766
|
+
clearTimeout(timer);
|
|
767
|
+
this.taskTimers.delete(taskId);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/** Note that `target` has taken delivery of a task (waiter handoff / queue). */
|
|
771
|
+
markTaskDelivered(taskId, target, ts) {
|
|
772
|
+
const record = this.taskLedger.get(taskId);
|
|
773
|
+
if (record === void 0) return;
|
|
774
|
+
const assignee = record.assignees.find((a) => a.peerId === target);
|
|
775
|
+
if (assignee === void 0 || assignee.deliveredMs !== void 0) return;
|
|
776
|
+
assignee.deliveredMs = ts;
|
|
777
|
+
record.updatedAt = ts;
|
|
778
|
+
this.options.onTaskChanged?.(this.taskOf(taskId));
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Fold an ack into the task's timeline and (when legal) its status.
|
|
782
|
+
* - Unknown refs are a no-op (chat/notice can be acked for routing without a ledger row).
|
|
783
|
+
* - Terminal tasks keep the event on the timeline with `applied: false`.
|
|
784
|
+
* - Broadcast / group tasks update the acking assignee first, then re-aggregate.
|
|
785
|
+
*/
|
|
666
786
|
applyAckToLedger(message, from, ref, ack) {
|
|
667
787
|
const record = this.taskLedger.get(ref);
|
|
668
788
|
if (record === void 0) return;
|
|
669
|
-
record.
|
|
789
|
+
const next = nextTaskStatus(record.status, ack.status);
|
|
790
|
+
const assignee = record.assignees.find((a) => a.peerId === from);
|
|
791
|
+
const assigneeNext = assignee === void 0 ? null : nextTaskStatus(assignee.status, ack.status);
|
|
792
|
+
const applied = record.assignees.length <= 1 ? next !== null : assigneeNext !== null;
|
|
793
|
+
const event = {
|
|
670
794
|
from,
|
|
671
795
|
status: ack.status,
|
|
672
796
|
...ack.note !== void 0 ? { note: ack.note } : {},
|
|
797
|
+
...typeof ack.progress === "number" && Number.isFinite(ack.progress) ? { progress: Math.max(0, Math.min(100, ack.progress)) } : {},
|
|
673
798
|
ts: message.ts,
|
|
674
|
-
messageId: message.id
|
|
675
|
-
|
|
676
|
-
|
|
799
|
+
messageId: message.id,
|
|
800
|
+
applied
|
|
801
|
+
};
|
|
802
|
+
record.acks.push(event);
|
|
803
|
+
if (assignee !== void 0 && assigneeNext !== null) {
|
|
804
|
+
assignee.status = assigneeNext;
|
|
805
|
+
assignee.acks.push({ ...event });
|
|
806
|
+
}
|
|
807
|
+
if (record.assignees.length <= 1) {
|
|
808
|
+
if (next !== null) record.status = next;
|
|
809
|
+
} else {
|
|
810
|
+
record.status = aggregateTaskStatus(record.assignees);
|
|
811
|
+
}
|
|
677
812
|
record.updatedAt = message.ts;
|
|
813
|
+
if (TERMINAL_TASK_STATUSES.includes(record.status)) this.clearTaskTimer(record.id);
|
|
814
|
+
this.options.onTaskChanged?.(this.taskOf(record.id));
|
|
678
815
|
}
|
|
679
816
|
/** Keep the ledger bounded: drop oldest terminal tasks first. */
|
|
680
817
|
evictOverflowTasks() {
|
|
@@ -683,19 +820,25 @@ var AgentHub = class {
|
|
|
683
820
|
let overflow = this.taskLedger.size - MAX_TASKS;
|
|
684
821
|
for (const record of evictable) {
|
|
685
822
|
if (overflow <= 0) break;
|
|
823
|
+
this.clearTaskTimer(record.id);
|
|
686
824
|
this.taskLedger.delete(record.id);
|
|
687
825
|
overflow--;
|
|
688
826
|
}
|
|
689
827
|
}
|
|
690
|
-
/**
|
|
828
|
+
/** Deep-ish copy of one task by message id (the ack `ref`). */
|
|
691
829
|
taskOf(ref) {
|
|
692
830
|
const record = this.taskLedger.get(ref);
|
|
693
|
-
|
|
831
|
+
if (record === void 0) return void 0;
|
|
832
|
+
return {
|
|
833
|
+
...record,
|
|
834
|
+
acks: record.acks.map((a) => ({ ...a })),
|
|
835
|
+
assignees: record.assignees.map((a) => ({ ...a, acks: a.acks.map((e) => ({ ...e })) }))
|
|
836
|
+
};
|
|
694
837
|
}
|
|
695
838
|
/**
|
|
696
839
|
* Tasks involving `peer`. `role`:
|
|
697
840
|
* - `sent` — tasks this peer delegated (`from`)
|
|
698
|
-
* - `received` — tasks addressed to this peer
|
|
841
|
+
* - `received` — tasks addressed to this peer, a group they are in, or broadcast
|
|
699
842
|
* - `all` (default) — either side
|
|
700
843
|
* Optional `status` filter matches the ledger's current status.
|
|
701
844
|
*/
|
|
@@ -704,17 +847,94 @@ var AgentHub = class {
|
|
|
704
847
|
const limit = Math.max(1, options?.limit ?? 50);
|
|
705
848
|
const out = [];
|
|
706
849
|
for (const record of this.taskLedger.values()) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
850
|
+
if (peerId !== "all") {
|
|
851
|
+
const isSent = record.from === peerId;
|
|
852
|
+
const isReceived = record.to === peerId || record.to === BROADCAST || record.assignees.some((a) => a.peerId === peerId) || (this.groups.get(record.to)?.members.includes(peerId) ?? false);
|
|
853
|
+
if (role === "sent" && !isSent) continue;
|
|
854
|
+
if (role === "received" && !isReceived) continue;
|
|
855
|
+
if (role === "all" && !isSent && !isReceived) continue;
|
|
856
|
+
}
|
|
712
857
|
if (options?.status !== void 0 && record.status !== options.status) continue;
|
|
713
|
-
|
|
858
|
+
const copy = this.taskOf(record.id);
|
|
859
|
+
if (copy !== void 0) out.push(copy);
|
|
714
860
|
}
|
|
715
861
|
out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
716
862
|
return out.slice(0, limit);
|
|
717
863
|
}
|
|
864
|
+
/** Snapshot of every task (persistence). */
|
|
865
|
+
exportTasks() {
|
|
866
|
+
return [...this.taskLedger.values()].map((record) => this.taskOf(record.id)).filter(Boolean);
|
|
867
|
+
}
|
|
868
|
+
/** Restore the task ledger at boot (and re-arm SLA timers). */
|
|
869
|
+
importTasks(tasks) {
|
|
870
|
+
if (!Array.isArray(tasks)) return 0;
|
|
871
|
+
let count = 0;
|
|
872
|
+
for (const raw of tasks) {
|
|
873
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
874
|
+
const t = raw;
|
|
875
|
+
if (typeof t.id !== "string" || typeof t.from !== "string" || typeof t.to !== "string" || typeof t.prompt !== "string") continue;
|
|
876
|
+
if (typeof t.status !== "string" || !TERMINAL_TASK_STATUSES.includes(t.status) && t.status !== "pending" && t.status !== "accepted" && t.status !== "working") continue;
|
|
877
|
+
const acks = Array.isArray(t.acks) ? t.acks.flatMap((rawAck) => {
|
|
878
|
+
if (typeof rawAck !== "object" || rawAck === null) return [];
|
|
879
|
+
const a = rawAck;
|
|
880
|
+
if (typeof a.from !== "string" || typeof a.status !== "string" || typeof a.ts !== "number" || typeof a.messageId !== "string") return [];
|
|
881
|
+
return [{
|
|
882
|
+
from: a.from,
|
|
883
|
+
status: a.status,
|
|
884
|
+
...typeof a.note === "string" ? { note: a.note } : {},
|
|
885
|
+
...typeof a.progress === "number" ? { progress: a.progress } : {},
|
|
886
|
+
ts: a.ts,
|
|
887
|
+
messageId: a.messageId,
|
|
888
|
+
applied: a.applied !== false,
|
|
889
|
+
...a.system === true ? { system: true } : {}
|
|
890
|
+
}];
|
|
891
|
+
}) : [];
|
|
892
|
+
const assignees = Array.isArray(t.assignees) && t.assignees.length > 0 ? t.assignees.flatMap((rawA) => {
|
|
893
|
+
if (typeof rawA !== "object" || rawA === null) return [];
|
|
894
|
+
const a = rawA;
|
|
895
|
+
if (typeof a.peerId !== "string" || typeof a.status !== "string") return [];
|
|
896
|
+
return [{
|
|
897
|
+
peerId: a.peerId,
|
|
898
|
+
status: a.status,
|
|
899
|
+
...typeof a.deliveredMs === "number" ? { deliveredMs: a.deliveredMs } : {},
|
|
900
|
+
acks: Array.isArray(a.acks) ? a.acks.flatMap((rawE) => {
|
|
901
|
+
if (typeof rawE !== "object" || rawE === null) return [];
|
|
902
|
+
const e = rawE;
|
|
903
|
+
if (typeof e.from !== "string" || typeof e.status !== "string" || typeof e.ts !== "number" || typeof e.messageId !== "string") return [];
|
|
904
|
+
return [{
|
|
905
|
+
from: e.from,
|
|
906
|
+
status: e.status,
|
|
907
|
+
...typeof e.note === "string" ? { note: e.note } : {},
|
|
908
|
+
ts: e.ts,
|
|
909
|
+
messageId: e.messageId,
|
|
910
|
+
applied: e.applied !== false
|
|
911
|
+
}];
|
|
912
|
+
}) : []
|
|
913
|
+
}];
|
|
914
|
+
}) : [{ peerId: t.to, status: t.status, acks: [] }];
|
|
915
|
+
const record = {
|
|
916
|
+
id: t.id,
|
|
917
|
+
from: t.from,
|
|
918
|
+
to: t.to,
|
|
919
|
+
prompt: t.prompt,
|
|
920
|
+
...typeof t.context === "string" ? { context: t.context } : {},
|
|
921
|
+
...typeof t.deliverable === "string" ? { deliverable: t.deliverable } : {},
|
|
922
|
+
status: t.status,
|
|
923
|
+
acks,
|
|
924
|
+
assignees,
|
|
925
|
+
...typeof t.timeoutMs === "number" ? { timeoutMs: t.timeoutMs } : {},
|
|
926
|
+
...typeof t.deadlineAt === "number" ? { deadlineAt: t.deadlineAt } : {},
|
|
927
|
+
...typeof t.threadId === "string" ? { threadId: t.threadId } : {},
|
|
928
|
+
createdAt: typeof t.createdAt === "number" ? t.createdAt : Date.now(),
|
|
929
|
+
updatedAt: typeof t.updatedAt === "number" ? t.updatedAt : Date.now()
|
|
930
|
+
};
|
|
931
|
+
this.taskLedger.set(record.id, record);
|
|
932
|
+
this.scheduleTaskTimeout(record);
|
|
933
|
+
count++;
|
|
934
|
+
}
|
|
935
|
+
this.evictOverflowTasks();
|
|
936
|
+
return count;
|
|
937
|
+
}
|
|
718
938
|
/** Recent messages involving `peer` (inbound and outbound, plus group
|
|
719
939
|
* channels the peer belongs to), newest first. */
|
|
720
940
|
history(peerId, limit) {
|
|
@@ -798,20 +1018,7 @@ var AgentHub = class {
|
|
|
798
1018
|
const group = this.groups.get(groupId);
|
|
799
1019
|
if (group === void 0) throw new Error(`unknown group: ${groupId}`);
|
|
800
1020
|
if (!group.members.includes(from)) throw new Error(`peer '${from}' is not a member of group '${groupId}'`);
|
|
801
|
-
|
|
802
|
-
id: randomUUID(),
|
|
803
|
-
from,
|
|
804
|
-
to: groupId,
|
|
805
|
-
kind: "chat",
|
|
806
|
-
content,
|
|
807
|
-
channel: groupId,
|
|
808
|
-
ts: Date.now()
|
|
809
|
-
};
|
|
810
|
-
this.lastSeen.set(from, message.ts);
|
|
811
|
-
for (const member of group.members) {
|
|
812
|
-
if (member !== from && this.lastSeen.has(member)) this.deliver(member, message);
|
|
813
|
-
}
|
|
814
|
-
return message;
|
|
1021
|
+
return this.route(from, groupId, "chat", content);
|
|
815
1022
|
}
|
|
816
1023
|
/** Snapshot of every group for persistence. */
|
|
817
1024
|
exportGroups() {
|
|
@@ -859,6 +1066,7 @@ var AgentHub = class {
|
|
|
859
1066
|
content: m.content,
|
|
860
1067
|
...typeof m.ref === "string" ? { ref: m.ref } : {},
|
|
861
1068
|
...typeof m.channel === "string" ? { channel: m.channel } : {},
|
|
1069
|
+
...typeof m.threadId === "string" ? { threadId: m.threadId } : {},
|
|
862
1070
|
ts: m.ts
|
|
863
1071
|
});
|
|
864
1072
|
}
|
|
@@ -892,6 +1100,7 @@ var AgentHub = class {
|
|
|
892
1100
|
content: m.content,
|
|
893
1101
|
...typeof m.ref === "string" ? { ref: m.ref } : {},
|
|
894
1102
|
...typeof m.channel === "string" ? { channel: m.channel } : {},
|
|
1103
|
+
...typeof m.threadId === "string" ? { threadId: m.threadId } : {},
|
|
895
1104
|
ts: m.ts
|
|
896
1105
|
});
|
|
897
1106
|
}
|
|
@@ -943,11 +1152,12 @@ var AgentHub = class {
|
|
|
943
1152
|
* a multi-message burst (or a reconnect with a full queue) would silently
|
|
944
1153
|
* lose messages that never reach wait/poll again.
|
|
945
1154
|
*/
|
|
946
|
-
wait(peerId, timeoutMs, from, signal, ref) {
|
|
1155
|
+
wait(peerId, timeoutMs, from, signal, ref, threadId) {
|
|
947
1156
|
const startedAt = Date.now();
|
|
948
1157
|
const matches = (message) => {
|
|
949
1158
|
if (from !== void 0 && message.from !== from) return false;
|
|
950
1159
|
if (ref !== void 0 && !(message.kind === "ack" && message.ref === ref)) return false;
|
|
1160
|
+
if (threadId !== void 0 && message.threadId !== threadId) return false;
|
|
951
1161
|
return true;
|
|
952
1162
|
};
|
|
953
1163
|
const queue = this.queues.get(peerId);
|
|
@@ -992,15 +1202,28 @@ var AgentHub = class {
|
|
|
992
1202
|
timer,
|
|
993
1203
|
onAbort,
|
|
994
1204
|
...from !== void 0 ? { from } : {},
|
|
995
|
-
...ref !== void 0 ? { ref } : {}
|
|
1205
|
+
...ref !== void 0 ? { ref } : {},
|
|
1206
|
+
...threadId !== void 0 ? { threadId } : {}
|
|
996
1207
|
});
|
|
997
1208
|
this.waiters.set(peerId, list);
|
|
998
1209
|
});
|
|
999
1210
|
}
|
|
1000
|
-
/** Create a message from `from` addressed to `to` and deliver it.
|
|
1001
|
-
|
|
1211
|
+
/** Create a message from `from` addressed to `to` and deliver it. `to` may
|
|
1212
|
+
* be a peer id, {@link BROADCAST}, or a group id. */
|
|
1213
|
+
route(from, to, kind, content, ref, threadId) {
|
|
1002
1214
|
if (!this.lastSeen.has(from)) throw new Error(`sender not registered: ${from}`);
|
|
1003
|
-
|
|
1215
|
+
const group = this.groups.get(to);
|
|
1216
|
+
if (to !== BROADCAST && group === void 0 && !this.lastSeen.has(to)) {
|
|
1217
|
+
throw new Error(`unknown recipient: ${to} (registered peers: ${this.peers().join(", ") || "none"})`);
|
|
1218
|
+
}
|
|
1219
|
+
let hop = 0;
|
|
1220
|
+
if (ref !== void 0) {
|
|
1221
|
+
const parent = this.historyRing.findLast((m) => m.id === ref);
|
|
1222
|
+
hop = (parent?.hop ?? 0) + 1;
|
|
1223
|
+
if (hop > MAX_HOP) {
|
|
1224
|
+
throw new Error(`hop limit exceeded (max ${MAX_HOP}): start a new task/thread instead of extending this chain`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1004
1227
|
const message = {
|
|
1005
1228
|
id: randomUUID(),
|
|
1006
1229
|
from,
|
|
@@ -1008,31 +1231,42 @@ var AgentHub = class {
|
|
|
1008
1231
|
kind,
|
|
1009
1232
|
content,
|
|
1010
1233
|
...ref !== void 0 ? { ref } : {},
|
|
1234
|
+
...threadId !== void 0 ? { threadId } : {},
|
|
1235
|
+
...group !== void 0 ? { channel: group.id } : {},
|
|
1236
|
+
...hop > 0 ? { hop } : {},
|
|
1011
1237
|
ts: Date.now()
|
|
1012
1238
|
};
|
|
1013
1239
|
this.lastSeen.set(from, message.ts);
|
|
1240
|
+
this.remember(message);
|
|
1014
1241
|
if (to === BROADCAST) {
|
|
1015
1242
|
for (const peer of this.peers()) {
|
|
1016
1243
|
if (peer !== from) this.deliver(peer, message);
|
|
1017
1244
|
}
|
|
1245
|
+
} else if (group !== void 0) {
|
|
1246
|
+
for (const member of group.members) {
|
|
1247
|
+
if (member !== from) this.deliver(member, message);
|
|
1248
|
+
}
|
|
1018
1249
|
} else {
|
|
1019
1250
|
this.deliver(to, message);
|
|
1020
1251
|
}
|
|
1021
1252
|
return message;
|
|
1022
1253
|
}
|
|
1023
|
-
/** Queue or hand off a message; wake the first matching waiter for its target.
|
|
1254
|
+
/** Queue or hand off a message; wake the first matching waiter for its target.
|
|
1255
|
+
* History / onMessage are recorded by the caller (route / sendToGroup) —
|
|
1256
|
+
* exactly once per message, never per fan-out target. */
|
|
1024
1257
|
deliver(target, message) {
|
|
1025
1258
|
const list = this.waiters.get(target) ?? [];
|
|
1026
1259
|
const index = list.findIndex((waiter) => {
|
|
1027
1260
|
if (waiter.from !== void 0 && waiter.from !== message.from) return false;
|
|
1028
1261
|
if (waiter.ref !== void 0 && !(message.kind === "ack" && message.ref === waiter.ref)) return false;
|
|
1262
|
+
if (waiter.threadId !== void 0 && message.threadId !== waiter.threadId) return false;
|
|
1029
1263
|
return true;
|
|
1030
1264
|
});
|
|
1265
|
+
if (message.kind === "task") this.markTaskDelivered(message.id, target, message.ts);
|
|
1031
1266
|
if (index >= 0) {
|
|
1032
1267
|
const [waiter] = list.splice(index, 1);
|
|
1033
1268
|
this.waiters.set(target, list);
|
|
1034
1269
|
clearTimeout(waiter.timer);
|
|
1035
|
-
this.remember(message);
|
|
1036
1270
|
waiter.resolve({ type: "message", message });
|
|
1037
1271
|
return;
|
|
1038
1272
|
}
|
|
@@ -1040,11 +1274,10 @@ var AgentHub = class {
|
|
|
1040
1274
|
if (queue.length >= this.options.maxQueue) queue.shift();
|
|
1041
1275
|
queue.push(message);
|
|
1042
1276
|
this.queues.set(target, queue);
|
|
1043
|
-
this.remember(message);
|
|
1044
1277
|
this.options.onQueued?.(message, target);
|
|
1045
1278
|
}
|
|
1046
|
-
/** Append a
|
|
1047
|
-
*
|
|
1279
|
+
/** Append a sent message to the history ring (and fire the persistence /
|
|
1280
|
+
* relay hooks). Exactly one call per message — see route() / sendToGroup(). */
|
|
1048
1281
|
remember(message) {
|
|
1049
1282
|
this.historyRing.push(message);
|
|
1050
1283
|
if (this.historyRing.length > this.options.historyLimit) this.historyRing.splice(0, this.historyRing.length - this.options.historyLimit);
|
|
@@ -1209,11 +1442,13 @@ function loadTokenFileVoid(rows) {
|
|
|
1209
1442
|
// src/hub-tools.ts
|
|
1210
1443
|
var DEFAULT_WAIT_MS = 3e4;
|
|
1211
1444
|
function present(message) {
|
|
1212
|
-
const { ref, channel, ...rest } = message;
|
|
1445
|
+
const { ref, channel, threadId, hop, ...rest } = message;
|
|
1213
1446
|
return {
|
|
1214
1447
|
...rest,
|
|
1215
1448
|
...ref !== void 0 ? { ref } : {},
|
|
1216
1449
|
...channel !== void 0 ? { channel } : {},
|
|
1450
|
+
...threadId !== void 0 ? { threadId } : {},
|
|
1451
|
+
...hop !== void 0 ? { hop } : {},
|
|
1217
1452
|
content: decodeContent(message.kind, message.content)
|
|
1218
1453
|
};
|
|
1219
1454
|
}
|
|
@@ -1314,7 +1549,10 @@ function hubTools(hub, registry, options) {
|
|
|
1314
1549
|
from: message.from,
|
|
1315
1550
|
to: message.to,
|
|
1316
1551
|
kind: message.kind,
|
|
1552
|
+
...message.ref !== void 0 ? { ref: message.ref } : {},
|
|
1317
1553
|
...message.channel !== void 0 ? { channel: message.channel } : {},
|
|
1554
|
+
...message.threadId !== void 0 ? { threadId: message.threadId } : {},
|
|
1555
|
+
...message.hop !== void 0 ? { hop: message.hop } : {},
|
|
1318
1556
|
ts: message.ts
|
|
1319
1557
|
});
|
|
1320
1558
|
const presentWait = (result) => result.type === "timeout" ? result : { type: "message", message: present(result.message) };
|
|
@@ -1456,70 +1694,93 @@ function hubTools(hub, registry, options) {
|
|
|
1456
1694
|
},
|
|
1457
1695
|
{
|
|
1458
1696
|
name: "bridge_chat",
|
|
1459
|
-
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.',
|
|
1697
|
+
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.',
|
|
1460
1698
|
inputSchema: schema(
|
|
1461
|
-
{
|
|
1699
|
+
{
|
|
1700
|
+
to: str('Target peerId, group id, or "all" to broadcast.'),
|
|
1701
|
+
message: str("The message text."),
|
|
1702
|
+
threadId: optStr("Optional conversation / workflow thread id."),
|
|
1703
|
+
ref: optStr("Optional reply-to message id.")
|
|
1704
|
+
},
|
|
1462
1705
|
["to", "message"]
|
|
1463
1706
|
),
|
|
1464
|
-
handler: wrap(true, async (args, peer) => receipt(hub.send(peer, String(args.to), "chat", String(args.message)
|
|
1707
|
+
handler: wrap(true, async (args, peer) => receipt(hub.send(peer, String(args.to), "chat", String(args.message), {
|
|
1708
|
+
...args.threadId !== void 0 ? { threadId: String(args.threadId) } : {},
|
|
1709
|
+
...args.ref !== void 0 ? { ref: String(args.ref) } : {}
|
|
1710
|
+
})))
|
|
1465
1711
|
},
|
|
1466
1712
|
{
|
|
1467
1713
|
name: "bridge_task",
|
|
1468
|
-
description:
|
|
1714
|
+
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.',
|
|
1469
1715
|
inputSchema: schema(
|
|
1470
1716
|
{
|
|
1471
|
-
to: str('Target peerId, or "all" to broadcast.'),
|
|
1717
|
+
to: str('Target peerId, group id, or "all" to broadcast.'),
|
|
1472
1718
|
prompt: str("What the receiving agent should do."),
|
|
1473
1719
|
context: optStr("Optional background information for the task."),
|
|
1474
|
-
deliverable: optStr("Optional expected deliverable description.")
|
|
1720
|
+
deliverable: optStr("Optional expected deliverable description."),
|
|
1721
|
+
timeoutMs: int("Optional soft SLA in ms; hub marks the task timeout when it expires."),
|
|
1722
|
+
threadId: optStr("Optional conversation / workflow thread id.")
|
|
1475
1723
|
},
|
|
1476
1724
|
["to", "prompt"]
|
|
1477
1725
|
),
|
|
1478
1726
|
handler: wrap(true, async (args, peer) => receipt(hub.sendTask(peer, String(args.to), {
|
|
1479
1727
|
prompt: String(args.prompt),
|
|
1480
1728
|
...args.context !== void 0 ? { context: String(args.context) } : {},
|
|
1481
|
-
...args.deliverable !== void 0 ? { deliverable: String(args.deliverable) } : {}
|
|
1729
|
+
...args.deliverable !== void 0 ? { deliverable: String(args.deliverable) } : {},
|
|
1730
|
+
...args.timeoutMs !== void 0 ? { timeoutMs: Number(args.timeoutMs) } : {},
|
|
1731
|
+
...args.threadId !== void 0 ? { threadId: String(args.threadId) } : {}
|
|
1482
1732
|
})))
|
|
1483
1733
|
},
|
|
1484
1734
|
{
|
|
1485
1735
|
name: "bridge_ack",
|
|
1486
|
-
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).",
|
|
1736
|
+
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.",
|
|
1487
1737
|
inputSchema: schema(
|
|
1488
1738
|
{
|
|
1489
1739
|
ref: str("The id of the message being acknowledged."),
|
|
1490
|
-
status: { type: "string", enum: ["accepted", "rejected", "done", "failed"], description: "acknowledgement status." },
|
|
1491
|
-
note: optStr("Optional explanation for the acknowledgement.")
|
|
1740
|
+
status: { type: "string", enum: ["accepted", "working", "rejected", "done", "failed"], description: "acknowledgement status." },
|
|
1741
|
+
note: optStr("Optional explanation for the acknowledgement."),
|
|
1742
|
+
progress: int("Optional 0-100 progress (meaningful with status=working).")
|
|
1492
1743
|
},
|
|
1493
1744
|
["ref", "status"]
|
|
1494
1745
|
),
|
|
1495
1746
|
handler: wrap(true, async (args, peer) => {
|
|
1496
1747
|
const status = String(args.status);
|
|
1497
|
-
const valid = ["accepted", "rejected", "done", "failed"];
|
|
1748
|
+
const valid = ["accepted", "working", "rejected", "done", "failed"];
|
|
1498
1749
|
if (!valid.includes(status)) {
|
|
1499
1750
|
throw new Error(`invalid ack status: ${status} (expected ${valid.join(" | ")})`);
|
|
1500
1751
|
}
|
|
1501
|
-
return receipt(hub.sendAck(peer, String(args.ref), {
|
|
1752
|
+
return receipt(hub.sendAck(peer, String(args.ref), {
|
|
1753
|
+
status,
|
|
1754
|
+
...args.note !== void 0 ? { note: String(args.note) } : {},
|
|
1755
|
+
...args.progress !== void 0 ? { progress: Number(args.progress) } : {}
|
|
1756
|
+
}));
|
|
1502
1757
|
})
|
|
1503
1758
|
},
|
|
1504
1759
|
{
|
|
1505
1760
|
name: "bridge_tasks",
|
|
1506
|
-
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.",
|
|
1761
|
+
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.",
|
|
1507
1762
|
inputSchema: schema({
|
|
1508
1763
|
role: { type: "string", enum: ["sent", "received", "all"], description: "Which side of the task to list (default all)." },
|
|
1509
|
-
status: { type: "string", enum: ["pending", "accepted", "rejected", "done", "failed"], description: "Only tasks whose current ledger status matches." },
|
|
1764
|
+
status: { type: "string", enum: ["pending", "accepted", "working", "rejected", "done", "failed", "timeout"], description: "Only tasks whose current ledger status matches." },
|
|
1765
|
+
peer: optStr('Manager only: "all" or another peerId for the admin board.'),
|
|
1510
1766
|
limit: int("How many tasks to return (default 50).")
|
|
1511
1767
|
}),
|
|
1512
|
-
handler: wrap(true, async (args, peer) => {
|
|
1768
|
+
handler: wrap(true, async (args, peer, sessionId) => {
|
|
1513
1769
|
const role = args.role === void 0 ? void 0 : String(args.role);
|
|
1514
1770
|
if (role !== void 0 && role !== "sent" && role !== "received" && role !== "all") {
|
|
1515
1771
|
throw new Error(`invalid role: ${role} (expected sent | received | all)`);
|
|
1516
1772
|
}
|
|
1517
1773
|
const status = args.status === void 0 ? void 0 : String(args.status);
|
|
1518
|
-
const validStatus = ["pending", "accepted", "rejected", "done", "failed"];
|
|
1774
|
+
const validStatus = ["pending", "accepted", "working", "rejected", "done", "failed", "timeout"];
|
|
1519
1775
|
if (status !== void 0 && !validStatus.includes(status)) {
|
|
1520
1776
|
throw new Error(`invalid status: ${status} (expected ${validStatus.join(" | ")})`);
|
|
1521
1777
|
}
|
|
1522
|
-
|
|
1778
|
+
let target = peer;
|
|
1779
|
+
if (args.peer !== void 0) {
|
|
1780
|
+
target = String(args.peer);
|
|
1781
|
+
if (target !== peer) requireManager(peer, `listing tasks of '${target}'`, sessionId);
|
|
1782
|
+
}
|
|
1783
|
+
const tasks = hub.listTasks(target === "all" ? "all" : target, {
|
|
1523
1784
|
...role !== void 0 ? { role } : {},
|
|
1524
1785
|
...status !== void 0 ? { status } : {},
|
|
1525
1786
|
...args.limit !== void 0 ? { limit: Number(args.limit) } : {}
|
|
@@ -1529,7 +1790,7 @@ function hubTools(hub, registry, options) {
|
|
|
1529
1790
|
},
|
|
1530
1791
|
{
|
|
1531
1792
|
name: "bridge_task_status",
|
|
1532
|
-
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).",
|
|
1793
|
+
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).",
|
|
1533
1794
|
inputSchema: schema({ ref: str("Task message id (the id returned by bridge_task, also used as ack ref).") }, ["ref"]),
|
|
1534
1795
|
handler: wrap(true, async (args, peer, sessionId) => {
|
|
1535
1796
|
const ref = String(args.ref);
|
|
@@ -1543,10 +1804,11 @@ function hubTools(hub, registry, options) {
|
|
|
1543
1804
|
},
|
|
1544
1805
|
{
|
|
1545
1806
|
name: "bridge_wait",
|
|
1546
|
-
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.",
|
|
1807
|
+
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.",
|
|
1547
1808
|
inputSchema: schema({
|
|
1548
1809
|
from: optStr("Only wait for messages from this peerId."),
|
|
1549
1810
|
ref: optStr("Only wait for the ack of this task message id (bridge_task id)."),
|
|
1811
|
+
threadId: optStr("Only wait for messages carrying this threadId."),
|
|
1550
1812
|
timeoutMs: int(`Max wait in milliseconds (default ${DEFAULT_WAIT_MS}, ceiling is server waitTimeoutMs).`)
|
|
1551
1813
|
}),
|
|
1552
1814
|
handler: wrap(true, async (args, peer) => presentWait(await hub.wait(
|
|
@@ -1554,7 +1816,8 @@ function hubTools(hub, registry, options) {
|
|
|
1554
1816
|
args.timeoutMs === void 0 ? options.defaultWaitMs ?? DEFAULT_WAIT_MS : Number(args.timeoutMs),
|
|
1555
1817
|
args.from === void 0 ? void 0 : String(args.from),
|
|
1556
1818
|
void 0,
|
|
1557
|
-
args.ref === void 0 ? void 0 : String(args.ref)
|
|
1819
|
+
args.ref === void 0 ? void 0 : String(args.ref),
|
|
1820
|
+
args.threadId === void 0 ? void 0 : String(args.threadId)
|
|
1558
1821
|
)))
|
|
1559
1822
|
},
|
|
1560
1823
|
{
|
|
@@ -2374,7 +2637,7 @@ function readBody(req) {
|
|
|
2374
2637
|
import { DatabaseSync } from "node:sqlite";
|
|
2375
2638
|
var KIND_NAMES = ["chat", "task", "notice", "ack"];
|
|
2376
2639
|
function messageToRow(message) {
|
|
2377
|
-
return [message.id, message.from, message.to, message.kind, message.content, message.ref ?? null, message.channel ?? null, message.ts];
|
|
2640
|
+
return [message.id, message.from, message.to, message.kind, message.content, message.ref ?? null, message.channel ?? null, message.threadId ?? null, message.ts];
|
|
2378
2641
|
}
|
|
2379
2642
|
function rowToMessage(row) {
|
|
2380
2643
|
const id = row.id;
|
|
@@ -2393,6 +2656,7 @@ function rowToMessage(row) {
|
|
|
2393
2656
|
content,
|
|
2394
2657
|
...typeof row.ref === "string" && row.ref !== "" ? { ref: row.ref } : {},
|
|
2395
2658
|
...typeof row.channel === "string" && row.channel !== "" ? { channel: row.channel } : {},
|
|
2659
|
+
...typeof row.thread_id === "string" && row.thread_id !== "" ? { threadId: row.thread_id } : {},
|
|
2396
2660
|
ts
|
|
2397
2661
|
};
|
|
2398
2662
|
}
|
|
@@ -2407,16 +2671,25 @@ var SQLiteStateStore = class {
|
|
|
2407
2671
|
);
|
|
2408
2672
|
CREATE TABLE IF NOT EXISTS messages (
|
|
2409
2673
|
seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL, from_peer TEXT NOT NULL,
|
|
2410
|
-
to_peer TEXT NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL, ref TEXT, channel TEXT, ts INTEGER NOT NULL
|
|
2674
|
+
to_peer TEXT NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL, ref TEXT, channel TEXT, thread_id TEXT, ts INTEGER NOT NULL
|
|
2411
2675
|
);
|
|
2412
2676
|
CREATE TABLE IF NOT EXISTS mailboxes (
|
|
2413
2677
|
peer TEXT NOT NULL, seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL, from_peer TEXT NOT NULL,
|
|
2414
|
-
to_peer TEXT NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL, ref TEXT, channel TEXT, ts INTEGER NOT NULL
|
|
2678
|
+
to_peer TEXT NOT NULL, kind TEXT NOT NULL, content TEXT NOT NULL, ref TEXT, channel TEXT, thread_id TEXT, ts INTEGER NOT NULL
|
|
2415
2679
|
);
|
|
2416
2680
|
CREATE TABLE IF NOT EXISTS groups (
|
|
2417
2681
|
id TEXT PRIMARY KEY, name TEXT, members TEXT NOT NULL, created_by TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
2418
2682
|
);
|
|
2683
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
2684
|
+
id TEXT PRIMARY KEY, body TEXT NOT NULL, updated_at INTEGER NOT NULL
|
|
2685
|
+
);
|
|
2419
2686
|
`);
|
|
2687
|
+
for (const table of ["messages", "mailboxes"]) {
|
|
2688
|
+
try {
|
|
2689
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN thread_id TEXT`);
|
|
2690
|
+
} catch {
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2420
2693
|
log2.info(`sqlite state open: ${file}`);
|
|
2421
2694
|
}
|
|
2422
2695
|
db;
|
|
@@ -2425,17 +2698,17 @@ var SQLiteStateStore = class {
|
|
|
2425
2698
|
appendMessage(message) {
|
|
2426
2699
|
this.buffer.push(message);
|
|
2427
2700
|
}
|
|
2428
|
-
/** Insert buffered messages and re-snapshot mailboxes/profiles/groups. */
|
|
2701
|
+
/** Insert buffered messages and re-snapshot mailboxes/profiles/groups/tasks. */
|
|
2429
2702
|
flush(snapshot) {
|
|
2430
2703
|
const insert = this.db.prepare(
|
|
2431
|
-
"INSERT INTO messages (id, from_peer, to_peer, kind, content, ref, channel, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
2704
|
+
"INSERT INTO messages (id, from_peer, to_peer, kind, content, ref, channel, thread_id, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
2432
2705
|
);
|
|
2433
2706
|
for (const message of this.buffer.splice(0, this.buffer.length)) {
|
|
2434
2707
|
insert.run(...messageToRow(message));
|
|
2435
2708
|
}
|
|
2436
2709
|
this.db.exec("DELETE FROM mailboxes");
|
|
2437
2710
|
const insertMail = this.db.prepare(
|
|
2438
|
-
"INSERT INTO mailboxes (peer, id, from_peer, to_peer, kind, content, ref, channel, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
2711
|
+
"INSERT INTO mailboxes (peer, id, from_peer, to_peer, kind, content, ref, channel, thread_id, ts) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
2439
2712
|
);
|
|
2440
2713
|
for (const [peer, queue] of Object.entries(snapshot.mailboxes)) {
|
|
2441
2714
|
for (const message of queue) insertMail.run(peer, ...messageToRow(message));
|
|
@@ -2454,6 +2727,13 @@ var SQLiteStateStore = class {
|
|
|
2454
2727
|
for (const group of snapshot.groups) {
|
|
2455
2728
|
insertGroup.run(group.id, group.name ?? null, JSON.stringify(group.members), group.createdBy, group.createdAt);
|
|
2456
2729
|
}
|
|
2730
|
+
this.db.exec("DELETE FROM tasks");
|
|
2731
|
+
const insertTask = this.db.prepare(
|
|
2732
|
+
"INSERT INTO tasks (id, body, updated_at) VALUES (?, ?, ?)"
|
|
2733
|
+
);
|
|
2734
|
+
for (const task of snapshot.tasks) {
|
|
2735
|
+
insertTask.run(task.id, JSON.stringify(task), task.updatedAt);
|
|
2736
|
+
}
|
|
2457
2737
|
const row = this.db.prepare("SELECT COUNT(*) AS n FROM messages").get();
|
|
2458
2738
|
const total = Number(row.n);
|
|
2459
2739
|
if (total > this.historyLimit * 2) {
|
|
@@ -2501,7 +2781,16 @@ var SQLiteStateStore = class {
|
|
|
2501
2781
|
} catch {
|
|
2502
2782
|
}
|
|
2503
2783
|
}
|
|
2504
|
-
|
|
2784
|
+
const tasks = [];
|
|
2785
|
+
for (const row of this.db.prepare("SELECT * FROM tasks").all()) {
|
|
2786
|
+
if (typeof row.body !== "string") continue;
|
|
2787
|
+
try {
|
|
2788
|
+
const parsed = JSON.parse(row.body);
|
|
2789
|
+
if (typeof parsed === "object" && parsed !== null) tasks.push(parsed);
|
|
2790
|
+
} catch {
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
return { profiles, messages, mailboxes, groups, tasks };
|
|
2505
2794
|
}
|
|
2506
2795
|
/** Final flush + close (hub shutdown). */
|
|
2507
2796
|
close(snapshot) {
|
|
@@ -2512,7 +2801,7 @@ var SQLiteStateStore = class {
|
|
|
2512
2801
|
|
|
2513
2802
|
// src/index.ts
|
|
2514
2803
|
var SERVER_NAME = "agent-comm-hub";
|
|
2515
|
-
var SERVER_VERSION = "0.
|
|
2804
|
+
var SERVER_VERSION = "0.9.0";
|
|
2516
2805
|
var DEFAULT_HOST = "127.0.0.1";
|
|
2517
2806
|
var DEFAULT_PORT = 18764;
|
|
2518
2807
|
var DEFAULT_PATH = "/mcp";
|
|
@@ -2615,7 +2904,7 @@ function startHub(config = {}, log2 = console) {
|
|
|
2615
2904
|
if (dbDirty && dbStore !== void 0) {
|
|
2616
2905
|
dbDirty = false;
|
|
2617
2906
|
try {
|
|
2618
|
-
dbStore.flush({ mailboxes: hub.exportMailboxes(), profiles: hub.exportProfiles(), groups: hub.exportGroups() });
|
|
2907
|
+
dbStore.flush({ mailboxes: hub.exportMailboxes(), profiles: hub.exportProfiles(), groups: hub.exportGroups(), tasks: hub.exportTasks() });
|
|
2619
2908
|
} catch (error) {
|
|
2620
2909
|
log2.warn(`sqlite flush failed: ${error.message}`);
|
|
2621
2910
|
}
|
|
@@ -2645,6 +2934,7 @@ function startHub(config = {}, log2 = console) {
|
|
|
2645
2934
|
if (dbStore !== void 0) {
|
|
2646
2935
|
dbStore.appendMessage(message);
|
|
2647
2936
|
dbDirty = true;
|
|
2937
|
+
scheduleSave();
|
|
2648
2938
|
}
|
|
2649
2939
|
try {
|
|
2650
2940
|
pushRelay(message);
|
|
@@ -2655,6 +2945,15 @@ function startHub(config = {}, log2 = console) {
|
|
|
2655
2945
|
onMailboxesChanged: () => {
|
|
2656
2946
|
if (dbStore === void 0) return;
|
|
2657
2947
|
dbDirty = true;
|
|
2948
|
+
scheduleSave();
|
|
2949
|
+
},
|
|
2950
|
+
// Task ledger transitions (ack folding, SLA timeout, first delivery) are
|
|
2951
|
+
// their own persistence trigger: without it a crash between two roster
|
|
2952
|
+
// events loses the ledger state the admin board reads back.
|
|
2953
|
+
onTaskChanged: () => {
|
|
2954
|
+
if (dbStore === void 0) return;
|
|
2955
|
+
dbDirty = true;
|
|
2956
|
+
scheduleSave();
|
|
2658
2957
|
},
|
|
2659
2958
|
onGroupsChanged: () => {
|
|
2660
2959
|
if (dbStore === void 0) return;
|
|
@@ -2705,6 +3004,7 @@ function startHub(config = {}, log2 = console) {
|
|
|
2705
3004
|
const restoredMessages = hub.importHistory(state.messages);
|
|
2706
3005
|
const restoredQueued = hub.importMailboxes(state.mailboxes);
|
|
2707
3006
|
const restoredGroups = hub.importGroups(state.groups);
|
|
3007
|
+
const restoredTasks = hub.importTasks(state.tasks);
|
|
2708
3008
|
if (restoredMessages + restoredQueued + restoredGroups > 0) {
|
|
2709
3009
|
log2.info(`state restored from ${resolved.db}: ${restoredMessages} message(s), ${restoredQueued} queued, ${restoredGroups} group(s)`);
|
|
2710
3010
|
dbDirty = true;
|
|
@@ -2807,7 +3107,7 @@ function startHub(config = {}, log2 = console) {
|
|
|
2807
3107
|
flushSaves();
|
|
2808
3108
|
if (dbStore !== void 0) {
|
|
2809
3109
|
try {
|
|
2810
|
-
dbStore.close({ mailboxes: hub.exportMailboxes(), profiles: hub.exportProfiles(), groups: hub.exportGroups() });
|
|
3110
|
+
dbStore.close({ mailboxes: hub.exportMailboxes(), profiles: hub.exportProfiles(), groups: hub.exportGroups(), tasks: hub.exportTasks() });
|
|
2811
3111
|
} catch (error) {
|
|
2812
3112
|
log2.warn(`sqlite close failed: ${error.message}`);
|
|
2813
3113
|
}
|
|
@@ -2883,6 +3183,9 @@ function validateRegistry(registry) {
|
|
|
2883
3183
|
if (agent.skill !== null && (typeof agent.skill !== "string" || !agent.skill.startsWith("~/"))) {
|
|
2884
3184
|
throw new Error(`registry: ${agent.id}: skill must be '~'-relative or null`);
|
|
2885
3185
|
}
|
|
3186
|
+
if (agent.serverName !== void 0 && (typeof agent.serverName !== "string" || agent.serverName === "")) {
|
|
3187
|
+
throw new Error(`registry: ${agent.id}: serverName must be a non-empty string`);
|
|
3188
|
+
}
|
|
2886
3189
|
if (agent.os !== void 0 && (!Array.isArray(agent.os) || agent.os.some((os2) => !["win32", "darwin", "linux"].includes(os2)))) {
|
|
2887
3190
|
throw new Error(`registry: ${agent.id}: invalid os list`);
|
|
2888
3191
|
}
|
|
@@ -3134,7 +3437,7 @@ async function mergeDshPatch(file, opts) {
|
|
|
3134
3437
|
return "removed";
|
|
3135
3438
|
}
|
|
3136
3439
|
const ranges = hubEntryRanges();
|
|
3137
|
-
if (ranges.length === 1 && lines.slice(ranges[0].start, ranges[0].end).some((line) => line.includes(`url: ${opts.url}`))) {
|
|
3440
|
+
if (ranges.length === 1 && lines.slice(ranges[0].start, ranges[0].end).some((line) => line.includes(`url: ${opts.url}`)) && lines.slice(ranges[0].start, ranges[0].end).some((line) => line.includes(`serverName: ${opts.serverName}`))) {
|
|
3138
3441
|
return "unchanged";
|
|
3139
3442
|
}
|
|
3140
3443
|
await backup(file);
|
|
@@ -3205,9 +3508,10 @@ async function runSetup(options = {}) {
|
|
|
3205
3508
|
}
|
|
3206
3509
|
for (const agent of targetAgents) {
|
|
3207
3510
|
for (const config of agent.configs) {
|
|
3511
|
+
const nameFor = config.serverName ?? agent.serverName ?? serverName;
|
|
3208
3512
|
for (const file of expandConfigFile(config.file, home)) {
|
|
3209
3513
|
try {
|
|
3210
|
-
const status = config.strategy === "json" ? await mergeJsonServer(file, config.section, substitute(config.entry, { url, serverName }), { serverName, url, remove }) : config.strategy === "toml" ? await mergeTomlSection(file, { serverName, url, remove }) : await mergeDshPatch(file, { serverName, url, remove });
|
|
3514
|
+
const status = config.strategy === "json" ? await mergeJsonServer(file, config.section, substitute(config.entry, { url, serverName: nameFor }), { serverName: nameFor, url, remove }) : config.strategy === "toml" ? await mergeTomlSection(file, { serverName: nameFor, url, remove }) : await mergeDshPatch(file, { serverName: nameFor, url, remove });
|
|
3211
3515
|
record(status, agent.id, file);
|
|
3212
3516
|
} catch (error) {
|
|
3213
3517
|
summary.errors.push(`${agent.id}: ${file} \u2014 ${error.message}`);
|