@trops/dash-core 0.1.627 → 0.1.629

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.
@@ -73814,11 +73814,25 @@ function validateBotDefinition$1(def) {
73814
73814
  if (!WHILE_PAUSED.has(def.whilePaused)) {
73815
73815
  errors.push(`whilePaused must be one of ${[...WHILE_PAUSED].join(", ")}`);
73816
73816
  }
73817
- for (const field of ["mcpServers", "allowedTools", "schedules"]) {
73818
- if (!Array.isArray(def[field])) {
73817
+ for (const field of [
73818
+ "mcpServers",
73819
+ "allowedTools",
73820
+ "schedules",
73821
+ "subscriptions",
73822
+ ]) {
73823
+ if (def[field] !== undefined && !Array.isArray(def[field])) {
73819
73824
  errors.push(`${field} must be an array`);
73820
73825
  }
73821
73826
  }
73827
+ // Each subscription must name an eventType (PRD FR-009).
73828
+ if (Array.isArray(def.subscriptions)) {
73829
+ for (const sub of def.subscriptions) {
73830
+ if (!sub || typeof sub.eventType !== "string" || !sub.eventType.trim()) {
73831
+ errors.push("each subscription must have a non-empty eventType");
73832
+ break;
73833
+ }
73834
+ }
73835
+ }
73822
73836
  return { valid: errors.length === 0, errors };
73823
73837
  }
73824
73838
 
@@ -75740,6 +75754,92 @@ function handleMemoryTool$1(memory, ctx, toolName, args = {}) {
75740
75754
 
75741
75755
  var memoryTools = { MEMORY_SERVER: MEMORY_SERVER$1, MEMORY_TOOLS: MEMORY_TOOLS$1, handleMemoryTool: handleMemoryTool$1 };
75742
75756
 
75757
+ /**
75758
+ * eventMatcher.js
75759
+ *
75760
+ * Given a fired event and the set of bots, returns the bots that subscribe to
75761
+ * it (PRD FR-009 / US-010). A bot subscribes via `subscriptions: [{ eventType }]`.
75762
+ *
75763
+ * Workspace scoping is designed in but not yet exercised: when an event carries
75764
+ * a `workspaceId`, a bot only matches if it's unscoped (`workspaceId` falsy) or
75765
+ * its `workspaceId` equals the event's. Events aren't stamped with a workspace
75766
+ * yet (a follow-up slice), so today every subscribed bot matches — but the
75767
+ * scoping "just works" once stamping lands, with no matcher change.
75768
+ *
75769
+ * Pure (NFR-006): no Electron, no I/O.
75770
+ */
75771
+
75772
+ /**
75773
+ * @param {Array<object>} bots bot definitions (each may have `subscriptions`)
75774
+ * @param {{ eventType: string, workspaceId?: string }} event
75775
+ * @param {{ excludeBotId?: string }} [opts] loop guard — never match this bot
75776
+ * @returns {Array<object>} the subscribing bots
75777
+ */
75778
+ function matchSubscribedBots$1(bots, event, opts = {}) {
75779
+ if (!Array.isArray(bots) || !event || !event.eventType) return [];
75780
+ const excludeBotId = opts.excludeBotId || null;
75781
+ const eventWorkspace = event.workspaceId || null;
75782
+
75783
+ return bots.filter((bot) => {
75784
+ if (!bot || bot.id === excludeBotId) return false;
75785
+ const subs = Array.isArray(bot.subscriptions) ? bot.subscriptions : [];
75786
+ const subscribed = subs.some((s) => s && s.eventType === event.eventType);
75787
+ if (!subscribed) return false;
75788
+
75789
+ // Workspace scoping (forward-compatible): only enforced once events carry a
75790
+ // workspaceId. An unscoped bot (no workspaceId) listens everywhere.
75791
+ if (eventWorkspace && bot.workspaceId) {
75792
+ return bot.workspaceId === eventWorkspace;
75793
+ }
75794
+ return true;
75795
+ });
75796
+ }
75797
+
75798
+ var eventMatcher = { matchSubscribedBots: matchSubscribedBots$1 };
75799
+
75800
+ /**
75801
+ * EventDispatcher.js
75802
+ *
75803
+ * Rate guard for event-triggered bot runs (PRD FR-009): a chatty event must not
75804
+ * spawn a flood of runs. Enforces a per-bot cooldown — a given bot won't be
75805
+ * event-triggered more often than once per `cooldownMs`. The dispatch decision
75806
+ * (pause, already-running) lives in botController; this owns only the timing.
75807
+ *
75808
+ * Pure (NFR-006): the clock is injected, so it's deterministic under test and
75809
+ * Electron-free.
75810
+ */
75811
+
75812
+ const DEFAULT_COOLDOWN_MS = 10_000;
75813
+
75814
+ let EventDispatcher$1 = class EventDispatcher {
75815
+ /**
75816
+ * @param {{ cooldownMs?: number, clock?: { nowMs: () => number } }} [opts]
75817
+ */
75818
+ constructor(opts = {}) {
75819
+ this._cooldownMs =
75820
+ typeof opts.cooldownMs === "number"
75821
+ ? opts.cooldownMs
75822
+ : DEFAULT_COOLDOWN_MS;
75823
+ this._nowMs = (opts.clock && opts.clock.nowMs) || (() => Date.now());
75824
+ /** @type {Map<string, number>} botId → last dispatch time (ms) */
75825
+ this._last = new Map();
75826
+ }
75827
+
75828
+ /** @returns {boolean} true if the bot is allowed to run for an event now. */
75829
+ shouldDispatch(botId) {
75830
+ const last = this._last.get(botId);
75831
+ if (last === undefined) return true;
75832
+ return this._nowMs() - last >= this._cooldownMs;
75833
+ }
75834
+
75835
+ /** Record that the bot was just event-dispatched (starts its cooldown). */
75836
+ note(botId) {
75837
+ this._last.set(botId, this._nowMs());
75838
+ }
75839
+ };
75840
+
75841
+ var EventDispatcher_1 = { EventDispatcher: EventDispatcher$1, DEFAULT_COOLDOWN_MS };
75842
+
75743
75843
  /**
75744
75844
  * mcpResult.js
75745
75845
  *
@@ -75812,6 +75912,8 @@ const {
75812
75912
  MEMORY_TOOLS,
75813
75913
  handleMemoryTool,
75814
75914
  } = memoryTools;
75915
+ const { matchSubscribedBots } = eventMatcher;
75916
+ const { EventDispatcher } = EventDispatcher_1;
75815
75917
  const { normalizeMcpResult } = mcpResult;
75816
75918
  const {
75817
75919
  getProvider: getProvider$1,
@@ -75833,6 +75935,25 @@ function pricingFor(providerId, model) {
75833
75935
  return (m && m.pricing) || null;
75834
75936
  }
75835
75937
 
75938
+ /** Build the run prompt for an event-triggered bot (P1: FR-009). */
75939
+ function composeEventPrompt(event) {
75940
+ let payload;
75941
+ try {
75942
+ payload =
75943
+ event.content === undefined
75944
+ ? "(no payload)"
75945
+ : JSON.stringify(event.content);
75946
+ } catch (_e) {
75947
+ payload = "(unserializable payload)";
75948
+ }
75949
+ return (
75950
+ `An event you subscribe to just fired.\n\n` +
75951
+ `Event: ${event.eventType}\n` +
75952
+ `Payload: ${payload}\n\n` +
75953
+ `Follow your instructions to handle it.`
75954
+ );
75955
+ }
75956
+
75836
75957
  const botController$1 = {
75837
75958
  _ready: false,
75838
75959
 
@@ -75892,6 +76013,10 @@ const botController$1 = {
75892
76013
  // tools that every bot can use without a consent prompt.
75893
76014
  this._memory = new BotMemory({ persistence: host.memoryPersistence });
75894
76015
 
76016
+ // Event-bus bridge (P1: FR-009): per-bot cooldown so a chatty event can't
76017
+ // flood the runner with event-triggered runs.
76018
+ this._dispatcher = new EventDispatcher();
76019
+
75895
76020
  this._runner = new BotRunner({
75896
76021
  engines,
75897
76022
  store: this._store,
@@ -75947,6 +76072,30 @@ const botController$1 = {
75947
76072
  }
75948
76073
  },
75949
76074
 
76075
+ /**
76076
+ * Dispatch a fired event to every subscribed bot (P1: FR-009 / US-010).
76077
+ * Called by dash-electron's widget-event relay tap. Guards: never re-trigger
76078
+ * the origin bot (loop safety), skip paused bots and bots in cooldown; a bot
76079
+ * already running is skipped by the runner itself.
76080
+ * @param {{ eventType: string, content?: any, workspaceId?: string,
76081
+ * originBotId?: string }} event
76082
+ */
76083
+ handleEvent(event) {
76084
+ if (!this._ready || !event || !event.eventType) return;
76085
+ const bots = matchSubscribedBots(this._store.list(), event, {
76086
+ excludeBotId: event.originBotId,
76087
+ });
76088
+ for (const bot of bots) {
76089
+ if (this._pause.isPaused(bot.id)) continue;
76090
+ if (!this._dispatcher.shouldDispatch(bot.id)) continue;
76091
+ this._dispatcher.note(bot.id);
76092
+ this._run(bot.id, {
76093
+ prompt: composeEventPrompt(event),
76094
+ trigger: "event",
76095
+ });
76096
+ }
76097
+ },
76098
+
75950
76099
  // ---- IPC-facing operations ---------------------------------------------
75951
76100
 
75952
76101
  list() {