u-foo 2.3.18 → 2.3.20

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.
@@ -739,6 +739,10 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
739
739
  subscriber: null,
740
740
  queueFile: null,
741
741
  pending: new Set(),
742
+ watchedAgents: new Set(),
743
+ lastEventSeq: 0,
744
+ emittedEventKeys: [],
745
+ emittedEventKeySet: new Set(),
742
746
  };
743
747
  const eventBus = new EventBus(projectRoot);
744
748
  let joinInProgress = false;
@@ -758,6 +762,169 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
758
762
  return agentId;
759
763
  }
760
764
 
765
+ function getEventDedupeKey(evt) {
766
+ if (!evt || typeof evt !== "object") return "";
767
+ const seq = Number(evt.seq);
768
+ if (Number.isFinite(seq) && seq > 0) return `seq:${seq}`;
769
+ return [
770
+ "event",
771
+ evt.timestamp || evt.ts || "",
772
+ evt.event || "",
773
+ evt.publisher || "",
774
+ evt.target || "",
775
+ JSON.stringify(evt.data || {}),
776
+ ].join(":");
777
+ }
778
+
779
+ function rememberEmittedEvent(evt) {
780
+ const key = getEventDedupeKey(evt);
781
+ if (!key) return false;
782
+ if (state.emittedEventKeySet.has(key)) return true;
783
+ state.emittedEventKeySet.add(key);
784
+ state.emittedEventKeys.push(key);
785
+ if (state.emittedEventKeys.length > 500) {
786
+ const removed = state.emittedEventKeys.splice(0, state.emittedEventKeys.length - 500);
787
+ for (const item of removed) state.emittedEventKeySet.delete(item);
788
+ }
789
+ return false;
790
+ }
791
+
792
+ function hasPositiveSeq(seq) {
793
+ const value = Number(seq);
794
+ return Number.isFinite(value) && value > 0;
795
+ }
796
+
797
+ function toBridgeEvent(evt) {
798
+ const data = evt.data && typeof evt.data === "object" ? evt.data : {};
799
+ return {
800
+ seq: evt.seq,
801
+ event: evt.event,
802
+ publisher: evt.publisher,
803
+ target: evt.target,
804
+ data,
805
+ message: data.message || "",
806
+ state: data.state || "",
807
+ previous: data.previous || "",
808
+ subscriber: data.subscriber || "",
809
+ source: data.source || "",
810
+ injection_mode: data.injection_mode || "",
811
+ ts: evt.timestamp || evt.ts,
812
+ };
813
+ }
814
+
815
+ function emitBusEvent(evt) {
816
+ if (!evt || !onEvent) return;
817
+ if (rememberEmittedEvent(evt)) return;
818
+ onEvent(toBridgeEvent(evt));
819
+ }
820
+
821
+ function readAgentsData() {
822
+ try {
823
+ const busPath = getUfooPaths(projectRoot).agentsFile;
824
+ return JSON.parse(fs.readFileSync(busPath, "utf8"));
825
+ } catch {
826
+ return {};
827
+ }
828
+ }
829
+
830
+ function buildWatchedAliases() {
831
+ const aliases = new Set();
832
+ const bus = readAgentsData();
833
+ for (const agentId of state.watchedAgents) {
834
+ aliases.add(agentId);
835
+ const meta = bus.agents && bus.agents[agentId];
836
+ if (!meta) continue;
837
+ if (meta.nickname) aliases.add(meta.nickname);
838
+ if (meta.scoped_nickname) aliases.add(meta.scoped_nickname);
839
+ if (meta.display_nickname) aliases.add(meta.display_nickname);
840
+ }
841
+ return aliases;
842
+ }
843
+
844
+ function isWatchedEvent(evt, aliases = buildWatchedAliases()) {
845
+ if (!evt || (evt.event !== "message" && evt.event !== "activity_state_changed")) return false;
846
+ const publisher = String(evt.publisher || "");
847
+ const target = String(evt.target || "");
848
+ const subscriber = evt.data && evt.data.subscriber ? String(evt.data.subscriber) : "";
849
+ return aliases.has(publisher) || aliases.has(target) || aliases.has(subscriber);
850
+ }
851
+
852
+ function getEventFiles() {
853
+ try {
854
+ const dir = getUfooPaths(projectRoot).busEventsDir;
855
+ return fs.readdirSync(dir)
856
+ .filter((name) => name.endsWith(".jsonl"))
857
+ .sort()
858
+ .map((name) => path.join(dir, name));
859
+ } catch {
860
+ return [];
861
+ }
862
+ }
863
+
864
+ function readCurrentSeq() {
865
+ try {
866
+ const raw = fs.readFileSync(path.join(getUfooPaths(projectRoot).busDir, "seq.counter"), "utf8").trim();
867
+ const seq = Number(raw);
868
+ return Number.isFinite(seq) ? seq : 0;
869
+ } catch {
870
+ return 0;
871
+ }
872
+ }
873
+
874
+ function readEventFile(file) {
875
+ try {
876
+ return fs.readFileSync(file, "utf8")
877
+ .split(/\r?\n/)
878
+ .filter(Boolean)
879
+ .map((line) => {
880
+ try {
881
+ return JSON.parse(line);
882
+ } catch {
883
+ return null;
884
+ }
885
+ })
886
+ .filter(Boolean);
887
+ } catch {
888
+ return [];
889
+ }
890
+ }
891
+
892
+ function emitRecentWatchedEvents(agentId, limit = 80) {
893
+ if (!agentId) return;
894
+ const previous = new Set(state.watchedAgents);
895
+ state.watchedAgents.add(agentId);
896
+ const aliases = buildWatchedAliases();
897
+ state.watchedAgents = previous;
898
+ const matches = [];
899
+ const files = getEventFiles().slice(-3);
900
+ for (const file of files) {
901
+ for (const evt of readEventFile(file)) {
902
+ if (isWatchedEvent(evt, aliases)) matches.push(evt);
903
+ }
904
+ }
905
+ for (const evt of matches.slice(-limit)) emitBusEvent(evt);
906
+ }
907
+
908
+ function pollWatchedEvents() {
909
+ if (state.watchedAgents.size === 0) {
910
+ state.lastEventSeq = readCurrentSeq();
911
+ return;
912
+ }
913
+ const aliases = buildWatchedAliases();
914
+ let maxSeq = state.lastEventSeq;
915
+ for (const file of getEventFiles().slice(-2)) {
916
+ for (const evt of readEventFile(file)) {
917
+ const seq = Number(evt.seq);
918
+ if (hasPositiveSeq(seq)) {
919
+ if (seq <= state.lastEventSeq) continue;
920
+ if (seq > maxSeq) maxSeq = seq;
921
+ }
922
+ if (isWatchedEvent(evt, aliases)) emitBusEvent(evt);
923
+ }
924
+ }
925
+ state.lastEventSeq = Math.max(state.lastEventSeq, maxSeq);
926
+ }
927
+
761
928
  function ensureSubscriber() {
762
929
  if (state.subscriber || joinInProgress) return;
763
930
  const debugFile = path.join(getUfooPaths(projectRoot).runDir, "bus-join-debug.txt");
@@ -785,9 +952,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
785
952
  })();
