@trops/dash-core 0.1.626 → 0.1.628

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.
@@ -73683,6 +73683,7 @@ const Store = require$$0$5;
73683
73683
  function createElectronHost$1() {
73684
73684
  const store = new Store({ name: "dash-bots" });
73685
73685
  const budgetStore = new Store({ name: "dash-bot-budgets" });
73686
+ const memoryStore = new Store({ name: "dash-bot-memory" });
73686
73687
  const botsRoot = path$3.join(app$2.getPath("userData"), "bots");
73687
73688
 
73688
73689
  return {
@@ -73700,6 +73701,11 @@ function createElectronHost$1() {
73700
73701
  budgetStore.get("store", { budgets: {}, spend: {}, overrides: {} }),
73701
73702
  write: (obj) => budgetStore.set("store", obj),
73702
73703
  },
73704
+ // Separate blob for scoped bot memory (P1: FR-010).
73705
+ memoryPersistence: {
73706
+ read: () => memoryStore.get("store", { scopes: {} }),
73707
+ write: (obj) => memoryStore.set("store", obj),
73708
+ },
73703
73709
  };
73704
73710
  }
73705
73711
 
@@ -73808,11 +73814,25 @@ function validateBotDefinition$1(def) {
73808
73814
  if (!WHILE_PAUSED.has(def.whilePaused)) {
73809
73815
  errors.push(`whilePaused must be one of ${[...WHILE_PAUSED].join(", ")}`);
73810
73816
  }
73811
- for (const field of ["mcpServers", "allowedTools", "schedules"]) {
73812
- 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])) {
73813
73824
  errors.push(`${field} must be an array`);
73814
73825
  }
73815
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
+ }
73816
73836
  return { valid: errors.length === 0, errors };
73817
73837
  }
73818
73838
 
@@ -74777,6 +74797,7 @@ var engines$1 = { getEngine, listEngines, registerEngine };
74777
74797
  * botId: string,
74778
74798
  * allowedTools?: string[],
74779
74799
  * mcpServers?: string[],
74800
+ * internalServers?: string[],
74780
74801
  * approvalPolicy?: "ask" | "allow",
74781
74802
  * resolveServer: (toolName: string) => (string | null),
74782
74803
  * createApproval: (request: object) => { id: string, promise: Promise<any> },