786
953
  }
787
954
 
788
- function poll() {
789
- ensureSubscriber();
790
- if (typeof shouldDrain === "function" && !shouldDrain()) return;
955
+ function pollQueue() {
791
956
  if (!state.queueFile) return;
792
957
  if (!fs.existsSync(state.queueFile)) return;
793
958
  let content = "";
@@ -828,15 +993,7 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
828
993
  continue;
829
994
  }
830
995
  if (!evt) continue;
831
- if (onEvent) {
832
- onEvent({
833
- event: evt.event,
834
- publisher: evt.publisher,
835
- target: evt.target,
836
- message: evt.data?.message || "",
837
- ts: evt.timestamp || evt.ts,
838
- });
839
- }
996
+ emitBusEvent(evt);
840
997
  if (evt.publisher && state.pending.has(evt.publisher)) {
841
998
  state.pending.delete(evt.publisher);
842
999
  if (onStatus) {
@@ -847,6 +1004,13 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
847
1004
  }
848
1005
  }
849
1006
 
1007
+ function poll() {
1008
+ ensureSubscriber();
1009
+ if (typeof shouldDrain === "function" && !shouldDrain()) return;
1010
+ pollQueue();
1011
+ pollWatchedEvents();
1012
+ }
1013
+
850
1014
  const interval = setInterval(poll, 1000);
851
1015
  return {
852
1016
  markPending(target) {
@@ -865,6 +1029,19 @@ function startBusBridge(projectRoot, provider, onEvent, onStatus, shouldDrain) {
865
1029
  } catch {}
866
1030
  return state.subscriber;
867
1031
  },
1032
+ watchAgent(agentId, enabled = true) {
1033
+ if (!agentId) return;
1034
+ if (enabled) {
1035
+ emitRecentWatchedEvents(agentId);
1036
+ state.watchedAgents.add(agentId);
1037
+ state.lastEventSeq = Math.max(state.lastEventSeq, readCurrentSeq());
1038
+ } else {
1039
+ state.watchedAgents.delete(agentId);
1040
+ if (state.watchedAgents.size === 0) {
1041
+ state.lastEventSeq = readCurrentSeq();
1042
+ }
1043
+ }
1044
+ },
868
1045
  stop() {
869
1046
  clearInterval(interval);
870
1047
  },
@@ -1187,6 +1364,13 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
1187
1364
  }
1188
1365
  return;
1189
1366
  }
1367
+ if (req.type === IPC_REQUEST_TYPES.BUS_WATCH) {
1368
+ const agentId = String(req.agent_id || "").trim();
1369
+ if (agentId) {
1370
+ busBridge.watchAgent(agentId, req.enabled !== false);
1371
+ }
1372
+ return;
1373
+ }
1190
1374
  if (req.type === IPC_REQUEST_TYPES.CRON) {
1191
1375
  if (!daemonCronController) {
1192
1376
  socket.write(
@@ -5,6 +5,7 @@ const IPC_REQUEST_TYPES = {
5
5
  PROMPT: "prompt",
6
6
  CRON: "cron",
7
7
  BUS_SEND: "bus_send",
8
+ BUS_WATCH: "bus_watch",
8
9
  CLOSE_AGENT: "close_agent",
9
10
  LAUNCH_AGENT: "launch_agent",
10
11
  LAUNCH_GROUP: "launch_group",