@@ -74792,6 +74813,7 @@ function createRequestPermission$1(ctx) {
74792
74813
  botId,
74793
74814
  allowedTools = [],
74794
74815
  mcpServers = [],
74816
+ internalServers = [],
74795
74817
  resolveServer,
74796
74818
  createApproval,
74797
74819
  audit = () => {},
@@ -74840,6 +74862,16 @@ function createRequestPermission$1(ctx) {
74840
74862
 
74841
74863
  const serverName = resolveServer(toolName);
74842
74864
 
74865
+ // 1b. Internal Bot Factory tools (e.g. bot-memory) are the bot's own
74866
+ // sandbox — auto-allowed, never a consent prompt or external action.
74867
+ if (serverName && internalServers.includes(serverName)) {
74868
+ return record(
74869
+ toolName,
74870
+ { allow: true },
74871
+ { outcome: "internal", serverName },
74872
+ );
74873
+ }
74874
+
74843
74875
  // 2. A tool whose server the bot was never configured with is denied
74844
74876
  // without prompting — it's not a consent gap, it's out of scope.
74845
74877
  if (!serverName || !mcpServers.includes(serverName)) {
@@ -74959,6 +74991,9 @@ let BotRunner$1 = class BotRunner {
74959
74991
  this._makeRequestPermission =
74960
74992
  deps.makeRequestPermission || createRequestPermission;
74961
74993
  this._gate = deps.gate; // forwarded to PermissionGate ctx (optional)
74994
+ // Virtual servers whose tools are auto-allowed (e.g. "bot-memory") — the
74995
+ // bot's own sandbox, not external actions. Forwarded to the gate ctx.
74996
+ this._internalServers = deps.internalServers || [];
74962
74997
  this._isPaused = deps.isPaused || (() => false);
74963
74998
  // Called after each run with the run's token usage so budgets (Slice 5)
74964
74999
  // can accrue cost. Optional — default no-op keeps the runner portable.
@@ -75030,6 +75065,7 @@ let BotRunner$1 = class BotRunner {
75030
75065
  workspaceId: bot.workspaceId,
75031
75066
  allowedTools: bot.allowedTools || [],
75032
75067
  mcpServers: bot.mcpServers || [],
75068
+ internalServers: this._internalServers,
75033
75069
  resolveServer,
75034
75070
  createApproval: (request) => this._approvals.create(request),
75035
75071
  audit: this._audit,
@@ -75491,6 +75527,319 @@ let PauseController$1 = class PauseController {
75491
75527
 
75492
75528
  var PauseController_1 = PauseController$1;
75493
75529
 
75530
+ /**
75531
+ * BotMemory.js
75532
+ *
75533
+ * Durable, scoped key-value memory for bots (PRD FR-010 / FR-012, US-012/015):
75534
+ * a bot can remember facts across runs. Values are namespaced by scope
75535
+ * (`workspace` — isolated per workspace — or `global` — shared across a
75536
+ * principal's bots) and carry a monotonic `version` + `updatedAt` so a later
75537
+ * slice can add history/optimistic concurrency without a data migration.
75538
+ *
75539
+ * Portable (NFR-006): the durable blob is injected as `persistence` (same shape
75540
+ * as BotStore/BudgetController), so this loads and runs under plain Node. The
75541
+ * Electron wiring (electron-store) lives in host.js. A KV store needs no SQL for
75542
+ * v1; SQLite can later slot in behind this exact interface if volume demands.
75543
+ */
75544
+
75545
+ const VALID_SCOPES = ["workspace", "global"];
75546
+
75547
+ /** Bucket key for a (scope, id) pair. Global collapses all ids to one bucket. */
75548
+ function bucketKey(scope, id) {
75549
+ return scope === "global" ? "global" : `workspace:${id || "default"}`;
75550
+ }
75551
+
75552
+ let BotMemory$1 = class BotMemory {
75553
+ /**
75554
+ * @param {{ persistence: { read: () => object, write: (o: object) => void },
75555
+ * clock?: { now: () => string } }} deps
75556
+ */
75557
+ constructor({ persistence, clock } = {}) {
75558
+ if (!persistence) throw new Error("BotMemory: persistence is required");
75559
+ this._persistence = persistence;
75560
+ this._now = (clock && clock.now) || (() => new Date().toISOString());
75561
+ }
75562
+
75563
+ _read() {
75564
+ const blob = this._persistence.read() || {};
75565
+ return blob.scopes ? blob : { scopes: {} };
75566
+ }
75567
+
75568
+ _write(blob) {
75569
+ this._persistence.write(blob);
75570
+ }
75571
+
75572
+ /** @returns {*} the stored value, or undefined if absent. */
75573
+ get(scope, id, key) {
75574
+ const bucket = this._read().scopes[bucketKey(scope, id)] || {};
75575
+ const entry = bucket[key];
75576
+ return entry ? entry.value : undefined;
75577
+ }
75578
+
75579
+ /** Store (or overwrite) a value; bumps version. @returns the stored entry. */
75580
+ set(scope, id, key, value) {
75581
+ const blob = this._read();
75582
+ const bk = bucketKey(scope, id);
75583
+ const bucket = blob.scopes[bk] || (blob.scopes[bk] = {});
75584
+ const prev = bucket[key];
75585
+ const entry = {
75586
+ value,
75587
+ version: prev ? prev.version + 1 : 1,
75588
+ updatedAt: this._now(),
75589
+ };
75590
+ bucket[key] = entry;
75591
+ this._write(blob);
75592
+ return entry;
75593
+ }
75594
+
75595
+ /** @returns {Array<{key,value,version,updatedAt}>} entries, optionally prefixed. */
75596
+ list(scope, id, prefix) {
75597
+ const bucket = this._read().scopes[bucketKey(scope, id)] || {};
75598
+ return Object.keys(bucket)
75599
+ .filter((k) => !prefix || k.startsWith(prefix))
75600
+ .sort()
75601
+ .map((k) => ({
75602
+ key: k,
75603
+ value: bucket[k].value,
75604
+ version: bucket[k].version,
75605
+ updatedAt: bucket[k].updatedAt,
75606
+ }));
75607
+ }
75608
+
75609
+ /** @returns {boolean} true if a value was removed. */
75610
+ delete(scope, id, key) {
75611
+ const blob = this._read();
75612
+ const bucket = blob.scopes[bucketKey(scope, id)];
75613
+ if (!bucket || !(key in bucket)) return false;
75614
+ delete bucket[key];
75615
+ this._write(blob);
75616
+ return true;
75617
+ }
75618
+ };
75619
+
75620
+ var BotMemory_1 = { BotMemory: BotMemory$1, VALID_SCOPES };
75621
+
75622
+ /**
75623
+ * memoryTools.js
75624
+ *
75625
+ * Exposes BotMemory to bots as in-process tools (PRD FR-010): memory_get /
75626
+ * memory_set / memory_list / memory_delete. These are served under a virtual
75627
+ * "bot-memory" server that the bot PermissionGate auto-allows (it's the bot's
75628
+ * own sandbox, not an external action), so no consent prompt is required.
75629
+ *
75630
+ * Portable (NFR-006): no Electron. botController wires a BotMemory instance and
75631
+ * the calling bot's scope, and routes MEMORY_SERVER tool calls here. The result
75632
+ * shape matches the tool-loop engine's executeTool contract: { text, isError }.
75633
+ */
75634
+
75635
+ const MEMORY_SERVER$1 = "bot-memory";
75636
+
75637
+ const SCOPE_PROP = {
75638
+ type: "string",
75639
+ enum: ["workspace", "global"],
75640
+ description:
75641
+ "Which memory scope to use: 'workspace' (default, isolated to this bot's workspace) or 'global' (shared across your bots).",
75642
+ };
75643
+
75644
+ const MEMORY_TOOLS$1 = [
75645
+ {
75646
+ name: "memory_set",
75647
+ description:
75648
+ "Store a value in your durable memory under a key. Persists across runs so you can recall it later.",
75649
+ inputSchema: {
75650
+ type: "object",
75651
+ properties: {
75652
+ key: { type: "string", description: "Key to store the value under." },
75653
+ value: { description: "The value to store (any JSON)." },
75654
+ scope: SCOPE_PROP,
75655
+ },
75656
+ required: ["key", "value"],
75657
+ },
75658
+ },
75659
+ {
75660
+ name: "memory_get",
75661
+ description:
75662
+ "Retrieve a value previously stored with memory_set. Returns the value, or a note if nothing is stored.",
75663
+ inputSchema: {
75664
+ type: "object",
75665
+ properties: {
75666
+ key: { type: "string", description: "Key to look up." },
75667
+ scope: SCOPE_PROP,
75668
+ },
75669
+ required: ["key"],
75670
+ },
75671
+ },
75672
+ {
75673
+ name: "memory_list",
75674
+ description:
75675
+ "List the keys currently in your memory, optionally filtered by a key prefix.",
75676
+ inputSchema: {
75677
+ type: "object",
75678
+ properties: {
75679
+ prefix: {
75680
+ type: "string",
75681
+ description: "Only list keys with this prefix.",
75682
+ },
75683
+ scope: SCOPE_PROP,
75684
+ },
75685
+ },
75686
+ },
75687
+ {
75688
+ name: "memory_delete",
75689
+ description: "Delete a key from your memory.",
75690
+ inputSchema: {
75691
+ type: "object",
75692
+ properties: {
75693
+ key: { type: "string", description: "Key to delete." },
75694
+ scope: SCOPE_PROP,
75695
+ },
75696
+ required: ["key"],
75697
+ },
75698
+ },
75699
+ ];
75700
+
75701
+ function ok$1(text) {
75702
+ return { text, isError: false };
75703
+ }
75704
+ function fail$1(text) {
75705
+ return { text, isError: true };
75706
+ }
75707
+
75708
+ /**
75709
+ * Execute a memory_* tool call in-process.
75710
+ * @param {import("./BotMemory").BotMemory} memory
75711
+ * @param {{ workspaceId?: string }} ctx the calling bot's scope context
75712
+ * @param {string} toolName
75713
+ * @param {object} args
75714
+ * @returns {{ text: string, isError: boolean }}
75715
+ */
75716
+ function handleMemoryTool$1(memory, ctx, toolName, args = {}) {
75717
+ const scope = args && args.scope === "global" ? "global" : "workspace";
75718
+ const id = ctx && ctx.workspaceId;
75719
+ try {
75720
+ switch (toolName) {
75721
+ case "memory_set": {
75722
+ if (!args || !args.key) return fail$1("memory_set requires a 'key'.");
75723
+ const entry = memory.set(scope, id, args.key, args.value);
75724
+ return ok$1(`Stored '${args.key}' (${scope}, v${entry.version}).`);
75725
+ }
75726
+ case "memory_get": {
75727
+ if (!args || !args.key) return fail$1("memory_get requires a 'key'.");
75728
+ const value = memory.get(scope, id, args.key);
75729
+ if (value === undefined)
75730
+ return ok$1(`No value stored for '${args.key}' (${scope}).`);
75731
+ return ok$1(typeof value === "string" ? value : JSON.stringify(value));
75732
+ }
75733
+ case "memory_list": {
75734
+ const entries = memory.list(scope, id, args && args.prefix);
75735
+ if (!entries.length) return ok$1(`No memory keys stored (${scope}).`);
75736
+ return ok$1(entries.map((e) => `${e.key} (v${e.version})`).join("\n"));
75737
+ }
75738
+ case "memory_delete": {
75739
+ if (!args || !args.key) return fail$1("memory_delete requires a 'key'.");
75740
+ const removed = memory.delete(scope, id, args.key);
75741
+ return ok$1(
75742
+ removed
75743
+ ? `Deleted '${args.key}' (${scope}).`
75744
+ : `No value to delete for '${args.key}' (${scope}).`,
75745
+ );
75746
+ }
75747
+ default:
75748
+ return fail$1(`Unknown memory tool '${toolName}'.`);
75749
+ }
75750
+ } catch (e) {
75751
+ return fail$1(`Memory tool error: ${(e && e.message) || e}`);
75752
+ }
75753
+ }
75754
+
75755
+ var memoryTools = { MEMORY_SERVER: MEMORY_SERVER$1, MEMORY_TOOLS: MEMORY_TOOLS$1, handleMemoryTool: handleMemoryTool$1 };
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
+
75494
75843
  /**
75495
75844
  * mcpResult.js
75496
75845
  *
@@ -75557,6 +75906,14 @@ const BotRunner = BotRunner_1;
75557
75906
  const BotScheduler = BotScheduler_1;
75558
75907
  const BudgetController = BudgetControllerExports;
75559
75908
  const PauseController = PauseController_1;
75909
+ const { BotMemory } = BotMemory_1;
75910
+ const {
75911
+ MEMORY_SERVER,
75912
+ MEMORY_TOOLS,
75913
+ handleMemoryTool,
75914
+ } = memoryTools;
75915
+ const { matchSubscribedBots } = eventMatcher;
75916
+ const { EventDispatcher } = EventDispatcher_1;
75560
75917
  const { normalizeMcpResult } = mcpResult;
75561
75918
  const {
75562
75919
  getProvider: getProvider$1,
@@ -75578,6 +75935,25 @@ function pricingFor(providerId, model) {
75578
75935
  return (m && m.pricing) || null;
75579
75936
  }
75580
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
+
75581
75957
  const botController$1 = {
75582
75958
  _ready: false,
75583
75959
 
@@ -75633,10 +76009,20 @@ const botController$1 = {
75633
76009
  getPricing: pricingFor,
75634
76010
  });
75635
76011
 
76012
+ // Scoped, durable bot memory (P1: FR-010), served as in-process memory_*
76013
+ // tools that every bot can use without a consent prompt.
76014
+ this._memory = new BotMemory({ persistence: host.memoryPersistence });
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
+
75636
76020
  this._runner = new BotRunner({
75637
76021
  engines,
75638
76022
  store: this._store,
75639
76023
  approvals: approvalsForRunner,
76024
+ // Memory tools are the bot's own sandbox — auto-allowed at the gate.
76025
+ internalServers: [MEMORY_SERVER],
75640
76026
  resolveRunProfile: (bot) => this._resolveRunProfile(bot),
75641
76027
  resolveTools: (bot) => this._resolveTools(bot),
75642
76028
  callTool: (serverName, toolName, args, o) =>
@@ -75686,6 +76072,30 @@ const botController$1 = {
75686
76072
  }
75687
76073
  },
75688
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
+
75689
76099
  // ---- IPC-facing operations ---------------------------------------------
75690
76100
 
75691
76101
  list() {
@@ -75869,10 +76279,24 @@ const botController$1 = {
75869
76279
  toolServer[tool.name] = server.serverName;
75870
76280
  }
75871
76281
  }
76282
+ // Always-available in-process memory tools (P1: FR-010).
76283
+ for (const tool of MEMORY_TOOLS) {
76284
+ tools.push(tool);
76285
+ toolServer[tool.name] = MEMORY_SERVER;
76286
+ }
75872
76287
  return { tools, resolveServer: (toolName) => toolServer[toolName] || null };
75873
76288
  },
75874
76289
 
75875
76290
  async _callTool(serverName, toolName, args, opts = {}) {
76291
+ // In-process memory tools bypass MCP; scoped to the calling bot's workspace.
76292
+ if (serverName === MEMORY_SERVER) {
76293
+ return handleMemoryTool(
76294
+ this._memory,
76295
+ { workspaceId: opts.workspaceId },
76296
+ toolName,
76297
+ args,
76298
+ );
76299
+ }
75876
76300
  const win = this._getMainWindow();
75877
76301
  // widgetId + token are null → the widget permission gate is bypassed; bot
75878
76302
  // tool calls are gated upstream by the bot PermissionGate.